"""
kis_trader/ws/trigger_snapshot_recorder.py — 호가(0D)·프로그램(0w) 스냅샷 DB 저장
================================================================================
TickRecorder 와 동일 패턴: 종목별 스로틀 → 큐 → 배치 INSERT (ws_orderbook / ws_program).
백테·파람서치 TRIGGER 필터 재현용.
"""
from __future__ import annotations

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

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_snapshot_recorder")


class TriggerSnapshotRecorder:
    """키움 0D/0w 스냅샷 기록기 — 후보 종목만 필터·종목당 최소 간격 스로틀."""

    def __init__(self, db=None) -> None:
        self.db = db
        self._ob_enabled = get_env_bool("WS_ORDERBOOK_SAVE_ENABLED", False)
        self._pg_enabled = get_env_bool("WS_PROGRAM_SAVE_ENABLED", False)
        self._interval_sec = max(
            0.5,
            get_env_float("WS_TRIGGER_SNAPSHOT_DB_INTERVAL_SEC", 3.0),
        )
        self._batch_size = get_env_int("WS_TRIGGER_SNAPSHOT_DB_BATCH_SIZE", 100)
        self._flush_sec = get_env_float("WS_TRIGGER_SNAPSHOT_DB_FLUSH_SEC", 3.0)
        self._queue_max = get_env_int("WS_TRIGGER_SNAPSHOT_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._lock = threading.Lock()
        self._record_codes: Optional[Set[str]] = None
        self._last_ob_save: Dict[str, float] = {}
        self._last_pg_save: Dict[str, float] = {}
        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._ob_enabled or self._pg_enabled) and self.db is not None:
            self._ensure_tables()
            self._start_writer()
            logger.info(
                "✅ TriggerSnapshotRecorder 활성 (ob=%s pg=%s interval=%.1fs keep=%d/%dd)",
                self._ob_enabled, self._pg_enabled, self._interval_sec,
                self._ob_keep_days, self._pg_keep_days,
            )
        elif self._ob_enabled or self._pg_enabled:
            logger.warning("⚠️ TriggerSnapshotRecorder: db=None → DB 저장 비활성")
        else:
            logger.info("ℹ️ TriggerSnapshotRecorder 비활성")

    def set_record_codes(self, codes: Optional[Set[str]]) -> None:
        with self._lock:
            if codes is None:
                self._record_codes = None
            else:
                self._record_codes = {str(c).strip() for c in codes if c}

    def on_orderbook(self, snap: OrderbookSnapshot, snap_time: Optional[str] = None) -> None:
        if not self._ob_enabled or self.db is None or snap is None:
            return
        code = (snap.code or "").strip()
        if not code:
            return
        if not self._should_record(code):
            return
        now = time.time()
        with self._lock:
            last = self._last_ob_save.get(code, 0.0)
            if now - last < self._interval_sec:
                return
            self._last_ob_save[code] = now
        item = self._build_orderbook_item(snap, snap_time)
        self._enqueue(item)

    def on_program(self, snap: ProgramSnapshot, snap_time: Optional[str] = None) -> None:
        if not self._pg_enabled or self.db is None or snap is None:
            return
        code = (snap.code or "").strip()
        if not code:
            return
        if not self._should_record(code):
            return
        now = time.time()
        with self._lock:
            last = self._last_pg_save.get(code, 0.0)
            if now - last < self._interval_sec:
                return
            self._last_pg_save[code] = now
        item = self._build_program_item(snap, snap_time)
        self._enqueue(item)

    def remove_code(self, code: str) -> None:
        code = (code or "").strip()
        if not code:
            return
        with self._lock:
            self._last_ob_save.pop(code, None)
            self._last_pg_save.pop(code, None)

    def _should_record(self, code: str) -> bool:
        with self._lock:
            filt = self._record_codes
        if filt is None:
            return True
        return code in filt

    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(
                    "⚠️ TriggerSnapshotRecorder Queue 가득참 — DROP 누적=%d",
                    self._drops,
                )

    def _build_orderbook_item(
        self, snap: OrderbookSnapshot, snap_time: Optional[str],
    ) -> Dict[str, Any]:
        st = self._normalize_snap_time(snap_time, snap.ts)
        body = snap.to_storage_dict()
        return {
            "kind": "orderbook",
            "market": self._default_market[:8],
            "code": code[:32] if (code := str(snap.code or "").strip()) else "",
            "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": str(body.get("source") or "kiwoom_0d")[:16],
            "recv_ts": dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        }

    def _build_program_item(
        self, snap: ProgramSnapshot, snap_time: Optional[str],
    ) -> Dict[str, Any]:
        st = self._normalize_snap_time(snap_time, snap.ts)
        body = snap.to_storage_dict()
        return {
            "kind": "program",
            "market": self._default_market[:8],
            "code": str(snap.code or "").strip()[: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": str(body.get("source") or "kiwoom_0w")[:16],
            "recv_ts": dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        }

    @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"
        if len(raw) == 6 and raw.isdigit():
            return dt.datetime.now().strftime("%Y%m%d") + raw
        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()
        except Exception as e:
            logger.warning("trigger snapshot 테이블 확인 실패: %s", e)

    def _start_writer(self) -> None:
        self._writer_thread = threading.Thread(
            target=self._writer_loop,
            name="TriggerSnapshotDBWriter",
            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_batch"):
                self.db.insert_ws_orderbook_batch(batch_ob)
            if 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 snapshot 배치 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"):
                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"):
                self.db.cleanup_old_ws_program(keep_days=self._pg_keep_days)
        except Exception as e:
            logger.debug("trigger snapshot 정리 실패: %s", e)
