"""
3단계: monitor_rules 발화 시 빗썸 실주문 (가드·로그).

dry-run·live 체결 배분: 시뮬 sim_causal_hybrid 와 동일 (hybrid_sim_execution).
"""

from __future__ import annotations

import json
import time
from datetime import date, datetime
from pathlib import Path
from typing import Any

from config import (
    CHART_LOOKBACK_DAYS,
    COIN_NAME,
    GT_INITIAL_CASH_KRW,
    LIVE_COOLDOWN_MIN,
    LIVE_DAILY_KRW_MAX,
    LIVE_DAILY_LOSS_LIMIT_KRW,
    LIVE_MAX_TRADES_PER_DAY,
    LIVE_ORDER_KRW,
    LIVE_TRADING_ENABLED,
    MATCH_PRIMARY_INTERVAL,
    SYMBOL,
    TRADING_FEE_RATE,
)
from deepcoin.data.mtf_bb import load_frames_from_db
from deepcoin.ground_truth.ground_truth import load_ground_truth
from deepcoin.matching.live_eval import evaluate_live_rules
from deepcoin.matching.live_sizing import LivePositionState, live_sizing_enabled
from deepcoin.matching.load_rules import load_monitor_rules
from deepcoin.matching.position_sizing import (
    load_ev_wf_approved_rule_ids,
    top_leg_ids_by_forward_return,
)
from deepcoin.ops.alert_message import build_rule_alert_message
from deepcoin.ops.hybrid_sim_execution import (
    hit_key,
    plan_live_hit,
    replay_paper_portfolio,
    sort_hits_sim_order,
)
from deepcoin.ops.monitor import Monitor
from deepcoin.ops.paper_portfolio import PaperPortfolio
from deepcoin.paths import (
    LIVE_SIGNAL_HISTORY_JSON,
    LIVE_TRADES_LOG,
    PAPER_FIRES_LOG,
    resolve_ground_truth_file,
)


