"""
kis_trader/ws/trigger_eval_recorder.py — TRIGGER 필터 판정 시점 스냅샷 DB 저장
=============================================================================
실매 ``orderbook_reject_for_entry`` / ``program_reject_for_entry`` 가 RAM 에서
본 스냅샷을 그대로 ws_orderbook / ws_program 에 저장 (source=filter_eval).
백테는 이 기록만 재생 → 3초 주기 WS 덤프보다 실매와 일치.
"""
from __future__ import annotations

import datetime as dt
import queue
import threading
import time
from typing import Any, Dict, List, Optional

from ..utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
from ..utils.logger import get_logger
from .orderbook_cache import OrderbookSnapshot
from .program_cache import ProgramSnapshot

logger = get_logger("kis_trader.trigger_eval_recorder")

FILTER_EVAL_SOURCE = "filter_eval"

_RECORDER: Optional["TriggerEvalRecorder"] = None
_RECORDER_LOCK = threading.Lock()


def get_trigger_eval_recorder() -> Optional["TriggerEvalRecorder"]:
    with _RECORDER_LOCK:
        return _RECORDER


def set_trigger_eval_recorder(recorder: Optional["TriggerEvalRecorder"]) -> None:
    global _RECORDER
    with _RECORDER_LOCK:
        _RECORDER = recorder


