"""
Ground truth(450타점) vs 규칙 발화·시뮬 결과 비교 리포트.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import numpy as np
import pandas as pd

from config import MATCH_GT_TOLERANCE_MIN
from deepcoin.ground_truth.ground_truth import load_ground_truth
from deepcoin.matching.select_rules import (
    _rule_metrics,
    _split_train_valid_holdout,
    gt_overlap_report,
)
from deepcoin.paths import (
    MATCHING_FIRE_OUTCOMES,
    MATCHING_GT_COMPARISON_HTML,
    MATCHING_GT_COMPARISON_JSON,
    MATCHING_MATCHED_RULES,
    MATCHING_SIMULATION_JSON,
    resolve_ground_truth_file,
)


def _precision_near_gt(
    fire_ts: pd.Series,
    gt_ts: pd.Series,
    tolerance: pd.Timedelta,
) -> dict[str, Any]:
    """
    발화 시각이 GT 타점 ±허용 내인 비율(precision proxy).

    Args:
        fire_ts: 규칙 발화 시각.
        gt_ts: GT 시각.
        tolerance: 허용 timedelta.

    Returns:
        near_count, fire_count, precision.
    """
    if fire_ts.empty:
        return {"near_count": 0, "fire_count": 0, "precision": 0.0}
    gt_sorted = gt_ts.sort_values()
    near = 0
    for fts in fire_ts:
        if (gt_sorted - fts).abs().min() <= tolerance:
            near += 1
    n = len(fire_ts)
    return {
        "near_count": near,
        "fire_count": n,
        "precision": round(near / n, 4) if n else 0.0,
    }


def _matched_pairs(
    fires: pd.DataFrame,
    gt_df: pd.DataFrame,
    rule_id: str,
    tolerance: pd.Timedelta,
) -> pd.DataFrame:
    """
    GT 타점별 가장 가까운 동일 rule·side 발화와 수익률 쌍을 만듭니다.

    Args:
        fires: fire_outcomes.
        gt_df: GT trades DataFrame.
        rule_id: 규칙 ID.
        tolerance: 매칭 허용.

    Returns:
        매칭된 행 DataFrame.
    """
    sub = fires[fires["rule_id"] == rule_id].copy()
    if sub.empty:
        return pd.DataFrame()
    side = sub["side"].iloc[0]
    g = gt_df[gt_df["action"] == side].copy()
    g["ts"] = pd.to_datetime(g["dt"])
    sub["ts"] = pd.to_datetime(sub["dt"])
    rows: list[dict[str, Any]] = []
    for _, gt_row in g.iterrows():
        gts = pd.Timestamp(gt_row["ts"])
        delta = (sub["ts"] - gts).abs()
        if delta.empty or delta.min() > tolerance:
            continue
        idx = delta.idxmin()
        fr = sub.loc[idx]
        rows.append(
            {
                "side": side,
                "rule_id": rule_id,
                "gt_dt": str(gt_row["dt"]),
                "fire_dt": str(fr["dt"]),
                "delta_min": round(delta.min().total_seconds() / 60, 2),
                "gt_forward_pct": float(gt_row.get("forward_return_pct") or 0),
                "sim_leg_gt_pct": float(fr["forward_ret_pct"]),
                "split": fr.get("split"),
            }
        )
    return pd.DataFrame(rows)


def build_gt_comparison_report(
    outcomes_path: Path | None = None,
    matched_path: Path | None = None,
    gt_path: Path | None = None,
    sim_path: Path | None = None,
    tolerance_min: int = MATCH_GT_TOLERANCE_MIN,
) -> dict[str, Any]:
    """
    GT vs 발화·시뮬 비교 dict 생성.

    Args:
        outcomes_path: fire_outcomes.csv.
        matched_path: matched_rules.json.
        gt_path: ground_truth_trades.json.
        sim_path: simulation_report.json.
        tolerance_min: GT 매칭 허용(분).

    Returns:
        gt_comparison_report dict.
    """
    op = outcomes_path or MATCHING_FIRE_OUTCOMES
    mp = matched_path or MATCHING_MATCHED_RULES
    if not op.is_file():
        raise FileNotFoundError(f"fire_outcomes 없음: {op}")

    outcomes = pd.read_csv(op)
    outcomes["ts"] = pd.to_datetime(outcomes["dt"])
    outcomes["split"] = _split_train_valid_holdout(outcomes)
    matched: dict[str, Any] = {}
    if mp.is_file():
        matched = json.loads(mp.read_text(encoding="utf-8"))

    sim_report: dict[str, Any] = {}
    sp = sim_path or MATCHING_SIMULATION_JSON
    if sp.is_file():
        sim_report = json.loads(sp.read_text(encoding="utf-8"))

    gt_data = load_ground_truth(gt_path or resolve_ground_truth_file()) or {}
    gt_trades = gt_data.get("trades") or []
    gt_df = pd.DataFrame(gt_trades)
    tol = pd.Timedelta(minutes=tolerance_min)

    gt_baseline: dict[str, Any] = {
        "total": len(gt_df),
        "buy": int((gt_df["action"] == "buy").sum()) if not gt_df.empty else 0,
        "sell": int((gt_df["action"] == "sell").sum()) if not gt_df.empty else 0,
    }
    for side in ("buy", "sell"):
        sub = gt_df[gt_df["action"] == side] if not gt_df.empty else pd.DataFrame()
        if sub.empty or "forward_return_pct" not in sub.columns:
            gt_baseline[side] = {}
            continue
        r = sub["forward_return_pct"].astype(float)
        gt_baseline[side] = {
            "mean_forward_pct": round(float(r.mean()), 4),
            "median_forward_pct": round(float(r.median()), 4),
            "win_rate": round(float((r > 0).mean()), 4),
            "count": int(len(r)),
        }

    all_fires = outcomes.copy()
    if "rule_id" not in all_fires.columns:
        all_fires["rule_id"] = "all"
    overlap_all = gt_overlap_report(
        all_fires.drop_duplicates(subset=["dt", "side"]),
        gt_trades,
        tolerance_min=tolerance_min,
    )

    per_rule: list[dict[str, Any]] = []
    pair_stats: list[dict[str, Any]] = []
    for rid in sorted(outcomes["rule_id"].unique()):
        sub = outcomes[outcomes["rule_id"] == rid]
        side = str(sub["side"].iloc[0])
        gt_side = gt_df[gt_df["action"] == side]
        gt_ts = pd.to_datetime(gt_side["dt"]) if not gt_side.empty else pd.Series(dtype="datetime64[ns]")
        fire_ts = sub["ts"]
        ov = gt_overlap_report(sub, gt_trades, tolerance_min=tolerance_min)
        prec = _precision_near_gt(fire_ts, gt_ts, tol)
        m_all = _rule_metrics(sub)
        m_hold = _rule_metrics(sub[sub["split"] == "holdout"])

        pairs = _matched_pairs(outcomes, gt_df, rid, tol)
        pair_row: dict[str, Any] = {"rule_id": rid, "side": side, "pair_count": len(pairs)}
        if len(pairs) >= 2:
            corr = pairs["gt_forward_pct"].corr(pairs["sim_leg_gt_pct"])
            pair_row["corr_gt_vs_sim"] = round(float(corr), 4) if pd.notna(corr) else None
            pair_row["mean_abs_diff_pct"] = round(
                float((pairs["gt_forward_pct"] - pairs["sim_leg_gt_pct"]).abs().mean()),
                4,
            )
            pair_row["mean_delta_min"] = round(float(pairs["delta_min"].mean()), 2)
        pair_stats.append(pair_row)

        near_mask = []
        for fts in fire_ts:
            near_mask.append(
                not gt_ts.empty and (gt_ts - fts).abs().min() <= tol
            )
        sub_near = sub.loc[near_mask] if near_mask else sub.iloc[0:0]
        sub_far = sub.loc[[not x for x in near_mask]] if near_mask else sub

        per_rule.append(
            {
                "rule_id": rid,
                "side": side,
                "fire_count": int(len(sub)),
                "gt_recall": ov.get(side, {}).get("recall", 0),
                "gt_matched": ov.get(side, {}).get("matched", 0),
                "gt_count": ov.get(side, {}).get("gt_count", 0),
                "precision_near_gt": prec["precision"],
                "fires_near_gt": prec["near_count"],
                "sim_ev_all_pct": m_all.get("ev_pct"),
                "sim_ev_near_gt_pct": _rule_metrics(sub_near).get("ev_pct") if len(sub_near) else None,
                "sim_ev_far_gt_pct": _rule_metrics(sub_far).get("ev_pct") if len(sub_far) else None,
                "sim_win_rate": m_all.get("win_rate"),
                "sim_profit_factor": m_all.get("profit_factor"),
                "holdout_ev_pct": m_hold.get("ev_pct"),
                "holdout_count": m_hold.get("count"),
            }
        )

    monitor_ids = [r["rule_id"] for r in matched.get("monitor_rules", [])]
    monitor_summary = [r for r in per_rule if r["rule_id"] in monitor_ids]

    go = sim_report.get("go_no_go", {})

    return {
        "tolerance_min": tolerance_min,
        "label_mode": matched.get("label_mode"),
        "gt_baseline": gt_baseline,
        "gt_overlap_all_fires_dedup": overlap_all,
        "gt_overlap_matched_json": matched.get("gt_overlap"),
        "per_rule": per_rule,
        "pair_alignment": pair_stats,
        "monitor_rules": monitor_summary,
        "simulation_go_no_go": {
            "go": go.get("go"),
            "checks": go.get("checks", []),
            "live_cap_taken_ratio": go.get("live_cap_taken_ratio"),
        },
        "notes": [
            "gt_overlap_matched_json: 04 선별 시 전 규칙 발화 합산(중복 dt 제거 전) 기준.",
            "per_rule.gt_recall: 해당 규칙 발화만으로 GT 타점 커버.",
            "precision_near_gt: 발화 중 GT±tolerance 내 비율(낮을수록 잡음 많음).",
            "gt_forward_pct vs sim_leg_gt_pct: leg_gt 라벨과 GT JSON forward_return_pct 정의 차이 가능.",
        ],
    }


def write_gt_comparison_html(report: dict[str, Any], out_path: Path) -> Path:
    """
    gt_comparison_report.html 저장.

    Args:
        report: build_gt_comparison_report 결과.
        out_path: HTML 경로.

    Returns:
        out_path.
    """
    def _rows(items: list[dict], cols: list[str]) -> str:
        lines = []
        for it in items:
            cells = "".join(f"<td>{it.get(c, '')}</td>" for c in cols)
            lines.append(f"<tr>{cells}</tr>")
        return "\n".join(lines)

    pr_cols = [
        "rule_id", "side", "fire_count", "gt_recall", "precision_near_gt",
        "sim_ev_all_pct", "sim_ev_near_gt_pct", "sim_ev_far_gt_pct", "holdout_ev_pct",
    ]
    go = report.get("simulation_go_no_go", {})
    go_flag = "GO" if go.get("go") else "NO-GO"
    gb = report.get("gt_baseline", {})
    html = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8"/>
<title>GT vs Simulation Comparison</title>
<style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; max-width: 1100px; }}
table {{ border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 0.9rem; }}
th, td {{ border: 1px solid #ccc; padding: 6px 8px; text-align: right; }}
th {{ background: #e2e8f0; text-align: center; }}
td:first-child, th:first-child {{ text-align: left; }}
h2 {{ margin-top: 28px; }}
.warn {{ color: #b45309; }}
</style></head><body>
<h1>Ground Truth vs 규칙·시뮬 비교</h1>
<p>허용 오차: ±{report.get('tolerance_min')}분 · 라벨: {report.get('label_mode')}</p>
<p><strong>시뮬 Go/No-Go: {go_flag}</strong></p>

<h2>GT 기준선 (forward_return_pct)</h2>
<p>총 {gb.get('total')}건 (매수 {gb.get('buy')} / 매도 {gb.get('sell')})</p>
<table>
<thead><tr><th>구분</th><th>건수</th><th>평균 forward%</th><th>중앙값</th><th>승률</th></tr></thead>
<tbody>
<tr><td>매수 GT</td><td>{gb.get('buy', {}).get('count', '')}</td>
<td>{gb.get('buy', {}).get('mean_forward_pct', '')}</td>
<td>{gb.get('buy', {}).get('median_forward_pct', '')}</td>
<td>{gb.get('buy', {}).get('win_rate', '')}</td></tr>
<tr><td>매도 GT</td><td>{gb.get('sell', {}).get('count', '')}</td>
<td>{gb.get('sell', {}).get('mean_forward_pct', '')}</td>
<td>{gb.get('sell', {}).get('median_forward_pct', '')}</td>
<td>{gb.get('sell', {}).get('win_rate', '')}</td></tr>
</tbody></table>

<h2>규칙별 GT recall / precision / EV</h2>
<table>
<thead><tr>{''.join(f'<th>{c}</th>' for c in pr_cols)}</tr></thead>
<tbody>{_rows(report.get('per_rule', []), pr_cols)}</tbody>
</table>

<h2>monitor_rules (실감시·시뮬 대상)</h2>
<table>
<thead><tr>{''.join(f'<th>{c}</th>' for c in pr_cols)}</tr></thead>
<tbody>{_rows(report.get('monitor_rules', []), pr_cols)}</tbody>
</table>

<h2>GT–발화 수익률 정렬 (±{report.get('tolerance_min')}분)</h2>
<table>
<thead><tr><th>rule</th><th>side</th><th>pairs</th><th>corr</th><th>mean|diff|%</th><th>mean Δmin</th></tr></thead>
<tbody>
{''.join(
    f"<tr><td>{p['rule_id']}</td><td>{p['side']}</td><td>{p['pair_count']}</td>"
    f"<td>{p.get('corr_gt_vs_sim','')}</td><td>{p.get('mean_abs_diff_pct','')}</td>"
    f"<td>{p.get('mean_delta_min','')}</td></tr>"
    for p in report.get('pair_alignment', [])
)}
</tbody></table>

<h2>시뮬 검증 (monitor)</h2>
<pre>{json.dumps(go, ensure_ascii=False, indent=2)}</pre>

<h2>참고</h2>
<ul>
{''.join(f'<li>{n}</li>' for n in report.get('notes', []))}
</ul>
</body></html>"""
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(html, encoding="utf-8")
    return out_path


