"""백테스트 엔진 — 과거 일봉을 하루씩 되감으며 signals.py 규칙으로 가상 매매.

미래정보 차단:
  - 지표는 그날(t)까지의 봉으로만 계산, 체결은 당일 종가 (다음 봉 미참조).
  - 애널 게이트는 과거 재현 불가 → 중립(analyst=None, 자동 통과).
  - 수급은 flow_history(있으면) 사용, 없으면 중립(통과)으로 표시.
  - 시장 필터(ADR)는 데이터가 짧아 backtest 에선 통과 처리.

엔진과 동일한 signals.compute_indicators / evaluate_candidate / evaluate_holding /
compute_stop_target 를 그대로 재사용한다 (페이퍼와 같은 두뇌).
"""
from __future__ import annotations

import math
import sqlite3
import sys
from datetime import datetime

from . import config, signals


def _cal_days(entry_date: str | None, cur_date: str) -> int | None:
    """두 'YYYYMMDD' 사이 달력 일수 — 시간손절용. 파싱 실패 시 None."""
    if not entry_date:
        return None
    try:
        d0 = datetime.strptime(entry_date, '%Y%m%d')
        d1 = datetime.strptime(cur_date, '%Y%m%d')
        return max(0, (d1 - d0).days)
    except Exception:
        return None

sys.path.insert(0, str(config.SCRIPTS))
import daily_candles_cache as dcc  # noqa: E402

FLOW_DB = config.STATE_DIR / 'flow_history.sqlite'
WINDOW = 140  # 지표 계산에 넘길 최근 봉 수 (성능 상한 — 모든 지표 기간 + 버퍼 충분)


def apply_params(overrides: dict | None):
    """튜닝 오버라이드를 config 전역에 반영 (없는 키는 기본값). 각 run 독립 보장."""
    overrides = overrides or {}
    for spec in config.TUNABLE:
        k = spec['key']
        v = overrides.get(k, config._DEFAULTS[k])
        setattr(config, k, v)


def load_history(code: str, count: int = 600) -> list[dict]:
    """캐시(sqlite)에서 일봉 오름차순. 네트워크 X."""
    return list(reversed(dcc._select_latest(code, count)))


def load_flow(code: str) -> dict | None:
    """flow_history.sqlite 에서 {date: (foreign, institution)}. 없으면 None (수급 중립)."""
    if not FLOW_DB.exists():
        return None
    try:
        c = sqlite3.connect(FLOW_DB)
        rows = c.execute('SELECT date, foreign_net, inst_net FROM flow WHERE code=? ORDER BY date', (code,)).fetchall()
        c.close()
        return {r[0]: (r[1], r[2]) for r in rows} or None
    except Exception:
        return None


# 프로세스 메모이즈 — 같은 sweep(한 프로세스) 안에서 종목당 sqlite를 1회만 읽는다.
# backtest.run 은 sweep CLI 프로세스에서만 호출되므로(엔진/웹은 apply_params만 씀) stale 위험 없음.
_HIST_MEMO: dict = {}
_FLOW_MEMO: dict = {}


def _hist_cached(code: str) -> list[dict]:
    if code not in _HIST_MEMO:
        _HIST_MEMO[code] = load_history(code)
    return _HIST_MEMO[code]


def _flow_cached(code: str):
    if code not in _FLOW_MEMO:
        _FLOW_MEMO[code] = load_flow(code)
    return _FLOW_MEMO[code]


def _flow_net_upto(flow: dict | None, dates_seen: list[str]) -> dict:
    """최근 FLOW_DAYS 일 외국인·기관 누적. flow 없으면 중립(둘 다 +1 → 수급 통과)."""
    if flow is None:
        return {'foreign': 1, 'institution': 1, 'days': 0, 'neutral': True}
    recent = dates_seen[-config.FLOW_DAYS:]
    f = sum(flow.get(d, (0, 0))[0] for d in recent)
    i = sum(flow.get(d, (0, 0))[1] for d in recent)
    return {'foreign': f, 'institution': i, 'days': len(recent)}


