#!/usr/bin/env python3
"""
스캘핑·모멘텀 백테스트 공통 로더 — backtest_web / param_search 가
동일한 캔들·유니버스·손익 계산을 쓰도록 단일 진입점.
"""
from __future__ import annotations

from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple

from kis_trader.backtest.backtest_portfolio_common import (
    attach_scalp_trade_pnl,
    backtest_slip_pct,
    build_budget_warning,
    fee_and_slot_from_env_row,
    merge_portfolio_into_params,
    min_invest_ratio_of_slot,
    resolve_portfolio_params,
    summarize_trades,
)
from kis_trader.backtest.breakout_tick_loader import (
    load_breakout_ticks_by_code,
    tick_coverage_stats,
)
from kis_trader.engine import scalping_engine as se
from kis_trader.engine.tick_exit_common import strategy_use_tick_exit
from kis_trader.utils.env import get_env_bool

SCALP_STRATEGY_ID = "SCALP"
MOMENTUM_STRATEGY_ID = "MOMENTUM"


def date_keys(start: str, end: str) -> Tuple[str, str, str, str]:
    """YYYY-MM-DD → candle_time 키 및 ymd."""
    start_key = start.replace("-", "") + "0000"
    end_key = end.replace("-", "") + "2359"
    return start_key, end_key, start_key[:8], end_key[:8]


def resolve_scalp_universe(
    start_ymd: str,
    end_ymd: str,
    *,
    use_saved_history: bool,
    strategy_id: str = SCALP_STRATEGY_ID,
) -> Tuple[Optional[Dict[str, List[str]]], str, int, int]:
    """
    backtest_web 유니버스 해석과 동일.

    Returns:
        (universe_by_slot, source_label, history_slot_count, scan_interval_min)
    """
    if use_saved_history and strategy_id:
        try:
            from kis_trader.database.db_manager import get_db as _get_ext_db

            debounce_sec = scalp_universe_exit_debounce_sec()
            history = _get_ext_db().get_universe_by_candle_time(
                strategy_id=strategy_id,
                start_ymd=start_ymd,
                end_ymd=end_ymd,
                exit_debounce_sec=debounce_sec,
            )
            if history:
                return history, "history", len(history), 1
        except Exception:
            pass
    return None, "all", 0, 1


def scalp_universe_exit_debounce_sec() -> int:
    """실매 ``CONDITION_EXIT_GRACE_SEC`` 정합 — 스냅샷 축소 시 N초 유지."""
    from kis_trader.backtest.universe_timeline import universe_exit_debounce_sec_for_strategy

    return universe_exit_debounce_sec_for_strategy("SCALP")


def scalp_backtest_candle_warmup_bars() -> int:
    """백테 RSI 등 warm-up — 실매 봉 버퍼와 같이 기간 시작 전 N봉 prepend."""
    from kis_trader.utils.env import get_env_int

    return max(0, int(get_env_int("SCALP_BACKTEST_CANDLE_WARMUP_BARS", 50)))


def prepend_scalp_candle_warmup(
    db,
    candles_by_code: Dict[str, List[Dict]],
    period_start_key: str,
    *,
    warmup_bars: Optional[int] = None,
) -> int:
    """
    ``period_start_key``(YYYYMMDDHHMM) 이전 N봉을 종목별로 prepend.
    RSI 판별용 — 포트폴리오 all_times 는 ``_backtest_period_start_key`` 로 기간만 사용.
    """
    wb = (
        scalp_backtest_candle_warmup_bars()
        if warmup_bars is None
        else max(0, int(warmup_bars))
    )
    if wb <= 0 or db is None or not period_start_key:
        return 0
    ps = str(period_start_key)[:12]
    total_prepended = 0
    for code, rows in list(candles_by_code.items()):
        if not rows:
            continue
        first_period_idx = None
        for i, r in enumerate(rows):
            ct = str(r.get("candle_time") or "")
            if ct >= ps:
                first_period_idx = i
                break
        if first_period_idx is None:
            continue
        # 이미 기간 전 봉이 있으면 skip (idempotent)
        if first_period_idx > 0:
            continue
        first_ct = str(rows[first_period_idx].get("candle_time") or "")
        if not first_ct:
            continue
        warm_rows = db.conn.execute(
            "SELECT candle_time, open, high, low, close, volume "
            "FROM ws_candles WHERE timeframe=1 AND code=%s "
            "AND candle_time < %s AND is_confirmed=1 "
            "ORDER BY candle_time DESC LIMIT %s",
            [code, first_ct, wb],
        ).fetchall()
        if not warm_rows:
            continue
        prefix = [dict(r) for r in reversed(warm_rows)]
        candles_by_code[code] = prefix + [dict(r) for r in rows]
        total_prepended += len(prefix)
    return total_prepended


