"""
kis_trader/strategies/updown_scan.py — UPDOWN 박스권 SCAN 배선
================================================================================
[목적 — SCAN vs TRIGGER 분리 원칙]
조건검색(HTS) 결과를 받아 **박스권 필터**만 통과시켜 ``updown_watchlist`` 에
충전(membership 등록)한다. 무거운 매수 직전 검사(호가·실시간가)는 여기서 하지 않고,
"이 종목이 며칠짜리 박스권인가?"라는 **느리게 변하는 멤버십**만 판정한다.

[흐름]
  조건검색 후보(code,name)
    → 종목별 15분봉(기본) 조회
    → ``evaluate_box`` (좁은범위·밴드스퀴즈·추세없음)
    → is_box 통과분만 box_low/high/score 와 함께 watchlist upsert(soft)
    → 만료/정리(expire_stale)

[설계 메모]
- 박스 판별은 15분봉 lookback 기준 → 5분 스캔마다 종목당 분봉 1회 조회.
  조건검색 후보가 많아도 watchlist cap(30) 안에서만 유지되므로 호출량 제한적.
- ``get_candles_fn(code, tf, n)`` 를 주입받아 데이터 소스(WS/REST)와 분리(테스트 용이).
- 모든 임계값은 ``updown_box.get_box_cfg_from_env`` 단일 소스(하드코딩 0).
"""
from __future__ import annotations

import logging
import random
import time
from typing import Any, Callable, Dict, List, Optional

from ..engine.updown_box import evaluate_box, get_box_cfg_from_env
from ..utils.env import get_env_int
from .updown_watchlist import (
    ensure_updown_watchlist_table,
    expire_stale,
    upsert_from_scan,
)

logger = logging.getLogger("kis_trader.updown_scan")

# 분봉 조회 콜백 시그니처: (code, tf_min, n) -> List[candle dict]
GetCandlesFn = Callable[[str, int, int], List[Dict[str, Any]]]


def scan_tf_min() -> int:
    """SCAN 박스 판별에 쓸 분봉(분). 기본 15분 (수집·백테와 동일)."""
    return get_env_int("UPDOWN_SCAN_TF_MIN", 15)


def scan_fetch_n() -> int:
    """박스 판별용 분봉 조회 개수. lookback + 여유."""
    cfg = get_box_cfg_from_env()
    base = int(cfg.get("lookback_bars", 20)) + int(cfg.get("bb_period", 20)) + 5
    return get_env_int("UPDOWN_SCAN_FETCH_N", max(base, 45))


def box_filter_candidates(
    candidates: List[Dict[str, Any]],
    get_candles_fn: GetCandlesFn,
    *,
    cfg: Optional[Dict[str, float]] = None,
    tf_min: Optional[int] = None,
    fetch_n: Optional[int] = None,
    sleep_between: bool = True,
) -> List[Dict[str, Any]]:
    """조건검색 후보 → 박스권 통과분만 (box_low/high/score 부착) 반환.

    Args:
      candidates : [{"code":.., "name":..}, ...] (조건검색 결과)
      get_candles_fn : (code, tf_min, n) -> 분봉 리스트(시간 오름차순)
      sleep_between  : REST 부하 방지용 종목 간 random.sleep (실매 루프 기본 True)

    Returns: [{"code","name","box_low","box_high","box_score"}, ...]
    """
    c = cfg or get_box_cfg_from_env()
    tf = int(tf_min if tf_min is not None else scan_tf_min())
    n = int(fetch_n if fetch_n is not None else scan_fetch_n())
    passed: List[Dict[str, Any]] = []

    for cand in candidates or []:
        code = str(cand.get("code") or "").strip()
        if not code:
            continue
        name = (cand.get("name") or code or "").strip() or code
        try:
            raw = get_candles_fn(code, tf, n) or []
        except Exception as e:
            logger.debug("[UPDOWN SCAN] %s 분봉조회 실패: %s", code, e)
            raw = []
        # 종목 간 소량 슬립 (서버 부하/429 방지) — 실시간 매매 루프 아님
        if sleep_between:
            time.sleep(random.uniform(0.05, 0.15))
        if len(raw) < int(c.get("min_bars", 20)):
            continue
        box = evaluate_box(raw, c)
        if not box.get("is_box"):
            continue
        passed.append({
            "code": code,
            "name": name,
            "box_low": float(box.get("box_low", 0) or 0),
            "box_high": float(box.get("box_high", 0) or 0),
            "box_score": float(box.get("box_score", 0) or 0),
        })
    return passed


def run_updown_scan(
    db,
    condition_candidates: List[Dict[str, Any]],
    get_candles_fn: GetCandlesFn,
    *,
    source: str = "condition",
    cfg: Optional[Dict[str, float]] = None,
    sleep_between: bool = True,
) -> Dict[str, Any]:
    """SCAN 1회 실행: 조건검색 후보 → 박스필터 → watchlist 충전 + 만료정리.

    Returns: {"scanned":N, "box_passed":M, "upserted":K, "expired":E}
    """
    ensure_updown_watchlist_table(db)
    scanned = len(condition_candidates or [])

    box_cands = box_filter_candidates(
        condition_candidates, get_candles_fn,
        cfg=cfg, sleep_between=sleep_between,
    )

    stats: Dict[str, int] = {"added": 0, "reactivated": 0, "updated": 0, "skipped_full": 0}
    if box_cands:
        try:
            stats = upsert_from_scan(db, box_cands, source=source)
        except Exception as e:
            logger.error("[UPDOWN SCAN] watchlist upsert 실패: %s", e)

    expired = 0
    try:
        expired = expire_stale(db)
    except Exception as e:
        logger.debug("[UPDOWN SCAN] expire_stale 실패: %s", e)

    upserted = int(stats.get("added", 0)) + int(stats.get("reactivated", 0))
    logger.info(
        "🔎 [UPDOWN SCAN] 조건후보=%d → 박스통과=%d → 신규+재진입=%d 갱신=%d 만석스킵=%d (만료=%d)",
        scanned, len(box_cands), upserted,
        int(stats.get("updated", 0)), int(stats.get("skipped_full", 0)), expired,
    )
    return {
        "scanned": scanned,
        "box_passed": len(box_cands),
        "added": int(stats.get("added", 0)),
        "reactivated": int(stats.get("reactivated", 0)),
        "updated": int(stats.get("updated", 0)),
        "skipped_full": int(stats.get("skipped_full", 0)),
        "expired": expired,
    }
