"""
1단계 시뮬레이션 HTML — ground_truth 차트와 동일 스타일·타점·수익률·규칙 기준.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pandas as pd

from config import (
    CHART_LOOKBACK_DAYS,
    COIN_NAME,
    GT_INITIAL_CASH_KRW,
    LIVE_ORDER_KRW,
    SYMBOL,
    TRADING_FEE_RATE,
)
from deepcoin.ground_truth.ground_truth import (
    load_ground_truth,
    order_trades_chronological,
    simulate_truth_portfolio,
    simulate_truth_portfolio_steps,
)
from deepcoin.matching.portfolio_sim import (
    fires_to_trade_list,
    simulate_fixed_order_portfolio,
    simulate_fixed_order_portfolio_steps,
    simulate_sized_portfolio,
    sort_fires_chronological,
)
from deepcoin.matching.select_rules import _split_train_valid_holdout
from deepcoin.ops.chart_report import (
    card_html,
    go_no_go_table_html,
    market_cards_html,
    pnl_cards_html,
    rule_criteria_html,
    stacked_summary_cards_html,
    wrap_chart_report_page,
)
from deepcoin.ops.simulation import build_chart_html, load_chart_frames, _frames_to_mtf
from deepcoin.common.indicators import apply_bar_indicators, get_trend
from deepcoin.paths import (
    MATCHING_FIRE_OUTCOMES,
    MATCHING_MATCHED_RULES,
    MATCHING_SIMULATION_HTML,
    resolve_ground_truth_file,
)


def _fires_to_chart_trades(fires: pd.DataFrame) -> list[dict[str, Any]]:
    """
    fire_outcomes 행을 차트 마커용 dict 리스트로 변환.

    Args:
        fires: monitor holdout 발화.

    Returns:
        build_chart_html sim_trades 인자.
    """
    rows: list[dict[str, Any]] = []
    for _, r in fires.iterrows():
        rows.append(
            {
                "dt": str(r["dt"]),
                "action": r["side"],
                "price": float(r["close"]),
                "forward_ret_pct": float(r["forward_ret_pct"]),
                "rule_id": r["rule_id"],
            }
        )
    return rows


def _sim_fire_table_rows(
    fires: pd.DataFrame,
    rules_by_id: dict[str, dict],
    steps: list[dict[str, Any]],
) -> str:
    """
    시뮬 발화 테이블 tbody (GT와 동일하게 총 평가금액 포함).

    Args:
        fires: holdout 체결 발화.
        rules_by_id: rule_id → rule dict.
        steps: simulate_fixed_order_portfolio_steps 결과.

    Returns:
        tr HTML.
    """
    if fires.empty:
        return "<tr><td colspan='8'>발화 없음</td></tr>"
    sorted_fires = fires.sort_values("dt").reset_index(drop=True)
    lines: list[str] = []
    lines.append(
        f"""
        <tr class="initial-row">
          <td>시작</td><td>-</td><td>-</td><td>-</td><td>-</td>
          <td><b>₩{GT_INITIAL_CASH_KRW:,.0f}</b></td>
          <td>-</td><td>초기 현금 (1회 ₩{LIVE_ORDER_KRW:,.0f} 가정)</td>
        </tr>"""
    )
    for i in range(len(sorted_fires)):
        r = sorted_fires.iloc[i]
        side = r["side"]
        cls = "buy" if side == "buy" else "sell"
        mark = "매수" if side == "buy" else "매도"
        ret = float(r["forward_ret_pct"])
        ret_s = f" (+{ret:.2f}%)" if ret > 0 else f" ({ret:.2f}%)"
        win = "승" if int(r.get("win", 0)) else "패"
        win_cls = "pass" if int(r.get("win", 0)) else "fail"
        kind = rules_by_id.get(r["rule_id"], {}).get("kind", "")
        step = steps[i] if i < len(steps) else None
        if step:
            total_s = f"₩{step['total_asset_krw']:,.0f}"
            hold_s = (
                f" (현금 ₩{step['cash_krw']:,.0f} + 코인 {step['holding_qty']:,.2f}개)"
            )
        else:
            total_s = "-"
            hold_s = ""
        lines.append(
            f"<tr>"
            f"<td>{str(r['dt'])[:16]}</td>"
            f'<td class="{cls}">{mark}</td>'
            f"<td>{r['rule_id']}</td>"
            f"<td>{kind}</td>"
            f'<td class="num">₩{float(r["close"]):,.0f}{ret_s}</td>'
            f"<td><b>{total_s}</b>{hold_s}</td>"
            f'<td class="{win_cls}">{win}</td>'
            f"<td>leg_gt 구간 수익</td>"
            f"</tr>"
        )
    return "\n".join(lines)


def _gt_table_rows(trades: list[dict[str, Any]], steps: list[dict[str, Any]]) -> str:
    """
    GT 타점 테이블 tbody (ground_truth 차트와 동일).

    Args:
        trades: ground_truth trades.
        steps: simulate_truth_portfolio_steps.

    Returns:
        tr HTML.
    """
    if not trades:
        return "<tr><td colspan='6'>타점 없음</td></tr>"
    step_key = {
        (s["dt"], s["action"], float(s["price"]), float(s["weight"])): s
        for s in steps
    }
    lines: list[str] = []
    lines.append(
        f"""
        <tr class="initial-row">
          <td>시작</td><td>-</td><td>-</td><td>-</td>
          <td><b>₩{GT_INITIAL_CASH_KRW:,.0f}</b></td>
          <td>초기 현금 (보유 0)</td>
        </tr>"""
    )
    for t in sorted(trades, key=lambda x: x["dt"]):
        cls = "buy" if t["action"] == "buy" else "sell"
        mark = "매수" if t["action"] == "buy" else "매도"
        ret = t.get("forward_return_pct")
        ret_s = f" (+{ret}%)" if ret is not None else ""
        w = float(t.get("weight", 1.0))
        key = (t["dt"], t["action"], float(t["price"]), w)
        step = step_key.get(key)
        if step:
            total_s = f"₩{step['total_asset_krw']:,.0f}"
            hold_s = (
                f" (현금 ₩{step['cash_krw']:,.0f} + 코인 {step['holding_qty']:,.2f}개)"
            )
        else:
            total_s = "-"
            hold_s = ""
        lines.append(
            f"<tr>"
            f"<td>{t['dt'][:16]}</td>"
            f'<td class="{cls}">{mark}</td>'
            f"<td>{w*100:.0f}%</td>"
            f"<td>₩{t['price']:,.0f}{ret_s}</td>"
            f"<td><b>{total_s}</b>{hold_s}</td>"
            f"<td>{t.get('memo', '')}</td>"
            f"</tr>"
        )
    return "\n".join(lines)


def _summary_cards_html(
    close_last: float,
    bb_txt: str,
    gt_trades: list[dict[str, Any]],
    gt_pnl: dict[str, Any],
    sim_sized_pnl: dict[str, Any],
    sim_fixed_pnl: dict[str, Any],
    sim_trade_count: int,
    go_flag: bool,
    model_note: str = "",
    sim_causal_gt_pnl: dict[str, Any] | None = None,
    sim_causal_hybrid_pnl: dict[str, Any] | None = None,
    sim_tier_enhanced_pnl: dict[str, Any] | None = None,
    sim_primary_pnl: dict[str, Any] | None = None,
    primary_sizing: str = "",
    hybrid_go: bool = False,
    phase2_go: bool = False,
) -> str:
    """
    ground_truth HTML과 동일 구성의 상단 카드 (GT + 시뮬 2줄).

    Args:
        close_last: 종가.
        bb_txt: BB %B.
        gt_trades: GT trades.
        gt_pnl: GT 포트폴리오 요약.
        sim_sized_pnl: 총자산%·EV/WF·leg 시뮬 요약.
        sim_fixed_pnl: 고정 ₩/회 baseline.
        sim_trade_count: 체결 가정 발화 수.
        go_flag: Go/No-Go.
        model_note: GT 모델 한 줄 요약.
        sim_causal_gt_pnl: 인과 GT leg 엔진 요약.
        sim_causal_hybrid_pnl: monitor buy + 인과 sell 하이브리드.
        sim_tier_enhanced_pnl: monitor + conviction tier.
        sim_primary_pnl: 검증 통과 권장 배분 경로.
        primary_sizing: hybrid | causal_tier.
        hybrid_go: hybrid tier Go/No-Go.

    Returns:
        cards HTML.
    """
    go_cls = "go-pass" if go_flag else "go-fail"
    gt_sub = (
        "저점 분할매수(1/price 비중) · 고점 65/35% 매도 · "
        "총자산×비중×leg티어 · 시각순 복리"
    )
    if model_note:
        gt_sub = model_note
    gt_cards = market_cards_html(close_last, bb_txt) + pnl_cards_html(
        gt_pnl, "정답 GT", len(gt_trades)
    )
    sim_sized_title = (
        "시뮬·GT tier 복리 (전기간, 상한 없음) — "
        f'<span class="{go_cls}">{"GO" if go_flag else "NO-GO"}</span>'
    )
    sim_fixed_title = f"시뮬·고정 ₩{LIVE_ORDER_KRW:,}/회 (비교)"
    primary_block = ""
    if sim_primary_pnl and float(sim_primary_pnl.get("pnl_pct") or 0) != 0:
        hgo_cls = "go-pass" if hybrid_go else "go-fail"
        primary_block = stacked_summary_cards_html(
            (
                f"권장 primary · {primary_sizing or 'causal_tier'} · "
                f'<span class="{hgo_cls}">hybrid {"GO" if hybrid_go else "NO-GO"}</span> · '
                f'2차 {"GO" if phase2_go else "NO-GO"}'
            ),
            pnl_cards_html(
                sim_primary_pnl,
                "Primary",
                int(sim_primary_pnl.get("trade_count") or 0),
            ),
        )
    causal_block = ""
    if sim_causal_gt_pnl and (
        float(sim_causal_gt_pnl.get("pnl_pct") or 0) != 0
        or sim_causal_gt_pnl.get("trade_count")
    ):
        legs = sim_causal_gt_pnl.get("leg_count", "-")
        causal_block += stacked_summary_cards_html(
            f"인과 GT leg 엔진 (peak_local·분할매수·causal tier) · leg {legs}개",
            pnl_cards_html(
                sim_causal_gt_pnl,
                "인과 GT",
                int(sim_causal_gt_pnl.get("trade_count") or 0),
            ),
        )
    if sim_causal_hybrid_pnl and (
        float(sim_causal_hybrid_pnl.get("pnl_pct") or 0) != 0
        or sim_causal_hybrid_pnl.get("trade_count")
    ):
        legs_h = sim_causal_hybrid_pnl.get("leg_count", "-")
        causal_block += stacked_summary_cards_html(
            f"하이브리드 (monitor + DD tier) · 발화 {sim_causal_hybrid_pnl.get('input_fires', '-')}건",
            pnl_cards_html(
                sim_causal_hybrid_pnl,
                "하이브리드",
                int(sim_causal_hybrid_pnl.get("trade_count") or 0),
            ),
        )
    if sim_tier_enhanced_pnl and (
        float(sim_tier_enhanced_pnl.get("pnl_pct") or 0) != 0
        or sim_tier_enhanced_pnl.get("trade_count")
    ):
        ast = sim_tier_enhanced_pnl.get("alloc_stats") or {}
        causal_block += stacked_summary_cards_html(
            f"Enhanced tier (conviction·DD) · large매수 {ast.get('large_tier_buy_count', '-')}건",
            pnl_cards_html(
                sim_tier_enhanced_pnl,
                "Enhanced",
                int(sim_tier_enhanced_pnl.get("trade_count") or 0),
            ),
        )
    return (
        '<div class="summary-cards">'
        + stacked_summary_cards_html(gt_sub, gt_cards)
        + stacked_summary_cards_html(
            sim_sized_title,
            pnl_cards_html(sim_sized_pnl, "시뮬(비율)", sim_trade_count),
        )
        + stacked_summary_cards_html(
            sim_fixed_title,
            pnl_cards_html(sim_fixed_pnl, "시뮬(고정)", sim_trade_count),
        )
        + primary_block
        + causal_block
        + "</div>"
    )


def build_simulation_page_html(
    report: dict[str, Any],
    outcomes_path: Path | None = None,
    matched_path: Path | None = None,
    gt_path: Path | None = None,
    close_last: float | None = None,
) -> str:
    """
    시뮬 리포트 전체 HTML (차트 + Go/No-Go + 규칙 기준 + 타점 테이블).

    Args:
        report: build_simulation_report 결과.
        outcomes_path: fire_outcomes.csv.
        matched_path: matched_rules.json.
        gt_path: ground_truth JSON.
        close_last: 미청산 평가 종가 (None이면 DB 종가).

    Returns:
        HTML 문자열.
    """
    op = outcomes_path or MATCHING_FIRE_OUTCOMES
    mp = matched_path or MATCHING_MATCHED_RULES
    matched: dict[str, Any] = {}
    if mp.is_file():
        matched = json.loads(mp.read_text(encoding="utf-8"))

    monitor_rules = matched.get("monitor_rules") or report.get("monitor_rules") or []
    monitor_ids = {r["rule_id"] for r in monitor_rules}
    rules_by_id = {r["rule_id"]: r for r in monitor_rules}

    sim_fires = pd.DataFrame()
    holdout_fires = pd.DataFrame()
    if op.is_file():
        outcomes = pd.read_csv(op)
        outcomes["split"] = _split_train_valid_holdout(outcomes)
        sim_fires = outcomes[outcomes["rule_id"].isin(monitor_ids)].copy()
        holdout_fires = outcomes[
            (outcomes["rule_id"].isin(monitor_ids)) & (outcomes["split"] == "holdout")
        ].copy()

    compound_fires = sort_fires_chronological(sim_fires)

    gt_data = load_ground_truth(gt_path or resolve_ground_truth_file()) or {}
    gt_trades = gt_data.get("trades") or []
    gt_summary = gt_data.get("summary") or {}

    go = report.get("go_no_go", {})
    go_flag = bool(go.get("go"))
    go_hybrid = report.get("go_no_go_hybrid") or {}
    hybrid_go_flag = bool(go_hybrid.get("go"))
    go_phase2 = report.get("go_no_go_option_c_phase2") or {}
    phase2_go_flag = bool(go_phase2.get("go"))
    pc = report.get("portfolio_compare") or {}
    label_mode = report.get("label_mode", "leg_gt")

    frames = load_chart_frames()
    bb_txt = "-"
    trend = "-"
    close_val = float(close_last or 0)
    if frames is not None:
        df_1d, df_1h, df_3m = _frames_to_mtf(frames)
        trend = get_trend(df_1d, df_1h)
        df_chart = apply_bar_indicators(df_3m)
        close_val = float(df_chart["Close"].iloc[-1])
        bb_pos = (
            float(df_chart["bb_pos"].iloc[-1])
            if "bb_pos" in df_chart.columns and pd.notna(df_chart["bb_pos"].iloc[-1])
            else None
        )
        bb_txt = f"{bb_pos:.2f}" if bb_pos is not None else "-"
    elif gt_summary.get("mark_price"):
        close_val = float(gt_summary["mark_price"])

    sim_trades_sized = fires_to_trade_list(compound_fires, apply_dynamic_sizing=True)
    sim_trades_fixed = fires_to_trade_list(compound_fires, apply_dynamic_sizing=False)

    gt_pnl: dict[str, Any] = {}
    if gt_trades:
        gt_chron = order_trades_chronological(gt_trades)
        gt_pnl = simulate_truth_portfolio(
            gt_chron,
            initial_cash=GT_INITIAL_CASH_KRW,
            fee_rate=TRADING_FEE_RATE,
            last_price=close_val if close_val else None,
        )

    mark = close_val if close_val else None
    sim_sized_pnl = simulate_sized_portfolio(
        sim_trades_sized,
        initial_cash=GT_INITIAL_CASH_KRW,
        fee_rate=TRADING_FEE_RATE,
        last_price=mark,
    )
    sim_fixed_pnl = simulate_fixed_order_portfolio(
        sim_trades_fixed,
        order_krw=LIVE_ORDER_KRW,
        initial_cash=GT_INITIAL_CASH_KRW,
        fee_rate=TRADING_FEE_RATE,
        last_price=mark,
        sizing_mode="fixed",
    )
    sim_steps = simulate_fixed_order_portfolio_steps(
        sim_trades_sized,
        order_krw=LIVE_ORDER_KRW,
        initial_cash=GT_INITIAL_CASH_KRW,
        fee_rate=TRADING_FEE_RATE,
        sizing_mode="amount_krw",
    )
    gt_steps = (
        simulate_truth_portfolio_steps(
            order_trades_chronological(gt_trades),
            initial_cash=GT_INITIAL_CASH_KRW,
            fee_rate=TRADING_FEE_RATE,
        )
        if gt_trades
        else []
    )
    model = gt_data.get("model") or {}
    model_note = (
        f"mode={model.get('selection_mode', 'split_buy_peak_sell')} · "
        f"매수비중=1/price · 매도=65/35%"
        if model
        else ""
    )

    criteria_blocks = "".join(rule_criteria_html(r) for r in monitor_rules)
    go_table = go_no_go_table_html(go.get("checks", []), go_flag)
    hybrid_checks = go_hybrid.get("checks") or []
    if hybrid_checks:
        hybrid_rows = "".join(
            f"<tr><td>{c.get('name')}</td>"
            f"<td>{'PASS' if c.get('pass') else 'FAIL'}</td>"
            f"<td>{c.get('value', '-')}</td></tr>"
            for c in hybrid_checks
        )
        hgo_cls = "go-pass" if hybrid_go_flag else "go-fail"
        go_table += (
            f"<h2>Hybrid tier Go/No-Go · "
            f'<span class="{hgo_cls}">{"GO" if hybrid_go_flag else "NO-GO"}</span></h2>'
            f"<table><thead><tr><th>검사</th><th>결과</th><th>값</th></tr></thead>"
            f"<tbody>{hybrid_rows}</tbody></table>"
        )
    phase2_checks = go_phase2.get("checks") or []
    if phase2_checks:
        p2_rows = "".join(
            f"<tr><td>{c.get('name')}</td>"
            f"<td>{'PASS' if c.get('pass') else 'FAIL'}</td>"
            f"<td>{c.get('value', '-')}</td></tr>"
            for c in phase2_checks
        )
        p2_cls = "go-pass" if phase2_go_flag else "go-fail"
        go_table += (
            f"<h2>Option C 2차 (+1000%) · "
            f'<span class="{p2_cls}">{"GO" if phase2_go_flag else "NO-GO"}</span></h2>'
            f"<table><thead><tr><th>검사</th><th>결과</th><th>값</th></tr></thead>"
            f"<tbody>{p2_rows}</tbody></table>"
        )

    def _mark_note(price: float) -> str:
        if price > 0:
            return f" 총보유자산(미청산 포함)은 종가 ₩{price:,.0f} 평가."
        return ""

    sim_table = f"""
  <h2>시뮬 타점 (전기간 {len(sim_fires)}건 → 복리 체결 {len(compound_fires)}건)</h2>
  <p class="meta">총자산×GT비중×leg tier·보유현금 한도·전기간 복리(일한도 없음).
  가격 열 (+/-) = <b>{label_mode}</b> 구간 수익%.{_mark_note(close_val)}</p>
  <div class="table-scroll">
  <table>
    <thead><tr><th>시각</th><th>구분</th><th>규칙</th><th>유형</th><th>가격</th>
    <th>총 평가금액</th><th>승/패</th><th>비고</th></tr></thead>
    <tbody>{_sim_fire_table_rows(compound_fires, rules_by_id, sim_steps)}</tbody>
  </table>
  </div>"""

    gt_table = f"""
  <h2>정답 타점 (ground_truth)</h2>
  <p class="meta">삼각형 크기 = GT 체결 금액. 매수 분할·매도 leg 반영.{_mark_note(close_val)}</p>
  <div class="table-scroll">
  <table>
    <thead><tr><th>시각</th><th>구분</th><th>비중</th><th>가격</th><th>총 평가금액</th><th>해석</th></tr></thead>
    <tbody>{_gt_table_rows(gt_trades, gt_steps)}</tbody>
  </table>
  </div>"""

    sections = f"""
  {go_table}
  <h2>매수·매도 판단 기준 (monitor_rules)</h2>
  <p class="meta">04 GT 프로필 + 전구간 EV 선별. 조건 <b>모두</b> 충족 시 3분봉 종가에 신호.</p>
  {criteria_blocks}
  {sim_table}
  {gt_table}
