"""
kis_trader/ws/tick_recorder.py — 실시간 체결 틱 RAM 링버퍼 + ws_ticks 배치 저장 (C안)
==================================================================================
[트랙 1] 종목별 deque 링버퍼 — B안 봉 내 돌파·get_recent_ticks() 용 (DB 대기 없음)
[트랙 2] 제한 큐 → 백그라운드 스레드 배치 INSERT (journalctl 파싱 아님)

해외 확장: market / exchange / currency / session 컬럼 (국내 기본 KR/KRX/KRW).
"""
from __future__ import annotations

import datetime as dt
import queue
import threading
import time
from collections import deque
from typing import Any, Deque, 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

logger = get_logger("kis_trader.tick_recorder")


class TickRecorder:
    """
    WebSocket 체결 틱 기록기.

    - ``set_record_codes()`` 로 후보(또는 구독) 종목만 필터.
    - RAM: 종목당 최대 N틱 (링버퍼).
    - DB: BATCH_SIZE / FLUSH_SEC 마다 ws_ticks append INSERT.
    """

    def __init__(self, db=None) -> None:
        self.db = db
        self._enabled = get_env_bool("WS_TICK_SAVE_ENABLED", True)
        self._buffer_max = get_env_int("WS_TICK_BUFFER_MAX_PER_CODE", 500)
        self._batch_size = get_env_int("WS_TICK_DB_BATCH_SIZE", 200)
        self._flush_sec = get_env_float("WS_TICK_DB_FLUSH_SEC", 3.0)
        self._queue_max = get_env_int("WS_TICK_WRITE_QUEUE_MAX", 50000)
        self._keep_days = get_env_int("WS_TICK_KEEP_DAYS", 7)
        self._default_market = (get_env_from_db("WS_TICK_DEFAULT_MARKET", "KR") or "KR").strip().upper()
        self._default_exchange = (get_env_from_db("WS_TICK_DEFAULT_EXCHANGE", "KRX") or "KRX").strip()
        self._default_currency = (get_env_from_db("WS_TICK_DEFAULT_CURRENCY", "KRW") or "KRW").strip().upper()

        self._lock = threading.Lock()
        self._buffers: Dict[str, Deque[dict]] = {}
        self._record_codes: Optional[Set[str]] = None
        self._write_queue: queue.Queue = queue.Queue(maxsize=max(1000, 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_table()
            self._start_writer()
            logger.info(
                "✅ TickRecorder 활성 (buf=%d/종목, batch=%d, flush=%.1fs, keep=%dd)",
                self._buffer_max, self._batch_size, self._flush_sec, self._keep_days,
            )
        elif self._enabled and self.db is None:
            logger.warning("⚠️ TickRecorder: db=None → RAM 링버퍼만 (DB 저장 비활성)")
        else:
            logger.info("ℹ️ TickRecorder 비활성 (WS_TICK_SAVE_ENABLED=false)")

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def set_record_codes(self, codes: Optional[Set[str]]) -> None:
        """None=필터 없음(구독 전체), 빈 set=저장 안 함."""
        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_tick(
        self,
        code: str,
        price: float,
        volume: int,
        tick_time: str,
        *,
        market: Optional[str] = None,
        exchange: Optional[str] = None,
        currency: Optional[str] = None,
        source: str = "kis",
        session: Optional[str] = None,
        tick_seq: Optional[int] = None,
    ) -> None:
        """체결 틱 1건 — WS 핫패스에서 호출 (논블로킹)."""
        if not self._enabled or price <= 0:
            return
        code = (code or "").strip()
        if not code:
            return
        with self._lock:
            filt = self._record_codes
        if filt is not None and code not in filt:
            return

        tick_time_full = self._normalize_tick_time(tick_time)
        recv_ts = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        item = {
            "market": (market or self._default_market).upper()[:8],
            "exchange": (exchange or self._default_exchange)[:16] if (exchange or self._default_exchange) else None,
            "code": code[:32],
            "tick_time": tick_time_full,
            "price": float(price),
            "volume": int(volume or 0),
            "tick_seq": tick_seq,
            "session": (session or "")[:8] or None,
            "currency": (currency or self._default_currency).upper()[:8],
            "source": (source or "kis")[:16],
            "recv_ts": recv_ts,
        }

        with self._lock:
            buf = self._buffers.get(code)
            if buf is None:
                buf = deque(maxlen=max(10, self._buffer_max))
                self._buffers[code] = buf
            buf.append(item)

        if self.db is None:
            return
        try:
            self._write_queue.put_nowait(item)
        except queue.Full:
            self._drops += 1
            if self._drops == 1 or self._drops % 5000 == 0:
                logger.warning(
                    "⚠️ TickRecorder 쓰기 Queue 가득참 — 틱 DROP 누적=%d (code=%s)",
                    self._drops, code,
                )

    def get_recent_ticks(self, code: str, limit: int = 100) -> List[dict]:
        """RAM 링버퍼 최근 틱 (오래된→최신 순)."""
        code = (code or "").strip()
        if not code:
            return []
        with self._lock:
            buf = self._buffers.get(code)
            if not buf:
                return []
            rows = list(buf)
        n = max(1, int(limit))
        return rows[-n:]

    def get_last_price(self, code: str) -> Optional[float]:
        ticks = self.get_recent_ticks(code, limit=1)
        if not ticks:
            return None
        p = ticks[-1].get("price")
        return float(p) if p and float(p) > 0 else None

    def remove_code(self, code: str) -> None:
        """구독 해제 시 RAM 버퍼 정리."""
        code = (code or "").strip()
        if not code:
            return
        with self._lock:
            self._buffers.pop(code, None)

    # ------------------------------------------------------------------
    # 내부
    # ------------------------------------------------------------------

    @staticmethod
    def _normalize_tick_time(tick_time: str) -> str:
        """HHMMSS 또는 YYYYMMDDHHMMSS → 14자리 시장 시각."""
        raw = (tick_time or "").strip().replace(":", "").replace("-", "").replace(" ", "")
        if len(raw) >= 14:
            return raw[:14]
        if len(raw) == 6 and raw.isdigit():
            today = dt.datetime.now().strftime("%Y%m%d")
            return today + raw
        return dt.datetime.now().strftime("%Y%m%d%H%M%S")

    def _ensure_table(self) -> None:
        try:
            if hasattr(self.db, "ensure_ws_ticks_table"):
                self.db.ensure_ws_ticks_table()
        except Exception as e:
            logger.warning("ws_ticks 테이블 확인 실패: %s", e)

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

    def _writer_loop(self) -> None:
        batch: List[dict] = []
        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
                batch.append(item)
                self._write_queue.task_done()
            except queue.Empty:
                pass

            now = time.time()
            should = len(batch) >= self._batch_size or (
                batch and (now - last_flush) >= self._flush_sec
            )
            if should and batch:
                self._flush_batch(batch)
                batch = []
                last_flush = now
            if now - self._last_cleanup_ts >= 3600.0:
                self._maybe_cleanup_old()
                self._last_cleanup_ts = now

        if batch:
            self._flush_batch(batch)

    def _flush_batch(self, batch: List[dict]) -> None:
        if not batch or not self.db:
            return
        try:
            if hasattr(self.db, "insert_ws_ticks_batch"):
                self.db.insert_ws_ticks_batch(batch)
            logger.debug("💾 [틱배치] %d건 → ws_ticks", len(batch))
        except Exception as e:
            logger.warning("ws_ticks 배치 INSERT 실패 (%d건): %s", len(batch), e)

    def _maybe_cleanup_old(self) -> None:
        if self._keep_days <= 0:
            return
        try:
            if hasattr(self.db, "cleanup_old_ws_ticks"):
                self.db.cleanup_old_ws_ticks(keep_days=self._keep_days)
        except Exception as e:
            logger.debug("ws_ticks 정리 실패: %s", e)

