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

체결 배분: 시뮬 sim_causal_hybrid 와 동일
  - fire_outcomes 부트스트랩 + hybrid_sim_execution.plan_live_hit
  - enhanced=False, hybrid DD tier, EV/WF 매수 필터
LIVE_TRADING_ENABLED=1 필수.
"""

from __future__ import annotations

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

from config import (
    CHART_LOOKBACK_DAYS,
    COIN_NAME,
    LIVE_COOLDOWN_MIN,
    LIVE_DAILY_KRW_MAX,
    LIVE_DAILY_LOSS_LIMIT_KRW,
    LIVE_HYBRID_BOOTSTRAP_FIRES,
    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.matching.live_eval import evaluate_live_rules
from deepcoin.matching.load_rules import load_monitor_rules
from deepcoin.ops.alert_message import build_rule_alert_message
from deepcoin.ops.hybrid_sim_execution import (
    SimTradeResult,
    build_live_signal_history,
    hit_key,
    plan_live_hit,
    sort_hits_sim_order,
)
from deepcoin.ops.monitor import Monitor
from deepcoin.paths import (
    LIVE_SIGNAL_HISTORY_JSON,
    LIVE_TRADES_LOG,
)


class LiveTrader(Monitor):
    """
    규칙 발화 시 빗썸 실주문. 배분은 시뮬 sim_causal_hybrid 와 동일 엔진.
    """

    def __init__(self) -> None:
        """Monitor 초기화, hybrid 이력·일별 카운터."""
        if not LIVE_TRADING_ENABLED:
            raise RuntimeError(
                "LIVE_TRADING_ENABLED=0 — 실거래만 지원합니다. "
                ".env 에 LIVE_TRADING_ENABLED=1 설정 후 재기동하세요."
            )
        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._ohlc_df = None
        self._persisted_ops_signals: list[dict[str, Any]] = self._load_persisted_signals()
        self._live_signal_history = self._init_signal_history()
        self._load_ohlc_df()

    def _load_persisted_signals(self) -> list[dict[str, Any]]:
        """live_signal_history.json."""
        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 _init_signal_history(self) -> list[dict[str, Any]]:
        """
        시뮬과 동일 hybrid 입력: fire_outcomes 부트스트랩 + 운영 저장분.

        Returns:
            병합된 발화 이력.
        """
        merged = build_live_signal_history(self._persisted_ops_signals)
        n_boot = max(len(merged) - len(self._persisted_ops_signals), 0)
        print(
            f"[06] hybrid 이력: total={len(merged)} "
            f"(bootstrap={'on' if LIVE_HYBRID_BOOTSTRAP_FIRES else 'off'}, "
            f"from_fires~{n_boot}, ops_persisted={len(self._persisted_ops_signals)})"
        )
        return merged

    def _save_persisted_ops_signals(self) -> None:
        """운영 체결분만 저장 (fire_outcomes 부트스트랩은 재로드)."""
        LIVE_SIGNAL_HISTORY_JSON.parent.mkdir(parents=True, exist_ok=True)
        LIVE_SIGNAL_HISTORY_JSON.write_text(
            json.dumps(
                {"signals": self._persisted_ops_signals[-2000:]},
                ensure_ascii=False,
                indent=2,
            ),
            encoding="utf-8",
        )

    def _live_signal_seen(self, hit: dict[str, Any]) -> bool:
        """이력에 동일 봉 발화가 있는지."""
        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:
        """체결 성공 발화를 전체 이력·운영 저장분에 추가."""
        if self._live_signal_seen(hit):
            return
        row = {
            "dt": str(hit["dt"]),
            "rule_id": str(hit["rule_id"]),
            "side": str(hit["side"]),
            "close": float(hit["close"]),
        }
        self._live_signal_history.append(row)
        if not any(hit_key(s) == hit_key(row) for s in self._persisted_ops_signals):
            self._persisted_ops_signals.append(row)

    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 _can_trade(self, rule_id: str, planned_krw: float | None = None) -> tuple[bool, str]:
        """
        쿨다운(1봉=3분) + 일한도·손실한도·거래횟수.

        Args:
            rule_id: 규칙 ID.
            planned_krw: 예정 매수 원화.

        Returns:
            (허용 여부, 거절 사유).
        """
        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._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 _sim_plan(self, hit: dict[str, Any]) -> SimTradeResult:
        """시뮬 hybrid 배분 1건 (누적 이력·인과, 현금·보유 제약)."""
        if self._ohlc_df is None:
            self._load_ohlc_df()
        return plan_live_hit(
            list(self._live_signal_history),
            hit,
            self._ohlc_df,
            approved_buy_rules=None,
        )

    @staticmethod
    def _cap_plan_to_exchange(plan: SimTradeResult, hit: dict[str, Any], balances: dict) -> SimTradeResult:
        """
        시뮬 planned 금액을 거래소 가용 잔고 이내로 제한.

        Args:
            plan: hybrid 시뮬 배분 결과.
            hit: 발화.
            balances: load_balances_dict() 결과.

        Returns:
            조정된 SimTradeResult.
        """
        sym = balances.get(SYMBOL, {})
        price = float(hit["close"])
        side = hit["side"]

        if not plan.ok or plan.amount_krw <= 0:
            return plan

        if side == "buy":
            krw = float(sym.get("krw") or 0)
            max_buy = max(krw / (1.0 + TRADING_FEE_RATE) - 1.0, 0.0)
            capped = min(float(plan.amount_krw), max_buy)
            if capped <= 0:
                return SimTradeResult(
                    plan.hit, 0.0, 0.0, False, "거래소 현금 부족(시뮬 대비)"
                )
            if capped < plan.amount_krw - 1.0:
                return SimTradeResult(
                    plan.hit,
                    round(capped, 0),
                    0.0,
                    True,
                    f"sim_buy capped ₩{capped:,.0f} (plan ₩{plan.amount_krw:,.0f})",
                    leg_id=plan.leg_id,
                )
            return plan

        held = float(sym.get("balance") or 0)
        if held <= 0:
            return SimTradeResult(plan.hit, 0.0, 0.0, False, "거래소 보유 없음")
        sell_qty = min(float(plan.sell_qty), held)
        if sell_qty <= 0:
            return SimTradeResult(plan.hit, 0.0, 0.0, False, "매도 수량 0")
        gross = round(sell_qty * price, 0)
        if sell_qty < plan.sell_qty - 1e-8:
            return SimTradeResult(
                plan.hit,
                gross,
                sell_qty,
                True,
                f"sim_sell capped qty={sell_qty:.4f}",
                leg_id=plan.leg_id,
            )
        return plan

    def _execute_live_order(
        self, hit: dict[str, Any], plan: SimTradeResult, balances: dict
    ) -> dict[str, Any]:
        """실거래: 시뮬 plan(잔고 cap)으로 API 주문."""
        side = hit["side"]
        price = float(hit["close"])
        plan = self._cap_plan_to_exchange(plan, hit, balances)
        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":
                sell_qty = float(plan.sell_qty)
                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"
                )
            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()
            self._append_live_signal(hit)
            self._save_persisted_ops_signals()
        return record

    def run_once(self) -> None:
        """1회: 규칙 평가 → hybrid 배분 → 빗썸 주문 → 텔레그램."""
        from deepcoin.data.ops_sync import ensure_ops_candles

        ensure_ops_candles()
        rules = load_monitor_rules()
        print(
            f"[06] {datetime.now():%Y-%m-%d %H:%M:%S} "
            f"{COIN_NAME} LIVE rules={len(rules)} · sim=hybrid · bar={MATCH_PRIMARY_INTERVAL}m"
        )
        if not rules:
            print("  monitor_rules 없음 — scripts/04_match_rules.py 실행")
            return

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

        try:
            balances = self.load_balances_dict()
        except Exception:
            balances = {}

        for hit in sort_hits_sim_order(fired):
            rid = hit["rule_id"]
            if self._live_signal_seen(hit):
                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}")
                continue

            if not plan_preview.ok:
                print(f"  [{hit['side']}] {rid} @ {hit['dt']}")
                print(f"    skip: {plan_preview.message}")
                continue

            print(f"  [{hit['side']}] {rid} @ {hit['dt']}")
            log = self._execute_live_order(hit, plan_preview, balances)
            self._append_log(log)
            print(f"    order: {log['message']} ok={log['ok']}")
            if not log["ok"]:
                continue
            try:
                balances = self.load_balances_dict()
            except Exception:
                balances = None
            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[잔고] 현금 ₩{float(sym.get('krw', 0)):,.0f} · "
                    f"보유 {float(sym.get('balance', 0)):.4f} {SYMBOL}"
                )
            msg += f"\n[체결] {log['message']}"
            self._send_coin_msg(msg)

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