def load_scalp_candles_by_code(
    db,
    start_key: str,
    end_key: str,
    rsi_period: int = 3,
) -> Tuple[Dict[str, List[Dict]], int]:
    """ws_candles 1분봉 전 종목 로드 (+ 기간 전 웜업 prepend)."""
    codes_raw = db.conn.execute(
        "SELECT DISTINCT code FROM ws_candles WHERE timeframe=1 "
        "AND candle_time >= %s AND candle_time <= %s ORDER BY code",
        [start_key, end_key],
    ).fetchall()
    codes = [r["code"] for r in codes_raw]

    candles_by_code: Dict[str, List[Dict]] = {}
    total_candles = 0
    min_bars = int(rsi_period) + 5

    for code in codes:
        rows = db.conn.execute(
            "SELECT candle_time, open, high, low, close, volume "
            "FROM ws_candles WHERE timeframe=1 AND code=%s "
            "AND candle_time >= %s AND candle_time <= %s AND is_confirmed=1 "
            "ORDER BY candle_time ASC",
            [code, start_key, end_key],
        ).fetchall()
        if len(rows) < min_bars:
            continue
        candles_by_code[code] = [dict(r) for r in rows]
        total_candles += len(rows)

    prepend_scalp_candle_warmup(db, candles_by_code, str(start_key)[:12])
    return candles_by_code, total_candles


def _scalp_backtest_wants_ticks(params: Optional[Dict[str, Any]] = None) -> bool:
    """청산·진입 틱 재생이 필요한지 (기본 ON)."""
    if strategy_use_tick_exit(params, "SCALP_BACKTEST_USE_TICK_EXIT", default=True):
        return True
    if params is not None and params.get("backtest_use_tick_entry") is not None:
        return se._to_bool(params.get("backtest_use_tick_entry"), True)
    return get_env_bool("SCALP_BACKTEST_USE_TICK_ENTRY", True)


