from __future__ import annotations

import json
import os
import time
from dataclasses import asdict
from datetime import datetime
from typing import Dict, List, Optional, Tuple

import pandas as pd

from config import (
    COIN_TELEGRAM_CHAT_ID,
    COIN_TELEGRAM_BOT_TOKEN,
    KR_COINS,
)
from monitor_min import Monitor
from simulation_1min import (
    DEFAULT_DB_PATH,
    DEFAULT_ENTRY_COMBOS,
    EntrySignalEngine,
    IndicatorBuilder,
    MinuteDataLoader,
    StrategyOptimizer,
    StrategyParams,
    generate_entry_combos,
    get_tick_size,
    load_strategy_mode,
    MIN_ORDER_KRW,
    INITIAL_CAPITAL,
    MAX_POSITION_KRW,
)


class LiveOneMinuteStrategy(Monitor):
    """공통 1분봉 실전 매매 모니터 베이스 클래스."""

    def __init__(
        self,
        coins: Dict[str, str],
        cooldown_file: str,
        position_file: str,
        entry_max_len: int = 3,
        grid_limit: int = 4,
        random_trials: int = 4,
        sleep_seconds: float = 0.6,
        max_order_krw: int = 200_000,
    ) -> None:
        super().__init__(cooldown_file)
        self.coins = coins
        self.position_file = position_file
        self.grid_limit = grid_limit
        self.random_trials = random_trials
        self.sleep_seconds = sleep_seconds
        self.max_order_krw = max_order_krw
        self.loader = MinuteDataLoader(DEFAULT_DB_PATH)
        self.indicator_builder = IndicatorBuilder()
        self.use_full = load_strategy_mode()
        self.entry_combos = (
            generate_entry_combos(entry_max_len) if self.use_full else list(DEFAULT_ENTRY_COMBOS)
        )
        self.positions = self._load_positions()

    # ------------------------------------------------------------------ #
    # Position persistence
    # ------------------------------------------------------------------ #
    def _load_positions(self) -> Dict[str, dict]:
        if not os.path.exists(self.position_file):
            return {}
        try:
            with open(self.position_file, "r", encoding="utf-8") as f:
                raw = json.load(f)
        except Exception:
            return {}
        return raw if isinstance(raw, dict) else {}

    def _save_positions(self) -> None:
        os.makedirs(os.path.dirname(self.position_file), exist_ok=True)
        with open(self.position_file, "w", encoding="utf-8") as f:
            json.dump(self.positions, f, ensure_ascii=False, indent=2)

    # ------------------------------------------------------------------ #
    # Data / Optimization helpers
    # ------------------------------------------------------------------ #
    def _fetch_enriched_data(self, symbol: str) -> Optional[pd.DataFrame]:
        """
        모니터링 전용 1분봉 데이터 로딩.
        1) 실시간 + 저장 데이터를 혼합하는 monitor_min.get_coin_some_data를 우선 사용
        2) 실패 시 DB MinuteDataLoader → API fallback 순으로 보완
        """

        def normalize(df: Optional[pd.DataFrame]) -> Optional[pd.DataFrame]:
            if df is None or df.empty:
                return None
            normalized = df.copy()
            if "datetime" not in normalized.columns:
                normalized["datetime"] = normalized.index
            normalized = normalized.reset_index(drop=True)
            return normalized

        source_df = None

        # 1) monitor_min 방식 (API + DB 혼합)
        try:
            mix_df = self.get_coin_some_data(symbol, 1)
            source_df = normalize(mix_df)
        except Exception:
            source_df = None

        # 2) DB MinuteDataLoader
        if source_df is None:
            try:
                loader_df = self.loader.load(symbol, limit=9000)
                source_df = normalize(loader_df)
            except Exception:
                source_df = None

        # 3) API fallback (get_coin_more_data)
        if source_df is None:
            try:
                api_df = self.get_coin_more_data(symbol, 1, bong_count=4000).reset_index()
                api_df.rename(columns={"index": "datetime"}, inplace=True)
                source_df = api_df
            except Exception:
                return None

        if source_df is None or source_df.empty:
            return None

        try:
            enriched = self.indicator_builder.enrich(source_df)
        except Exception:
            return None

        if "datetime" not in enriched.columns and "datetime" in source_df.columns:
            enriched["datetime"] = source_df["datetime"]

        return enriched if len(enriched) > 500 else None

    def _optimize_strategy(self, symbol: str, data: pd.DataFrame) -> StrategyParams:
        optimizer = StrategyOptimizer(data, INITIAL_CAPITAL, entry_combos=self.entry_combos)
        best_candidates = []
        try:
            grid = optimizer.grid_search(limit=self.grid_limit)
            if grid:
                best_candidates.extend(grid[:2])
        except Exception:
            pass

        try:
            rand = optimizer.random_search(trials=self.random_trials)
            if rand:
                best_candidates.extend(rand[:2])
        except Exception:
            pass

        if not best_candidates:
            # fallback to default params
            return StrategyParams()

        best_overall = max(best_candidates, key=lambda r: r.metrics.get("CAGR", float("-inf")))
        return best_overall.params

    # ------------------------------------------------------------------ #
    # Live trading helpers
    # ------------------------------------------------------------------ #
    def _can_buy(self, symbol: str) -> bool:
        last_buy_dt = self.buy_cooldown.get(symbol, {}).get("buy", {}).get("datetime")
        if last_buy_dt and isinstance(last_buy_dt, datetime):
            diff = datetime.now() - last_buy_dt
            if diff.total_seconds() < 60:  # 최소 1분 쿨다운
                return False
        return True

    def _record_buy(self, symbol: str, signal: str) -> None:
        self.buy_cooldown.setdefault(symbol, {})["buy"] = {
            "datetime": datetime.now(),
            "signal": signal,
        }
        self._save_buy_cooldown()

    def _record_sell(self, symbol: str, signal: str) -> None:
        self.buy_cooldown.setdefault(symbol, {})["sell"] = {
            "datetime": datetime.now(),
            "signal": signal,
        }
        self._save_buy_cooldown()

    def _determine_buy_amount(self, params: StrategyParams, current_price: float) -> float:
        alloc = max(MIN_ORDER_KRW, INITIAL_CAPITAL * params.risk_pct)
        alloc = min(alloc, self.max_order_krw, MAX_POSITION_KRW)
        return alloc

    def _evaluate_entry(self, data: pd.DataFrame, params: StrategyParams) -> bool:
        if len(data) < params.ma_slow + 10:
            return False
        engine = EntrySignalEngine(params)
        entry_idx = len(data) - 2
        return engine.evaluate(data, entry_idx)

    def _update_position_record(
        self,
        symbol: str,
        position: dict,
        field: str,
        value,
    ) -> None:
        position[field] = value
        self.positions[symbol] = position

    def _get_recent_timestamp(self, data: pd.DataFrame) -> str:
        ts = data["datetime"].iloc[-1]
        return ts.isoformat() if isinstance(ts, pd.Timestamp) else str(ts)

    def _evaluate_exit(
        self,
        symbol: str,
        data: pd.DataFrame,
        position: dict,
    ) -> Tuple[bool, float, str, dict]:
        params = StrategyParams(**position.get("params", {})) if position.get("params") else StrategyParams()
        engine = EntrySignalEngine(params)
        idx = len(data) - 1
        low = data["Low"].iloc[idx]
        high = data["High"].iloc[idx]
        close = data["Close"].iloc[idx]
        macro_trend = data.get("macro_trend", pd.Series([0])).iloc[idx]
        entry_price = position["entry_price"]
        best_price = max(position.get("best_price", entry_price), high)
        atr = position.get("atr", data["atr14"].iloc[idx])
        trailing_mult = position.get("trailing_mult", params.trailing_atr_mult)

        stop_price = entry_price * (1 - position.get("stop_loss_pct", params.stop_loss_pct))
        take_price = entry_price * (1 + position.get("take_profit_pct", params.take_profit_pct))
        trail_price = best_price - atr * trailing_mult
        time_stop_bars = position.get("time_stop_bars", params.time_stop_bars)
        vol_drop_threshold = position.get("vol_drop_exit_z", params.vol_drop_exit_z)

        sell_reason = ""
        exec_price = close
        executed = False

        if low <= stop_price:
            exec_price = stop_price - get_tick_size(stop_price)
            sell_reason = "stop_loss"
            executed = True
        elif high >= take_price:
            exec_price = take_price - get_tick_size(take_price)
            sell_reason = "take_profit"
            executed = True
        elif low <= trail_price:
            exec_price = trail_price - get_tick_size(trail_price)
            sell_reason = "trailing_stop"
            executed = True
        else:
            entry_time = position.get("entry_time")
            if entry_time:
                try:
                    entry_dt = datetime.fromisoformat(entry_time)
                    bars_held = max(
                        1,
                        int((data["datetime"].iloc[idx] - entry_dt).total_seconds() / 60),
                    )
                except Exception:
                    bars_held = time_stop_bars + 1
            else:
                bars_held = time_stop_bars + 1

            if bars_held >= time_stop_bars:
                sell_reason = "time_stop"
                executed = True
            elif (
                vol_drop_threshold is not None
                and data.get("volume_z") is not None
                and data["volume_z"].iloc[idx] <= vol_drop_threshold
            ):
                sell_reason = "volume_drop"
                executed = True
            else:
                reverse_idx = max(0, len(data) - 2)
                if engine.evaluate(data, reverse_idx):
                    sell_reason = "reverse_signal"
                    executed = True

        if executed:
            sell_ratio = 0.5 if macro_trend >= 0 else 1.0
            qty = position["qty"] * sell_ratio
            qty = max(qty, 1e-8)
            return True, qty, sell_reason, {"best_price": best_price}

        return False, 0.0, "", {"best_price": best_price}

    # ------------------------------------------------------------------ #
    # Main monitoring routine
    # ------------------------------------------------------------------ #
    def monitor_once(self) -> None:
        balances = {}
        try:
            for bal in self.getBalances():
                balances[bal["currency"]] = float(bal.get("balance", 0))
        except Exception:
            pass

        for symbol in self.coins.keys():
            try:
                data = self._fetch_enriched_data(symbol)
                if data is None:
                    print(f"{symbol}: 데이터 부족으로 스킵")
                    continue
                params = self._optimize_strategy(symbol, data)

                # Exit handling first
                if symbol in self.positions:
                    should_exit, qty, reason, updates = self._evaluate_exit(symbol, data, self.positions[symbol])
                    if should_exit and qty > 0:
                        self._execute_sell(symbol, qty, data["Close"].iloc[-1], reason)
                        if qty >= self.positions[symbol]["qty"]:
                            self.positions.pop(symbol, None)
                        else:
                            self.positions[symbol]["qty"] -= qty
                            self.positions[symbol].update(updates)
                        self._record_sell(symbol, reason)
                        self._save_positions()

                # Entry logic
                if symbol not in self.positions and self._can_buy(symbol):
                    if self._evaluate_entry(data, params):
                        entry_price = data["Close"].iloc[-1] + get_tick_size(data["Close"].iloc[-1])
                        buy_amount = self._determine_buy_amount(params, entry_price)
                        qty = max(buy_amount / entry_price, 0)
                        if qty <= 0:
                            continue
                        self._execute_buy(symbol, buy_amount, entry_price, params)
                        position_payload = {
                            "entry_price": entry_price,
                            "qty": qty,
                            "atr": data["atr14"].iloc[-1],
                            "best_price": entry_price,
                            "entry_time": self._get_recent_timestamp(data),
                            "stop_loss_pct": params.stop_loss_pct,
                            "take_profit_pct": params.take_profit_pct,
                            "trailing_mult": params.trailing_atr_mult,
                            "time_stop_bars": params.time_stop_bars,
                            "vol_drop_exit_z": params.vol_drop_exit_z,
                            "params": asdict(params),
                        }
                        self.positions[symbol] = position_payload
                        self._record_buy(symbol, "+".join(params.entry_combo))
                        self._save_positions()
                print("{} {} ({})".format(symbol, data['datetime'].iloc[-1], len(data['datetime'])))
            except Exception as exc:
                print(f"[{symbol}] 오류: {exc}")
            finally:
                time.sleep(self.sleep_seconds)

    def _execute_buy(self, symbol: str, amount_krw: float, entry_price: float, params: StrategyParams) -> None:
        try:
            self.hts.buyCoinMarket(symbol, amount_krw)
            print(
                f"[BUY] {symbol} amount={amount_krw:,.0f}KRW price=₩{entry_price:,.2f} "
                f"strategy={'+'.join(params.entry_combo)}"
            )
            msg = (
                f"[KRW-COIN]\n"
                f"• 매수 {symbol} : {amount_krw:,.0f}원, 가격 ₩{entry_price:,.2f}\n"
                f"• 전략 { '+'.join(params.entry_combo) }"
            )
            self.sendMsg(msg)
        except Exception as exc:
            print(f"{symbol} 매수 실패: {exc}")

    def _execute_sell(self, symbol: str, qty: float, current_price: float, reason: str) -> None:
        try:
            self.hts.sellCoinMarket(symbol, 0, qty)
            print(f"[SELL] {symbol} qty={qty:.6f} price=₩{current_price:,.2f} reason={reason}")
            msg = (
                f"[KRW-COIN]\n"
                f"• 매도 {symbol} : 수량 {qty:.6f}, 가격 ₩{current_price:,.2f}\n"
                f"• 사유 {reason}"
            )
            self.sendMsg(msg)
        except Exception as exc:
            print(f"{symbol} 매도 실패: {exc}")

    def run_schedule(self, interval_seconds: int = 15) -> None:
        while True:
            self.monitor_once()
            time.sleep(interval_seconds)


def data_timestamp_to_iso(data: pd.DataFrame) -> str:
    pass

