#!/usr/bin/env python3
"""
유니버스 타임라인 (전략 공통) — 스캔 시각(초)별 ``get_universe_at`` 재현 + EXIT 디바운스.

실매: 조건검색 변동 시 초단위 스냅샷(``target_candidates_history``) → 루프에서 그 시각
최신 유니버스. 백테(구): 1분 슬롯 strict 집계 → 깜빡임 EXIT·분봉 끝 집계로 실매와 어긋남.

이 모듈은 전략 무관(strategy_id 파라미터)으로, 모멘텀·돌파 등 모든 전략이 **하나의
``UniverseTimeline``** 로 실매(``get_universe_at``)와 초단위 정합을 맞추도록 통일한다.
(기존 ``MomentumUniverseTimeline`` 은 이 클래스의 alias 로 유지 — 모멘텀 코드 무변경)
"""
from __future__ import annotations

from bisect import bisect_right
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple


def _event_time_to_key(event_time: str) -> str:
    et = str(event_time or "")
    if len(et) < 19:
        return ""
    return (
        et[0:4] + et[5:7] + et[8:10]
        + et[11:13] + et[14:16] + et[17:19]
    )


def _parse_scan_key(scan_key: str) -> datetime:
    return datetime.strptime(str(scan_key)[:14], "%Y%m%d%H%M%S")


def _add_minutes_to_scan_key(scan_key: str, minutes: int) -> str:
    dt = _parse_scan_key(scan_key)
    return (dt + timedelta(minutes=int(minutes))).strftime("%Y%m%d%H%M%S")


def _dedupe_codes_preserve_order(codes: Sequence[str]) -> List[str]:
    """스냅샷 내 종목 순서 유지 + 중복 제거 (HTS/DB insert 순)."""
    seen: Set[str] = set()
    out: List[str] = []
    for c in codes:
        cs = str(c).strip()
        if cs and cs not in seen:
            seen.add(cs)
            out.append(cs)
    return out


def normalize_universe_events(events: Sequence[Dict[str, Any]]) -> List[Tuple[str, List[str]]]:
    """iter_universe_events 결과 → (YYYYMMDDHHMMSS, codes) 시간순 리스트."""
    out: List[Tuple[str, List[str]]] = []
    for ev in events:
        et_key = _event_time_to_key(str(ev.get("event_time") or ""))
        if not et_key:
            continue
        codes = _dedupe_codes_preserve_order(
            [str(it["code"]) for it in ev.get("items", []) if it.get("code")]
        )
        out.append((et_key, codes))
    out.sort(key=lambda x: x[0])
    return out


def debounce_universe_snapshots(
    normalized: Sequence[Tuple[str, List[str]]],
    debounce_sec: int,
) -> List[Tuple[str, List[str]]]:
    """
    짧은 EXIT 후 재편입은 EXIT 로 치지 않음 (HTS 조건검색 노이즈 완화).
    각 스냅샷 시각에 **디바운스 적용 후** 유효 종목 리스트를 기록 (가나다 재정렬 없음).
    """
    if debounce_sec <= 0 or not normalized:
        return list(normalized)

    effective_order: List[str] = []
    effective_set: Set[str] = set()
    pending_exit: Dict[str, Tuple[str, datetime]] = {}
    out: List[Tuple[str, List[str]]] = []

    def _eff_remove(code: str) -> None:
        if code not in effective_set:
            return
        effective_set.discard(code)
        try:
            effective_order.remove(code)
        except ValueError:
            pass

    def _eff_add(code: str) -> None:
        if code in effective_set:
            return
        effective_set.add(code)
        effective_order.append(code)

    for et_key, codes in normalized:
        cur_list = _dedupe_codes_preserve_order([str(c) for c in codes if c])
        cur_set = set(cur_list)
        et_dt = _parse_scan_key(et_key)

        for code, (ex_key, ex_dt) in list(pending_exit.items()):
            if (et_dt - ex_dt).total_seconds() >= debounce_sec:
                if code not in cur_set:
                    _eff_remove(code)
                pending_exit.pop(code, None)

        for code in list(effective_order):
            if code not in cur_set and code not in pending_exit:
                pending_exit[code] = (et_key, et_dt)

        for code in cur_list:
            pending_exit.pop(code, None)
            _eff_add(code)

        out.append((et_key, list(effective_order)))

    return out


class UniverseTimeline:
    """스캔 시각별 유니버스 — 이진 탐색으로 O(log N) 조회 (전략 공통)."""

    __slots__ = ("_keys", "_codes", "_strict_avail")

    def __init__(
        self,
        snapshots: Sequence[Tuple[str, List[str]]],
        *,
        strict: bool = False,
        strict_lag_minutes: int = 0,
    ) -> None:
        keys: List[str] = []
        codes_list: List[List[str]] = []
        strict_avail: Dict[str, str] = {}
        prev_set: Set[str] = set()

        for et_key, codes in snapshots:
            keys.append(et_key)
            ordered = _dedupe_codes_preserve_order([str(c) for c in codes if c])
            cur_set = set(ordered)
            if strict:
                lag = max(0, int(strict_lag_minutes))
                for c in cur_set - prev_set:
                    strict_avail[c] = (
                        _add_minutes_to_scan_key(et_key, lag) if lag > 0 else et_key
                    )
                for c in prev_set - cur_set:
                    strict_avail.pop(c, None)
                prev_set = cur_set
            codes_list.append(ordered)

        self._keys = keys
        self._codes = codes_list
        self._strict_avail = strict_avail

    def codes_at(self, scan_key: str) -> List[str]:
        """scan_key(YYYYMMDDHHMMSS) 시점 유니버스 — 실매 get_universe_at 동일."""
        sk = str(scan_key or "")[:14]
        if not sk or not self._keys:
            return []
        idx = bisect_right(self._keys, sk) - 1
        if idx < 0:
            return []
        raw = list(self._codes[idx])
        if not self._strict_avail:
            return raw
        return [c for c in raw if sk >= self._strict_avail.get(c, "00000000000000")]

    @property
    def snapshot_count(self) -> int:
        return len(self._keys)


def build_universe_timeline(
    *,
    strategy_id: str,
    start_ymd: str,
    end_ymd: str,
    debounce_sec: int = 30,
    strict: bool = False,
    strict_lag_minutes: int = 1,
) -> Optional[UniverseTimeline]:
    """``target_candidates_history`` (strategy_id) → 초단위 유니버스 타임라인."""
    try:
        from kis_trader.database.db_manager import get_db
        ext = get_db()
        start_time = f"{start_ymd[:4]}-{start_ymd[4:6]}-{start_ymd[6:8]} 00:00:00"
        end_time = f"{end_ymd[:4]}-{end_ymd[4:6]}-{end_ymd[6:8]} 23:59:59"
        events = ext.iter_universe_events(
            strategy_id=strategy_id,
            start_time=start_time,
            end_time=end_time,
            preserve_insert_order=True,
        )
        normalized = normalize_universe_events(events)
        if not normalized:
            return None
        debounced = debounce_universe_snapshots(normalized, debounce_sec)
        return UniverseTimeline(
            debounced,
            strict=strict,
            strict_lag_minutes=strict_lag_minutes,
        )
    except Exception:
        return None