def run_gt_comparison_report(
    outcomes_path: Path | None = None,
    matched_path: Path | None = None,
) -> dict[str, Any]:
    """
    GT 비교 리포트 생성·저장.

    Args:
        outcomes_path: fire_outcomes.csv.
        matched_path: matched_rules.json.

    Returns:
        report dict.
    """
    report = build_gt_comparison_report(outcomes_path, matched_path)
    MATCHING_GT_COMPARISON_JSON.parent.mkdir(parents=True, exist_ok=True)
    MATCHING_GT_COMPARISON_JSON.write_text(
        json.dumps(report, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    write_gt_comparison_html(report, MATCHING_GT_COMPARISON_HTML)
    print(f"[GT비교] 저장: {MATCHING_GT_COMPARISON_JSON}")
    print(f"[GT비교] 저장: {MATCHING_GT_COMPARISON_HTML}")
    for m in report.get("monitor_rules", []):
        print(
            f"  {m['rule_id']}: recall={m['gt_recall']:.1%} prec={m['precision_near_gt']:.1%} "
            f"fires={m['fire_count']} EV={m['sim_ev_all_pct']}% holdout={m['holdout_ev_pct']}%"
        )
    go = report.get("simulation_go_no_go", {})
    print(f"[GT비교] 시뮬 연동: {'GO' if go.get('go') else 'NO-GO'}")
    return report