def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to: str = '9',
        capital: float | None = None) -> dict:
    """백테스트 1회. 반환: 지표 dict (+ trades 수)."""
    apply_params(overrides)
    capital = capital or config.INITIAL_CAPITAL

    hist = {}
    for code in codes:
        h = [c for c in _hist_cached(code) if date_from <= c['date'] <= date_to]
        if len(h) > config.SMA_LONG + config.ATR_PERIOD + 5:
            hist[code] = h
    if not hist:
        return {'error': 'no_data', 'trades': 0}
    flows = {code: _flow_cached(code) for code in hist}

    # code별 date→index, 전체 거래일 축
    idx_map = {code: {c['date']: i for i, c in enumerate(h)} for code, h in hist.items()}
    all_dates = sorted({c['date'] for h in hist.values() for c in h})

    cash = capital
    positions: dict[str, dict] = {}
    realized = 0.0
    wins = closed = 0
    gross_win = gross_loss = 0.0
    peak_eq = capital
    exited: dict[str, str] = {}   # code → 전량청산일 (당일 재진입 금지)
    max_dd = 0.0
    comm, tax = config.COMMISSION_RATE, config.SELL_TAX_RATE

    def equity(day_prices):
        return cash + sum(p['qty'] * day_prices.get(c, p['entry_price']) for c, p in positions.items())

    def _qty_for(fill, value):
        return int(value // (fill * (1 + comm)))

    slip = getattr(config, 'BT_SLIPPAGE', 0.002)
    pending: list[dict] = []   # 전일 종가 신호 → 오늘 시가 체결 주문 (look-ahead 제거)

    for day in all_dates:
        day_prices = {}

        # ---- 0. 전일 큐잉 주문을 오늘 시가에 체결 (+슬리피지: 매수 비싸게/매도 싸게) ----
        for od in pending:
            code = od['code']
            i = idx_map.get(code, {}).get(day)
            if i is None:
                continue   # 오늘 거래 없으면 주문 소멸 — 신호 지속 시 다음 종가에 재큐잉됨
            open_px = hist[code][i].get('open') or hist[code][i]['close']
            kind = od['kind']
            if kind in ('sell', 'scale_out'):
                pos = positions.get(code)
                if not pos:
                    continue
                fill = open_px * (1 - slip)
                total = pos['qty']
                qty = total if od.get('frac', 1.0) >= 1.0 else max(1, int(total * od['frac']))
                qty = min(qty, total)
                proceeds = qty * fill * (1 - comm - tax)
                cost = qty * pos['entry_price'] * (1 + comm)
                pnl = proceeds - cost
                cash += proceeds
                realized += pnl
                if qty >= total:
                    positions.pop(code)
                    exited[code] = day   # 당일 재진입 금지 (엔진과 동일 규칙)
                    closed += 1
                    if pnl > 0:
                        wins += 1
                else:
                    pos['qty'] = total - qty
                if pnl > 0:
                    gross_win += pnl
                else:
                    gross_loss += -pnl
            elif kind == 'add':
                pos = positions.get(code)
                if not pos:
                    continue
                fill = open_px * (1 + slip)
                tranche_val = pos.get('tranche_value') or (pos['entry_price'] * pos['qty'])
                qty = _qty_for(fill, tranche_val)
                cost = qty * fill * (1 + comm)
                if qty >= 1 and cost <= cash:
                    new_qty = pos['qty'] + qty
                    pos['entry_price'] = (pos['entry_price'] * pos['qty'] + fill * qty) / new_qty
                    pos['qty'] = new_qty
                    pos['tranches'] = pos.get('tranches', 1) + 1
                    pos['last_add_price'] = fill
                    cash -= cost
                    nstop, ntarget = signals.compute_stop_target(pos['entry_price'], od['atr'], od['recent_low'])
                    pos['stop'] = nstop if od.get('add_kind') == 'down' else max(pos['stop'], nstop)
                    pos['target'] = ntarget
            elif kind == 'buy':
                if code in positions or len(positions) >= config.MAX_POSITIONS or exited.get(code) == day:
                    continue
                fill = open_px * (1 + slip)
                stop, target = signals.compute_stop_target(fill, od['atr'], od['recent_low'])
                eq = equity(day_prices)
                tranche_val = signals.target_value(eq, fill, stop) / max(1, config.ENTRY_TRANCHES)
                qty = _qty_for(fill, tranche_val)
                cost = qty * fill * (1 + comm)
                if qty < 1 or cost > cash:
                    continue
                cash -= cost
                positions[code] = {'code': code, 'name': code, 'qty': qty, 'entry_price': fill,
                                   'stop': stop, 'target': target, 'peak': fill, 'trailing_on': False,
                                   'scaled_out': False, 'tranches': 1, 'tranche_value': tranche_val,
                                   'last_add_price': fill, 'entry_date': day}
        pending = []

        # ---- 1. 보유 판단 (당일 종가 기준) → 내일 시가 주문 큐잉 ----
        for code in list(positions.keys()):
            h = hist.get(code)
            i = idx_map[code].get(day)
            if i is None:
                continue
            window = h[max(0, i - WINDOW):i + 1]
            ind = signals.compute_indicators(window)
            if ind is None:
                continue
            day_prices[code] = ind['price']
            dates_seen = [c['date'] for c in h[:i + 1]]
            flow = _flow_net_upto(flows.get(code), dates_seen)
            pos = positions[code]
            dec = signals.evaluate_holding(pos, ind, flow, None,
                                           held_days=_cal_days(pos.get('entry_date'), day))
            pos.update(dec['position_update'])
            act = dec['action']
            if act in ('sell', 'scale_out'):
                pending.append({'kind': act, 'code': code, 'frac': dec.get('sell_frac', 1.0)})
            elif act == 'add':
                pending.append({'kind': 'add', 'code': code, 'add_kind': dec.get('add_kind'),
                                'atr': ind['atr'], 'recent_low': ind['recent_low']})

        # ---- 2. 신규 매수 판단 (당일 종가) → 내일 시가 주문 큐잉 ----
        for code, h in hist.items():
            if code in positions or len(positions) >= config.MAX_POSITIONS:
                continue
            if exited.get(code) == day:   # 당일 청산 종목 재매수 금지
                continue
            i = idx_map[code].get(day)
            if i is None:
                continue
            window = h[max(0, i - WINDOW):i + 1]
            ind = signals.compute_indicators(window)
            if ind is None:
                continue
            day_prices[code] = ind['price']
            if not signals.trend_ok(ind):
                continue
            dates_seen = [c['date'] for c in h[:i + 1]]
            flow = _flow_net_upto(flows.get(code), dates_seen)
            dec = signals.evaluate_candidate(code, code, [], ind, flow, None, True)
            if dec['action'] == 'buy':
                pending.append({'kind': 'buy', 'code': code,
                                'atr': ind['atr'], 'recent_low': ind['recent_low']})

        eq = equity(day_prices)
        if eq > peak_eq:
            peak_eq = eq
        dd = (peak_eq - eq) / peak_eq if peak_eq else 0
        if dd > max_dd:
            max_dd = dd

    # 마지막 날 종가로 잔여 포지션 청산 평가 (미실현 포함 최종자산)
    final_prices = {}
    for code, p in positions.items():
        h = hist[code]
        final_prices[code] = h[-1]['close']
    final_eq = cash + sum(p['qty'] * final_prices[c] for c, p in positions.items())

    n_days = len(all_dates)
    total_ret = (final_eq / capital - 1) * 100
    years = n_days / 252 if n_days else 0
    cagr = ((final_eq / capital) ** (1 / years) - 1) * 100 if years > 0 and final_eq > 0 else 0
    pf = (gross_win / gross_loss) if gross_loss > 0 else (math.inf if gross_win > 0 else 0)

    d_from = all_dates[0] if all_dates else None
    d_to = all_dates[-1] if all_dates else None
    from . import benchmark
    bench = benchmark.compare(round(total_ret, 2), d_from, d_to) if d_from else {}

    return {
        'final_equity': round(final_eq),
        'total_return_pct': round(total_ret, 2),
        'cagr_pct': round(cagr, 2),
        'mdd_pct': round(max_dd * 100, 2),
        'trades': closed,
        'open_positions': len(positions),
        'win_rate_pct': round(wins / closed * 100, 1) if closed else None,
        'profit_factor': round(pf, 2) if pf != math.inf else 'inf',
        'days': n_days,
        'from': d_from,
        'to': d_to,
        'flow_used': any(f is not None for f in flows.values()),
        'benchmark': bench,
        'alpha_pct': bench.get('KOSPI', {}).get('alpha'),
    }
