"""
kis_trader/backtest/breakout_tick_loader.py — ws_ticks 로드·분봉 인덱싱 (B안 백테)
"""
from __future__ import annotations

from collections import defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple

from ..utils.env import get_env_bool, get_env_from_db
from ..utils.logger import get_logger

logger = get_logger("kis_trader.breakout_tick_loader")


def _candle_keys_to_tick_range(start_key: str, end_key: str) -> Tuple[str, str]:
    """ws_candles 키(12자리) → ws_ticks tick_time(14자리) 범위."""
    s = (start_key or "")[:12]
    e = (end_key or "")[:12]
    return s + "00", e + "59"


def _prefer_kiwoom_minute_ticks(
    ticks: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """
    실매 후보 경로는 키움 체결량(FID15) 기준.
    같은 분에 kis(과거 BIDP1 오염 volume)가 섞이면 거래량 필터가 가짜 통과하므로
    키움 틱이 1건이라도 있으면 키움만 사용. 키움 없으면 kis/기타 유지.
    """
    if not ticks:
        return ticks
    kw = [
        t for t in ticks
        if str(t.get("source") or "").strip().lower() == "kiwoom"
    ]
    return kw if kw else ticks


def load_breakout_ticks_by_code(
    db,
    start_key: str,
    end_key: str,
    codes: Optional[Set[str]] = None,
    *,
    market: Optional[str] = None,
) -> Tuple[Dict[str, Dict[str, List[Dict[str, Any]]]], int]:
    """
    기간 내 ``ws_ticks`` 를 종목·분봉(YYYYMMDDHHMM) 단위로 로드.

    Returns:
        (``{code: {minute_key: [tick, ...]}}``, total_tick_rows)
    """
    mkt = (market or get_env_from_db("WS_TICK_DEFAULT_MARKET", "KR") or "KR").strip().upper()
    tt_start, tt_end = _candle_keys_to_tick_range(start_key, end_key)
    prefer_kw = get_env_bool("WS_TICK_PREFER_KIWOOM", True)

    try:
        if hasattr(db, "ensure_ws_ticks_table"):
            db.ensure_ws_ticks_table()
    except Exception:
        pass

    code_filter = ""
    params: List[Any] = [mkt, tt_start, tt_end]
    if codes:
        placeholders = ",".join(["%s"] * len(codes))
        code_filter = f" AND code IN ({placeholders})"
        params.extend(sorted(codes))

    sql = f"""
        SELECT code, tick_time, price, volume, source
        FROM ws_ticks
        WHERE market = %s
          AND tick_time >= %s
          AND tick_time <= %s
          {code_filter}
        ORDER BY code, tick_time
    """
    try:
        rows = db.conn.execute(sql, tuple(params)).fetchall()
    except Exception as e:
        logger.warning("ws_ticks 조회 실패 — OHLC 폴백만 사용: %s", e)
        return {}, 0

    out: Dict[str, Dict[str, List[Dict[str, Any]]]] = defaultdict(dict)
    total = 0
    for r in rows:
        code = str(r["code"]).strip()
        tt = str(r["tick_time"])[:14]
        if len(tt) < 12:
            continue
        minute_key = tt[:12]
        tick = {
            "code": code,
            "tick_time": tt,
            "price": float(r["price"] or 0),
            "volume": int(r.get("volume") or 0),
            "source": r.get("source") or "",
        }
        bucket = out[code].setdefault(minute_key, [])
        bucket.append(tick)
        total += 1

    # 실매(키움 구독) 정합: 분봉 단위 키움 우선 (모멘텀/꼬리 로더가 이 함수 재사용)
    if prefer_kw and out:
        kept = 0
        for code, minutes in out.items():
            for mk, ticks in list(minutes.items()):
                filtered = _prefer_kiwoom_minute_ticks(ticks)
                minutes[mk] = filtered
                kept += len(filtered)
        if kept != total:
            logger.info(
                "📌 WS_TICK_PREFER_KIWOOM: raw=%d → kept=%d (분봉내 키움 우선)",
                total, kept,
            )
        total = kept

    return dict(out), total


def _infer_bar_tf_min(candles: List[Dict], default: int = 1) -> int:
    """연속 candle_time 간격의 중앙값으로 봉주기(분) 추정. 실패 시 default."""
    from kis_trader.engine.candle_rollup import minute_diff

    times = sorted({
        str(c.get("candle_time") or "")[:12]
        for c in (candles or [])
        if len(str(c.get("candle_time") or "")[:12]) >= 12
    })
    if len(times) < 2:
        return max(1, int(default))
    diffs = []
    for i in range(1, min(len(times), 80)):
        d = minute_diff(times[i - 1], times[i])
        if d is not None and d > 0:
            diffs.append(int(d))
    if not diffs:
        return max(1, int(default))
    diffs.sort()
    return max(1, int(diffs[len(diffs) // 2]))


def _bar_has_ticks(
    candle_time: str,
    minute_map: Dict[str, List],
    tf_min: int,
) -> bool:
    """
    N분봉이면 시작~시작+tf 구간의 **어느 1분이라도** 틱이 있으면 커버.
    (옛 로직은 시작분만 봐 → 꼬리 3M에서 커버가 과소 집계됨)
    """
    from kis_trader.engine.candle_rollup import add_candle_minutes

    ct = str(candle_time or "")[:12]
    if len(ct) < 12 or not minute_map:
        return False
    tf = max(1, int(tf_min))
    for i in range(tf):
        mk = ct if i == 0 else add_candle_minutes(ct, i)
        if mk and minute_map.get(mk):
            return True
    return False


def tick_coverage_stats(
    codes_candles: Dict[str, List[Dict]],
    ticks_by_code: Dict[str, Dict[str, List[Dict]]],
    *,
    bar_tf_min: Optional[int] = None,
) -> Dict[str, Any]:
    """
    분봉 대비 틱 보유 비율 (백테 메타용).

    왜 '전체 %'가 낮아 보이는가
    ─────────────────────────
    분봉(ws_candles)은 REST 갭보정으로 **구독 전·틱 없는 구간**까지 채워지고,
    틱(ws_ticks)은 **구독 중·체결 있는 분**만 쌓인다. 분모에 REST 봉을 넣으면
    커버가 낮게 나온다 (기록 누락이 아님).

    반환 키
    ───────
    - tick_bar_coverage_pct: **구독구간**(종목별 첫틱~끝틱) 커버 — UI 주표시
    - tick_bar_coverage_pct_all: 전체 봉(REST 웜업 포함)
    - tick_bar_coverage_pct_friend: 틱이 1건이라도 있는 종목만
    """
    total_bars = 0
    covered_all = 0
    covered_exact_legacy = 0
    friend_bars = 0
    friend_covered = 0
    sub_bars = 0
    sub_covered = 0
    codes_with_any = 0

    for code, candles in (codes_candles or {}).items():
        minute_map = ticks_by_code.get(code) or {}
        has_ticks = bool(minute_map)
        if has_ticks:
            codes_with_any += 1
        tf = int(bar_tf_min) if bar_tf_min and int(bar_tf_min) > 0 else _infer_bar_tf_min(
            candles, default=1,
        )
        tick_keys = [str(k)[:12] for k in minute_map.keys() if k]
        first_m = min(tick_keys) if tick_keys else ""
        last_m = max(tick_keys) if tick_keys else ""

        for c in candles or []:
            ct = str(c.get("candle_time") or "")[:12]
            if len(ct) < 12:
                continue
            total_bars += 1
            hit = _bar_has_ticks(ct, minute_map, tf)
            if minute_map.get(ct):
                covered_exact_legacy += 1
            if hit:
                covered_all += 1
            if has_ticks:
                friend_bars += 1
                if hit:
                    friend_covered += 1
                # 구독구간: 첫 틱 분 ~ 마지막 틱 분 (봉 시작 시각 기준)
                if first_m and last_m and first_m <= ct <= last_m:
                    sub_bars += 1
                    if hit:
                        sub_covered += 1

    pct_all = (covered_all / total_bars * 100.0) if total_bars else 0.0
    pct_friend = (friend_covered / friend_bars * 100.0) if friend_bars else 0.0
    pct_sub = (sub_covered / sub_bars * 100.0) if sub_bars else 0.0
    pct_legacy = (covered_exact_legacy / total_bars * 100.0) if total_bars else 0.0

    return {
        # UI·로그 주표시 = 구독구간 (실매 '보면서 기록'에 가장 가깝다)
        "tick_bar_coverage_pct": round(pct_sub, 2),
        "tick_bar_coverage_pct_subscribed": round(pct_sub, 2),
        "tick_bars_subscribed_total": sub_bars,
        "tick_bars_subscribed_covered": sub_covered,
        "tick_bar_coverage_pct_all": round(pct_all, 2),
        "tick_bar_coverage_pct_friend": round(pct_friend, 2),
        "tick_bar_coverage_pct_legacy_exact": round(pct_legacy, 2),
        "tick_bars_total": total_bars,
        "tick_bars_covered": covered_all,
        "tick_bars_friend_total": friend_bars,
        "tick_bars_friend_covered": friend_covered,
        "tick_codes_with_data": codes_with_any,
        "tick_codes_total": len(codes_candles or {}),
    }


def enrich_tick_meta_with_traded_codes(
    tick_meta: Optional[Dict[str, Any]],
    candles_by_code: Dict[str, List[Dict]],
    ticks_by_code: Optional[Dict[str, Dict[str, List[Dict]]]],
    trades: Optional[List[Dict]],
    *,
    bar_tf_min: Optional[int] = None,
) -> Dict[str, Any]:
    """
    백테 **체결(거래) 종목만** 구독구간 커버를 tick_meta 에 보강.
    UI: ``거래종목 NN%`` — 산 친구 칸만 센 값.
    """
    out: Dict[str, Any] = dict(tick_meta or {})
    codes = sorted({
        str(t.get("code") or "").strip()
        for t in (trades or [])
        if str(t.get("code") or "").strip()
    })
    out["tick_codes_traded"] = len(codes)
    if not codes:
        return out
    sub_candles = {
        c: candles_by_code[c]
        for c in codes
        if c in (candles_by_code or {}) and candles_by_code.get(c)
    }
    if not sub_candles:
        return out
    ticks = ticks_by_code or {}
    sub_ticks = {c: ticks.get(c) or {} for c in sub_candles}
    sub = tick_coverage_stats(sub_candles, sub_ticks, bar_tf_min=bar_tf_min)
    out["tick_bar_coverage_pct_traded"] = sub.get("tick_bar_coverage_pct")
    out["tick_bars_traded_subscribed_total"] = sub.get("tick_bars_subscribed_total")
    out["tick_bars_traded_subscribed_covered"] = sub.get("tick_bars_subscribed_covered")
    return out


def build_tick_coverage_meta_for_day(
    db,
    codes: Set[str],
    start_ymd: str,
    end_ymd: str,
    *,
    timeframe: int = 1,
    trades: Optional[List[Dict]] = None,
) -> Dict[str, Any]:
    """
    실매·운영 탭용: 당일(기간) ws_candles + ws_ticks 로 분봉커버 메타 생성.
    UI 통일 — 백테 ``tick_backtest`` 와 동일 키.
    """
    codes_clean = sorted({str(c).strip() for c in (codes or set()) if str(c).strip()})
    if not codes_clean or not db:
        return {}
    start_key = str(start_ymd or "").replace("-", "")[:8] + "0900"
    end_key = str(end_ymd or start_ymd or "").replace("-", "")[:8] + "1530"
    if len(start_key) < 12 or len(end_key) < 12:
        return {}
    day_like = start_key[:8] + "%"
    tf = max(1, int(timeframe or 1))
    placeholders = ",".join(["%s"] * len(codes_clean))
    try:
        rows = db.conn.execute(
            f"SELECT code, candle_time, open, high, low, close, volume "
            f"FROM ws_candles WHERE timeframe=%s AND candle_time LIKE %s "
            f"AND code IN ({placeholders})",
            tuple([tf, day_like] + codes_clean),
        ).fetchall()
    except Exception as e:
        logger.warning("build_tick_coverage_meta_for_day candles: %s", e)
        return {}
    candles_by_code: Dict[str, List[Dict]] = defaultdict(list)
    for r in rows or []:
        code = str(r.get("code") or "").strip()
        ct = str(r.get("candle_time") or "")[:12]
        if not code or len(ct) < 12:
            continue
        candles_by_code[code].append({
            "candle_time": ct,
            "open": r.get("open"),
            "high": r.get("high"),
            "low": r.get("low"),
            "close": r.get("close"),
            "volume": r.get("volume"),
        })
    if not candles_by_code:
        return {"tick_codes_traded": len(codes_clean), "tick_bars_total": 0}
    ticks_by_code, tick_rows = load_breakout_ticks_by_code(
        db, start_key, end_key, set(candles_by_code.keys()),
    )
    meta = tick_coverage_stats(dict(candles_by_code), ticks_by_code, bar_tf_min=tf)
    meta["ws_tick_rows_loaded"] = tick_rows
    trade_list = trades if trades is not None else [{"code": c} for c in codes_clean]
    meta = enrich_tick_meta_with_traded_codes(
        meta, dict(candles_by_code), ticks_by_code, trade_list, bar_tf_min=tf,
    )
    return meta


def first_tick_minute_key(
    ticks_by_code: Optional[Dict[str, Dict[str, List[Dict]]]],
    code: str,
) -> Optional[str]:
    """종목 ws_ticks 중 가장 이른 분봉 키(YYYYMMDDHHMM). 없으면 None."""
    if not ticks_by_code or not code:
        return None
    minute_map = ticks_by_code.get(code) or {}
    if not minute_map:
        return None
    keys = [str(k)[:12] for k in minute_map.keys() if k]
    return min(keys) if keys else None


def entry_before_first_tick(
    ticks_by_code: Optional[Dict[str, Dict[str, List[Dict]]]],
    code: str,
    entry_bar_time: str,
) -> bool:
    """정합용: 진입봉이 첫 틱 분 이전이면 True (매수 스킵 대상)."""
    first = first_tick_minute_key(ticks_by_code, code)
    if not first:
        return False
    et = str(entry_bar_time or "")[:12]
    if not et:
        return False
    return et < first