class TriggerEvalRecorder:
    """필터 판정 직후 호가·프로그램 스냅샷 배치 INSERT."""

    def __init__(self, db=None) -> None:
        self.db = db
        self._enabled = get_env_bool("WS_TRIGGER_EVAL_SAVE_ENABLED", True)
        self._batch_size = get_env_int("WS_TRIGGER_EVAL_DB_BATCH_SIZE", 100)
        self._flush_sec = get_env_float("WS_TRIGGER_EVAL_DB_FLUSH_SEC", 2.0)
        self._queue_max = get_env_int("WS_TRIGGER_EVAL_WRITE_QUEUE_MAX", 20000)
        self._ob_keep_days = get_env_int("WS_ORDERBOOK_KEEP_DAYS", 7)
        self._pg_keep_days = get_env_int("WS_PROGRAM_KEEP_DAYS", 7)
        self._default_market = (
            get_env_from_db("WS_TICK_DEFAULT_MARKET", "KR") or "KR"
        ).strip().upper()

        self._write_queue: queue.Queue = queue.Queue(maxsize=max(500, self._queue_max))
        self._writer_thread: Optional[threading.Thread] = None
        self._last_cleanup_ts = 0.0
        self._drops = 0

        if self._enabled and self.db is not None:
            self._ensure_tables()
            self._start_writer()
            set_trigger_eval_recorder(self)
            logger.info(
                "✅ TriggerEvalRecorder 활성 (batch=%d flush=%.1fs keep=%d/%dd)",
                self._batch_size, self._flush_sec,
                self._ob_keep_days, self._pg_keep_days,
            )
        elif self._enabled and self.db is None:
            logger.warning("⚠️ TriggerEvalRecorder: db=None → DB 저장 비활성")
        else:
            logger.info("ℹ️ TriggerEvalRecorder 비활성 (WS_TRIGGER_EVAL_SAVE_ENABLED=false)")

    def on_orderbook_eval(
        self,
        *,
        strategy: str,
        code: str,
        snap: OrderbookSnapshot,
        reject_code: Optional[str],
        reject_msg: Optional[str],
        current_price: float,
        snap_time: Optional[str] = None,
    ) -> None:
        """호가 필터 판정 직후 — RAM 스냅샷 그대로 큐 적재."""
        if not self._enabled or self.db is None or snap is None:
            return
        code = (code or snap.code or "").strip()
        if not code:
            return
        st = self._normalize_snap_time(snap_time, snap.ts)
        body = snap.to_storage_dict()
        item = {
            "kind": "orderbook",
            "market": self._default_market[:8],
            "code": code[:32],
            "snap_time": st,
            "best_bid": int(body.get("best_bid") or 0),
            "best_ask": int(body.get("best_ask") or 0),
            "total_bid_qty": int(body.get("total_bid_qty") or 0),
            "total_ask_qty": int(body.get("total_ask_qty") or 0),
            "bid_qty_l3": int(body.get("bid_qty_l3") or 0),
            "ask_qty_l3": int(body.get("ask_qty_l3") or 0),
            "levels_json": body.get("levels_json") or "{}",
            "source": FILTER_EVAL_SOURCE,
            "recv_ts": dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "strategy": (strategy or "")[:16],
            "reject_code": (reject_code or "")[:64] or None,
            "reject_msg": (reject_msg or "")[:255] or None,
            "eval_price": int(current_price or 0),
        }
        self._enqueue(item)

    def on_program_eval(
        self,
        *,
        strategy: str,
        code: str,
        snap: ProgramSnapshot,
        reject_code: Optional[str],
        reject_msg: Optional[str],
        current_price: float = 0.0,
        snap_time: Optional[str] = None,
    ) -> None:
        """프로그램 필터 판정 직후."""
        if not self._enabled or self.db is None or snap is None:
            return
        code = (code or snap.code or "").strip()
        if not code:
            return
        st = self._normalize_snap_time(snap_time, snap.ts)
        body = snap.to_storage_dict()
        item = {
            "kind": "program",
            "market": self._default_market[:8],
            "code": code[:32],
            "snap_time": st,
            "buy_qty": int(body.get("buy_qty") or 0),
            "sell_qty": int(body.get("sell_qty") or 0),
            "net_qty": int(body.get("net_qty") or 0),
            "buy_amt": int(body.get("buy_amt") or 0),
            "sell_amt": int(body.get("sell_amt") or 0),
            "net_amt": int(body.get("net_amt") or 0),
            "source": FILTER_EVAL_SOURCE,
            "recv_ts": dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "strategy": (strategy or "")[:16],
            "reject_code": (reject_code or "")[:64] or None,
            "reject_msg": (reject_msg or "")[:255] or None,
            "eval_price": int(current_price or 0),
        }
        self._enqueue(item)

    def _enqueue(self, item: Dict[str, Any]) -> None:
        try:
            self._write_queue.put_nowait(item)
        except queue.Full:
            self._drops += 1
            if self._drops == 1 or self._drops % 2000 == 0:
                logger.warning(
                    "⚠️ TriggerEvalRecorder Queue 가득참 — DROP 누적=%d",
                    self._drops,
                )

    @staticmethod
    def _normalize_snap_time(snap_time: Optional[str], ts: float) -> str:
        raw = (snap_time or "").strip().replace(":", "").replace("-", "").replace(" ", "")
        if len(raw) >= 14:
            return raw[:14]
        if len(raw) == 12 and raw.isdigit():
            return raw + "00"
        try:
            return dt.datetime.fromtimestamp(float(ts)).strftime("%Y%m%d%H%M%S")
        except (TypeError, ValueError, OSError):
            return dt.datetime.now().strftime("%Y%m%d%H%M%S")

    def _ensure_tables(self) -> None:
        try:
            if hasattr(self.db, "ensure_ws_orderbook_table"):
                self.db.ensure_ws_orderbook_table()
            if hasattr(self.db, "ensure_ws_program_table"):
                self.db.ensure_ws_program_table()
            if hasattr(self.db, "migrate_trigger_eval_columns"):
                self.db.migrate_trigger_eval_columns()
        except Exception as e:
            logger.warning("trigger eval 테이블 확인 실패: %s", e)

    def _start_writer(self) -> None:
        self._writer_thread = threading.Thread(
            target=self._writer_loop,
            name="TriggerEvalDBWriter",
            daemon=True,
        )
        self._writer_thread.start()

    def _writer_loop(self) -> None:
        batch_ob: List[Dict[str, Any]] = []
        batch_pg: List[Dict[str, Any]] = []
        last_flush = time.time()
        while True:
            try:
                timeout = max(0.1, self._flush_sec - (time.time() - last_flush))
                item = self._write_queue.get(timeout=timeout)
                if item is None:
                    break
                if item.get("kind") == "program":
                    batch_pg.append(item)
                else:
                    batch_ob.append(item)
                self._write_queue.task_done()
            except queue.Empty:
                pass

            now = time.time()
            should = (
                len(batch_ob) + len(batch_pg) >= self._batch_size
                or ((batch_ob or batch_pg) and (now - last_flush) >= self._flush_sec)
            )
            if should:
                self._flush_batches(batch_ob, batch_pg)
                batch_ob, batch_pg = [], []
                last_flush = now
            if now - self._last_cleanup_ts >= 3600.0:
                self._maybe_cleanup_old()
                self._last_cleanup_ts = now

        if batch_ob or batch_pg:
            self._flush_batches(batch_ob, batch_pg)

    def _flush_batches(
        self,
        batch_ob: List[Dict[str, Any]],
        batch_pg: List[Dict[str, Any]],
    ) -> None:
        if not self.db:
            return
        try:
            if batch_ob and hasattr(self.db, "insert_ws_orderbook_eval_batch"):
                self.db.insert_ws_orderbook_eval_batch(batch_ob)
            elif batch_ob and hasattr(self.db, "insert_ws_orderbook_batch"):
                self.db.insert_ws_orderbook_batch(batch_ob)
            if batch_pg and hasattr(self.db, "insert_ws_program_eval_batch"):
                self.db.insert_ws_program_eval_batch(batch_pg)
            elif batch_pg and hasattr(self.db, "insert_ws_program_batch"):
                self.db.insert_ws_program_batch(batch_pg)
            if batch_ob or batch_pg:
                logger.debug(
                    "💾 [TRIGGER판정] ob=%d pg=%d → DB",
                    len(batch_ob), len(batch_pg),
                )
        except Exception as e:
            logger.warning(
                "trigger eval 배치 INSERT 실패 (ob=%d pg=%d): %s",
                len(batch_ob), len(batch_pg), e,
            )

    def _maybe_cleanup_old(self) -> None:
        try:
            if self._ob_keep_days > 0 and hasattr(self.db, "cleanup_old_ws_orderbook_eval"):
                self.db.cleanup_old_ws_orderbook_eval(keep_days=self._ob_keep_days)
            elif self._ob_keep_days > 0 and hasattr(self.db, "cleanup_old_ws_orderbook"):
                self.db.cleanup_old_ws_orderbook(keep_days=self._ob_keep_days)
            if self._pg_keep_days > 0 and hasattr(self.db, "cleanup_old_ws_program_eval"):
                self.db.cleanup_old_ws_program_eval(keep_days=self._pg_keep_days)
            elif self._pg_keep_days > 0 and hasattr(self.db, "cleanup_old_ws_program"):
                self.db.cleanup_old_ws_program(keep_days=self._pg_keep_days)
        except Exception as e:
            logger.debug("trigger eval 정리 실패: %s", e)