"""

    note = (
        f"1단계 시뮬 · holdout {report.get('holdout_ratio', 0.15)} · "
        f"전기간 발화 {len(sim_fires)}건 / holdout {len(holdout_fires)}건. "
        "상단 카드: 초기 금액·총보유자산·초기 대비 증감율·수수료."
    )
    legend = (
        "▲ <b>정답 매수</b> · ▼ <b>정답 매도</b> — 삼각형 = GT 체결 금액.<br>"
        "● <b>시뮬</b> — holdout 발화 (차트). 테이블 = 전기간 GT tier 복리 체결."
    )
    if frames is not None:
        meta_line = (
            f"추세(참고): {trend} | 기간: {df_chart.index[0]} ~ {df_chart.index[-1]} "
            f"| 봉 {len(df_chart)}"
        )
    else:
        meta_line = (
            f"추세·{SYMBOL} | lookback {CHART_LOOKBACK_DAYS}일 | "
            f"초기 ₩{GT_INITIAL_CASH_KRW:,.0f}"
        )

    cards = _summary_cards_html(
        close_val,
        bb_txt,
        gt_trades,
        gt_pnl,
        sim_sized_pnl,
        sim_fixed_pnl,
        len(compound_fires),
        go_flag,
        model_note=model_note,
        sim_causal_gt_pnl=pc.get("sim_causal_gt"),
        sim_causal_hybrid_pnl=pc.get("sim_causal_hybrid"),
        sim_tier_enhanced_pnl=pc.get("sim_tier_enhanced"),
        sim_primary_pnl=pc.get("sim_primary"),
        primary_sizing=str(pc.get("primary_sizing") or ""),
        hybrid_go=hybrid_go_flag,
        phase2_go=phase2_go_flag,
    )

    if frames is not None:
        return build_chart_html(
            df_chart,
            trend,
            note=note,
            truth_trades=gt_trades,
            sim_trades=_fires_to_chart_trades(holdout_fires),
            # 차트 마커는 holdout; 카드·테이블은 전기간 GT tier 복리
            title_suffix="1단계 시뮬레이션 (monitor · holdout)",
            legend_html=legend,
            footer_sections=sections,
            cards_html=cards,
        )

    return wrap_chart_report_page(
        page_title=f"{SYMBOL} 시뮬레이션",
        heading=f"{COIN_NAME} ({SYMBOL}) 1단계 시뮬레이션",
        meta_line=meta_line,
        note_html=f"<p class='note'>{note}</p>",
        legend_html=legend,
        cards_html=cards,
        chart_html=(
            "<p class='note'>차트 데이터 없음 — "
            "<code>python scripts/01_download.py</code> 후 재생성.</p>"
        ),
        sections_html=sections,
    )


def write_simulation_report_html(
    report: dict[str, Any],
    out_path: Path,
    outcomes_path: Path | None = None,
    matched_path: Path | None = None,
) -> Path:
    """
    simulation_report.html 저장 (ground_truth 동일 스타일).

    Args:
        report: build_simulation_report 결과.
        out_path: HTML 경로.
        outcomes_path: fire_outcomes.csv.
        matched_path: matched_rules.json.

    Returns:
        out_path.
    """
    html = build_simulation_page_html(report, outcomes_path, matched_path)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(html, encoding="utf-8")
    return out_path