class LiveTrader(Monitor):
    """
    규칙 발화 시 실거래 실행. LIVE_TRADING_ENABLED=0 이면 모의(sim hybrid)만.
    """

    def __init__(self) -> None:
        """Monitor 초기화, 일별 카운터 비움."""
        super().__init__(cooldown_file=None)
        self._rule_last_unix: dict[str, float] = {}
        self._day: str = ""
        self._day_spent_krw: float = 0.0
        self._day_trades: int = 0
        self._day_pnl_krw: float = 0.0
        self._gt_trades: list[dict] = []
        self._large_legs: set[int] = set()
        self._approved_rules: set[str] = set()
        self._position_state = LivePositionState.load()
        self._paper = PaperPortfolio.load() if not LIVE_TRADING_ENABLED else None
        self._live_signal_history: list[dict[str, Any]] = []
        self._ohlc_df = None
        self._load_sizing_context()
        if self._paper_mode and self._paper.signal_history:
            self._resync_paper_from_sim()
        if LIVE_TRADING_ENABLED:
            self._live_signal_history = self._load_live_signal_history()

    @property
    def _paper_mode(self) -> bool:
        """dry-run: 모의 계좌·시뮬 hybrid 체결."""
        return not LIVE_TRADING_ENABLED and self._paper is not None

    def _load_live_signal_history(self) -> list[dict[str, Any]]:
        """live 시뮬 정합용 발화 이력."""
        if not LIVE_SIGNAL_HISTORY_JSON.is_file():
            return []
        try:
            data = json.loads(LIVE_SIGNAL_HISTORY_JSON.read_text(encoding="utf-8"))
            return list(data.get("signals") or [])
        except (json.JSONDecodeError, OSError):
            return []

    def _save_live_signal_history(self) -> None:
        """live 발화 이력 저장."""
        LIVE_SIGNAL_HISTORY_JSON.parent.mkdir(parents=True, exist_ok=True)
        LIVE_SIGNAL_HISTORY_JSON.write_text(
            json.dumps(
                {"signals": self._live_signal_history[-2000:]},
                ensure_ascii=False,
                indent=2,
            ),
            encoding="utf-8",
        )

    def _live_signal_seen(self, hit: dict[str, Any]) -> bool:
        """live 이력에 동일 봉 발화가 있는지."""
        dt, rid, side = hit_key(hit)
        return any(
            str(s["dt"]) == dt and str(s["rule_id"]) == rid and str(s["side"]) == side
            for s in self._live_signal_history
        )

    def _append_live_signal(self, hit: dict[str, Any]) -> None:
        """live 발화 이력 추가."""
        if self._live_signal_seen(hit):
            return
        self._live_signal_history.append(
            {
                "dt": str(hit["dt"]),
                "rule_id": str(hit["rule_id"]),
                "side": str(hit["side"]),
                "close": float(hit["close"]),
            }
        )

    def _balances_for_trading(self) -> dict[str, dict[str, float]] | None:
        """
        dry-run: paper_portfolio만. live: 빗썸 API.
        """
        if self._paper_mode:
            return self._paper.balances_dict()
        try:
            return self.load_balances_dict()
        except Exception:
            return None

    def _load_sizing_context(self) -> None:
        """GT leg·EV/WF 통과 규칙 캐시."""
        gt = load_ground_truth(resolve_ground_truth_file()) or {}
        self._gt_trades = gt.get("trades") or []
        self._large_legs = top_leg_ids_by_forward_return(self._gt_trades)
        self._approved_rules = load_ev_wf_approved_rule_ids()

    def _reset_day_if_needed(self) -> None:
        """날짜 변경 시 일별 한도 카운터 초기화."""
        today = date.today().isoformat()
        if today != self._day:
            self._day = today
            self._day_spent_krw = 0.0
            self._day_trades = 0
            self._day_pnl_krw = 0.0

    def _append_log(self, record: dict[str, Any]) -> None:
        """live_trades.jsonl append."""
        LIVE_TRADES_LOG.parent.mkdir(parents=True, exist_ok=True)
        with LIVE_TRADES_LOG.open("a", encoding="utf-8") as f:
            f.write(json.dumps(record, ensure_ascii=False) + "\n")

    def _append_paper_fire(
        self,
        hit: dict[str, Any],
        planned_krw: float,
        would_trade: bool,
        skip_reason: str = "",
        order_log: dict[str, Any] | None = None,
    ) -> None:
        """Phase C paper_fires.jsonl."""
        PAPER_FIRES_LOG.parent.mkdir(parents=True, exist_ok=True)
        row = {
            "ts": datetime.now().isoformat(timespec="seconds"),
            "signal_dt": hit.get("dt"),
            "rule_id": hit.get("rule_id"),
            "side": hit.get("side"),
            "close": float(hit.get("close") or 0),
            "planned_krw": round(float(planned_krw), 0),
            "would_trade": bool(would_trade),
            "skip_reason": skip_reason or "",
            "live_enabled": bool(LIVE_TRADING_ENABLED),
            "order_message": (order_log or {}).get("message", ""),
            "sizing": "sim_causal_hybrid",
        }
        with PAPER_FIRES_LOG.open("a", encoding="utf-8") as f:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")

    def _can_trade(self, rule_id: str, planned_krw: float | None = None) -> tuple[bool, str]:
        """
        쿨다운(1봉=3분) + live 일한도. dry-run은 일한도만 생략.
        """
        self._reset_day_if_needed()
        last = self._rule_last_unix.get(rule_id, 0.0)
        if time.time() - last < LIVE_COOLDOWN_MIN * 60:
            return False, f"규칙 쿨다운({LIVE_COOLDOWN_MIN}분)"
        if self._paper_mode:
            return True, ""
        if self._day_trades >= LIVE_MAX_TRADES_PER_DAY:
            return False, "일 최대 거래 수 초과"
        need = float(planned_krw if planned_krw is not None else LIVE_ORDER_KRW)
        if self._day_spent_krw + need > LIVE_DAILY_KRW_MAX:
            return False, "일 주문 한도 초과"
        if self._day_pnl_krw <= -abs(LIVE_DAILY_LOSS_LIMIT_KRW):
            return False, "일 손실 한도 초과"
        return True, ""

    def _load_ohlc_df(self) -> None:
        """drawdown tier용 3m OHLC."""
        try:
            frames = load_frames_from_db(self, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
            self._ohlc_df = frames.get(MATCH_PRIMARY_INTERVAL)
        except Exception:
            self._ohlc_df = None

    def _resync_paper_from_sim(self) -> None:
        """기존 paper 잔고를 sim_causal_hybrid replay 로 맞춤."""
        if self._ohlc_df is None:
            self._load_ohlc_df()
        if self._ohlc_df is None or getattr(self._ohlc_df, "empty", True):
            return
        replayed, _ = replay_paper_portfolio(
            self._paper.signal_history,
            self._ohlc_df,
            approved_buy_rules=self._approved_rules,
        )
        self._paper.cash_krw = replayed.cash_krw
        self._paper.qty = replayed.qty
        self._paper.qty_by_leg = dict(replayed.qty_by_leg)
        self._paper.current_leg_id = replayed.current_leg_id
        self._paper.save()

    def _sim_plan(self, hit: dict[str, Any]) -> Any:
        """시뮬 hybrid 배분 1건."""
        if self._ohlc_df is None:
            self._load_ohlc_df()
        if self._paper_mode:
            hist = list(self._paper.signal_history)
        else:
            hist = list(self._live_signal_history)
        return plan_live_hit(
            hist,
            hit,
            self._ohlc_df,
            approved_buy_rules=self._approved_rules,
        )

    def _execute_live_order(self, hit: dict[str, Any], plan: Any) -> dict[str, Any]:
        """실거래: 시뮬 planned 금액·수량으로 API 주문."""
        side = hit["side"]
        price = float(hit["close"])
        record: dict[str, Any] = {
            "ts": datetime.now().isoformat(timespec="seconds"),
            "rule_id": hit["rule_id"],
            "side": side,
            "signal_dt": hit["dt"],
            "price": price,
            "amount_krw": plan.amount_krw,
            "live_enabled": True,
            "ok": False,
            "message": plan.message,
            "sizing": "sim_causal_hybrid",
        }
        if not plan.ok:
            return record

        try:
            if side == "buy":
                ok = self.buyCoinMarket(SYMBOL, int(plan.amount_krw), count=None)
                record["ok"] = bool(ok)
                record["message"] = "buyCoinMarket" if ok else "buy failed"
            elif side == "sell":
                bal = self.load_balances_dict().get(SYMBOL, {})
                held = float(bal.get("balance") or 0)
                if held <= 0:
                    record["message"] = "보유 없음"
                else:
                    sell_qty = min(float(plan.sell_qty), held)
                    if sell_qty <= 0:
                        record["message"] = "매도 수량 0"
                    else:
                        gross = sell_qty * price
                        record["amount_krw"] = round(gross, 0)
                        ok = self.sellCoinMarket(SYMBOL, int(price), sell_qty)
                        record["ok"] = bool(ok)
                        record["sell_qty"] = sell_qty
                        record["message"] = (
                            f"sell qty={sell_qty:.4f}" if ok else "sell failed"
                        )
                        if record["ok"] and live_sizing_enabled():
                            fee = gross * TRADING_FEE_RATE
                            self._position_state.record_sell(
                                gross, fee, full_close=(sell_qty >= held * 0.999)
                            )
                            self._position_state.save()
            else:
                record["message"] = f"unknown side {side}"
        except Exception as exc:
            record["message"] = str(exc)

        if record["ok"]:
            spent = float(record.get("amount_krw") or plan.amount_krw)
            self._day_spent_krw += spent
            self._day_trades += 1
            self._rule_last_unix[hit["rule_id"]] = time.time()
            if live_sizing_enabled() and side == "buy":
                fee = spent * TRADING_FEE_RATE
                self._position_state.record_buy(hit["dt"], price, spent, fee)
                self._position_state.save()
            self._append_live_signal(hit)
            self._save_live_signal_history()
        return record

    def _process_paper_batch(self, new_hits: list[dict[str, Any]]) -> None:
        """
        dry-run: 신규 발화를 이력에 넣고 시뮬 전체 재생 후 알림.
        """
        if not new_hits:
            return
        if self._ohlc_df is None:
            self._load_ohlc_df()
        for hit in new_hits:
            self._paper.append_signal(hit)

        replayed, results = replay_paper_portfolio(
            self._paper.signal_history,
            self._ohlc_df,
            approved_buy_rules=self._approved_rules,
        )
        self._paper.cash_krw = replayed.cash_krw
        self._paper.qty = replayed.qty
        self._paper.qty_by_leg = dict(replayed.qty_by_leg)
        self._paper.current_leg_id = replayed.current_leg_id

        for hit in new_hits:
            key = hit_key(hit)
            res = results.get(key)
            if res is None:
                self._paper.mark_processed(hit["rule_id"], hit["dt"])
                continue
            log = {
                "ok": res.ok,
                "message": res.message,
                "amount_krw": res.amount_krw,
                "sell_qty": res.sell_qty,
            }
            self._append_paper_fire(
                hit, res.amount_krw, res.ok, "" if res.ok else res.message, log
            )
            self._paper.mark_processed(hit["rule_id"], hit["dt"])
            print(f"  [{hit['side']}] {hit['rule_id']} @ {hit['dt']}")
            print(f"    order: {res.message} ok={res.ok}")
            if not res.ok:
                continue
            self._rule_last_unix[hit["rule_id"]] = time.time()
            post_balances = self._paper.balances_dict()
            msg = build_rule_alert_message(
                hit,
                post_balances,
                trade_krw=res.amount_krw,
                trade_qty=res.sell_qty if hit["side"] == "sell" else None,
            )
            sym = post_balances.get(SYMBOL, {})
            msg += (
                f"\n[모의잔고·체결후] 현금 {_fmt_paper_krw(sym.get('krw', 0))} · "
                f"보유 {float(sym.get('balance', 0)):.4f} {SYMBOL}"
            )
            msg += f"\n[체결] {res.message}"
            self._send_coin_msg(msg)
        self._paper.save()

    def run_once(self) -> None:
        """1회: 규칙 평가 → 시뮬 hybrid 체결 → 텔레그램."""
        rules = load_monitor_rules()
        print(
            f"[06] {datetime.now():%Y-%m-%d %H:%M:%S} "
            f"{COIN_NAME} live={'ON' if LIVE_TRADING_ENABLED else 'OFF'} "
            f"rules={len(rules)} · sim=hybrid · bar={MATCH_PRIMARY_INTERVAL}m"
        )
        if not rules:
            print("  monitor_rules 없음")
            return

        fired = evaluate_live_rules(rules, force_refresh=True)
        if not fired:
            print("  발화 없음")
            return

        if self._paper_mode:
            print(
                f"  [paper] 현금 ₩{self._paper.cash_krw:,.0f} · "
                f"보유 {self._paper.qty:.4f} {SYMBOL} "
                f"(초기 ₩{GT_INITIAL_CASH_KRW:,.0f})"
            )

        new_paper_hits: list[dict[str, Any]] = []
        for hit in sort_hits_sim_order(fired):
            rid = hit["rule_id"]
            if self._paper_mode and self._paper.already_processed(rid, hit["dt"]):
                print(f"  [{hit['side']}] {rid} @ {hit['dt']} (이미 처리)")
                continue
            if LIVE_TRADING_ENABLED and self._live_signal_seen(hit):
                continue

            if hit["side"] == "buy" and rid not in self._approved_rules:
                print(f"  [{hit['side']}] {rid} @ {hit['dt']}")
                print("    skip: EV/WF 미통과 규칙")
                if self._paper_mode:
                    self._append_paper_fire(hit, 0.0, False, "EV/WF 미통과 규칙")
                    self._paper.mark_processed(rid, hit["dt"])
                continue

            plan_preview = self._sim_plan(hit)
            ok, reason = self._can_trade(rid, plan_preview.amount_krw)
            if not ok:
                print(f"  [{hit['side']}] {rid} @ {hit['dt']}")
                print(f"    skip: {reason}")
                if self._paper_mode:
                    self._append_paper_fire(
                        hit, plan_preview.amount_krw, False, reason
                    )
                    self._paper.mark_processed(rid, hit["dt"])
                continue

            if self._paper_mode:
                new_paper_hits.append(hit)
                continue

            print(f"  [{hit['side']}] {rid} @ {hit['dt']}")
            log = self._execute_live_order(hit, plan_preview)
            self._append_log(log)
            print(f"    order: {log['message']} ok={log['ok']}")
            if not log["ok"]:
                continue
            balances = self._balances_for_trading()
            msg = build_rule_alert_message(
                hit,
                balances,
                trade_krw=float(log.get("amount_krw") or 0),
                trade_qty=float(log.get("sell_qty") or 0) or None,
            )
            if balances:
                sym = balances.get(SYMBOL, {})
                msg += (
                    f"\n[잔고] 현금 {_fmt_paper_krw(sym.get('krw', 0))} · "
                    f"보유 {float(sym.get('balance', 0)):.4f} {SYMBOL}"
                )
            msg += f"\n[체결] {log['message']}"
            self._send_coin_msg(msg)

        if self._paper_mode and new_paper_hits:
            self._process_paper_batch(new_paper_hits)

    def run_loop(self, sleep_sec: int) -> None:
        """상시 루프."""
        print(f"[06] 실거래 루프 시작 · sleep={sleep_sec}s")
        while True:
            self.run_once()
            time.sleep(sleep_sec)


def _fmt_paper_krw(value: float) -> str:
    """원화 표시."""
    return f"₩{float(value):,.0f}"