def run_scalping_backtest_web_aligned(
    candles_by_code: Dict[str, List[Dict]],
    params: Dict[str, Any],
    universe_by_slot: Optional[Dict[str, List[str]]],
    *,
    slot_money: float,
    fee_rate: float,
    sell_tax: float,
    max_stocks: Optional[int] = None,
    total_budget_krw: Optional[float] = None,
    meta_out: Optional[Dict[str, Any]] = None,
    mode: str = "reversal",
    ticks_by_code: Optional[Dict[str, Dict[str, List[Dict]]]] = None,
) -> List[Dict]:
    """엔진 1회 + 웹과 동일 손익 부착 (reversal / momentum)."""
    engine_params = dict(params)
    engine_params["slot_money"] = float(slot_money)
    if max_stocks is not None:
        engine_params["max_stocks"] = int(max_stocks)
    if total_budget_krw is not None:
        tb = float(total_budget_krw)
        engine_params["total_budget_krw"] = tb if tb > 0 else float(
            int(engine_params.get("max_stocks") or 3) * slot_money
        )
    if universe_by_slot is not None:
        engine_params.setdefault("scan_interval_min", 1)
    engine_params.setdefault("portfolio_mode", True)

    # 기간 시작 키 — 웜업 봉이 all_times 에 섞이지 않도록 (돌파·모멘텀과 동일)
    _sk_w = str((meta_out or {}).get("start_key") or "")[:12]
    if _sk_w:
        engine_params["_backtest_period_start_key"] = _sk_w
        _db_w = (meta_out or {}).get("db")
        if _db_w is not None and str(mode).strip().lower() != "momentum":
            prepend_scalp_candle_warmup(_db_w, candles_by_code, _sk_w)

    from kis_trader.backtest.backtest_env_timeline import attach_backtest_env_timeline_to_params
    attach_backtest_env_timeline_to_params(engine_params, meta_out, "SCALP")

    if str(mode).strip().lower() == "momentum":
        from kis_trader.backtest import momentum_backtest_common as mbc
        trades = mbc.run_momentum_backtest_web_aligned(
            candles_by_code, engine_params, universe_by_slot,
            slot_money=float(slot_money),
            fee_rate=float(fee_rate),
            sell_tax=float(sell_tax),
            max_stocks=int(engine_params.get("max_stocks") or max_stocks or 3),
            total_budget_krw=float(engine_params.get("total_budget_krw") or 0),
            meta_out=meta_out,
        )
    else:
        loaded_ticks: Dict[str, Dict[str, List[Dict]]] = dict(ticks_by_code or {})
        tick_meta: Dict[str, Any] = {}
        if _scalp_backtest_wants_ticks(engine_params):
            if not loaded_ticks and meta_out is not None:
                start_key = str(meta_out.get("start_key") or "")
                end_key = str(meta_out.get("end_key") or "")
                db = meta_out.get("db")
                if db and start_key and end_key:
                    loaded_ticks, tick_rows = load_breakout_ticks_by_code(
                        db, start_key, end_key, set(candles_by_code.keys()),
                    )
                    tick_meta = tick_coverage_stats(candles_by_code, loaded_ticks)
                    tick_meta["ws_tick_rows_loaded"] = tick_rows
                    if tick_rows <= 0:
                        from kis_trader.utils.logger import get_logger as _get_logger

                        _get_logger("kis_trader.scalping_backtest").warning(
                            "⚠️ ws_ticks 데이터 없음 — SCALP 틱 청산/진입 스킵 "
                            "(틱 수집 후 재백테 권장, FALLBACK_OHLC=1 시 OHLC)",
                        )
            elif loaded_ticks:
                tick_meta = tick_coverage_stats(candles_by_code, loaded_ticks)
                tick_meta["ws_tick_rows_loaded"] = sum(
                    len(lst) for cm in loaded_ticks.values() for lst in cm.values()
                )

        trades = se.run_scalping_backtest(
            candles_by_code, engine_params, universe_by_slot=universe_by_slot,
            ticks_by_code=loaded_ticks or None,
        )
        if meta_out is not None and tick_meta:
            from kis_trader.backtest.breakout_tick_loader import enrich_tick_meta_with_traded_codes
            tick_meta = enrich_tick_meta_with_traded_codes(
                tick_meta, candles_by_code, loaded_ticks, trades,
            )
            meta_out["tick_backtest"] = tick_meta
            if tick_meta.get("ws_tick_rows_loaded", 0) > 0:
                meta_out["backtest_buy_source"] = "ws_ticks"
            elif _scalp_backtest_wants_ticks(engine_params):
                meta_out["backtest_buy_source"] = "ohlc_fallback"
            else:
                meta_out["backtest_buy_source"] = "align"
        if meta_out is not None:
            meta_out["universe_exit_debounce_sec"] = scalp_universe_exit_debounce_sec()
            meta_out["candle_warmup_bars"] = scalp_backtest_candle_warmup_bars()

    attach_scalp_trade_pnl(
        trades, fee_rate=fee_rate, sell_tax=sell_tax,
        slip_pct=backtest_slip_pct(engine_params),
    )
    if meta_out is not None and str(mode).strip().lower() != "momentum":
        skip_stats = engine_params.get("_portfolio_skip_stats") or {}
        meta_out["skip_stats"] = dict(skip_stats)
        meta_out["engine_params"] = engine_params
    return trades


def resolve_scalp_portfolio_params(
    env_row: Optional[Dict[str, Any]],
    base_defaults: Optional[Dict[str, Any]] = None,
    *,
    strategy: str = "SCALP",
    slot_money: Optional[float] = None,
    max_stocks: Optional[int] = None,
    total_budget_krw: Optional[float] = None,
) -> Dict[str, Any]:
    """웹·파라서치 공통 포트폴리오 해석."""
    return resolve_portfolio_params(
        env_row,
        base_defaults,
        strategy=strategy,
        slot_money=slot_money,
        max_stocks=max_stocks,
        total_budget_krw=total_budget_krw,
    )


def merge_scalp_portfolio_into_params(
    params: Dict[str, Any],
    portfolio: Dict[str, Any],
) -> Dict[str, Any]:
    return merge_portfolio_into_params(params, portfolio)


def build_scalp_budget_warning(
    portfolio: Dict[str, Any],
    skip_stats: Optional[Dict[str, Any]] = None,
    *,
    strategy: str = "SCALP",
) -> Optional[str]:
    ratio = min_invest_ratio_of_slot({}, strategy=strategy)
    return build_budget_warning(portfolio, skip_stats, min_invest_ratio=ratio)


def summarize_scalp_trades(
    trades: List[Dict],
    *,
    total_budget_krw: float,
    period_days: int = 1,
) -> Dict[str, Any]:
    return summarize_trades(
        trades,
        total_budget_krw=total_budget_krw,
        period_days=period_days,
    )


def fee_and_slot_from_env(
    row: Optional[Dict[str, Any]],
    *,
    strategy: str = "SCALP",
) -> Tuple[float, float, float]:
    return fee_and_slot_from_env_row(row, strategy=strategy)
