"""Ground Truth와 기법 신호·레그 정합 평가."""

from __future__ import annotations

from typing import Any


def _bar_distance(a: int, b: int) -> int:
    """두 봉 인덱스 간 절대 거리."""
    return abs(a - b)


def _match_signal(
    gt_bar: int,
    candidates: list[dict[str, Any]],
    tolerance_bars: int,
    used: set[int],
) -> dict[str, Any] | None:
    """GT 신호에 가장 가까운 기법 신호를 찾는다."""
    best: dict[str, Any] | None = None
    best_dist = tolerance_bars + 1
    best_idx = -1

    for idx, candidate in enumerate(candidates):
        if idx in used:
            continue
        pivot_bar = candidate.get("pivot_bar_index")
        compare_bar = pivot_bar if pivot_bar is not None else candidate["bar_index"]
        dist = _bar_distance(gt_bar, compare_bar)
        if dist <= tolerance_bars and dist < best_dist:
            best = candidate
            best_dist = dist
            best_idx = idx

    if best is None:
        return None

    return {
        "matched": True,
        "gt_bar_index": gt_bar,
        "tech_bar_index": best.get("pivot_bar_index") or best["bar_index"],
        "signal_bar_index": best["bar_index"],
        "bar_offset": best_dist,
        "tech_price": best["price"],
        "tech_datetime": best["datetime"],
        "candidate_index": best_idx,
    }


def align_signals(
    gt_signals: list[dict[str, Any]],
    tech_signals: list[dict[str, Any]],
    tolerance_bars: int,
    side: str,
) -> dict[str, Any]:
    """GT 신호와 기법 신호의 정합률을 계산한다.

    Args:
        gt_signals: GT signals 리스트.
        tech_signals: 기법 신호 dict 리스트.
        tolerance_bars: 허용 봉 오차.
        side: buy | sell.

    Returns:
        정합 메트릭 dict.
    """
    gt_filtered = [s for s in gt_signals if s["side"] == side]
    tech_filtered = [s for s in tech_signals if s["side"] == side]

    used: set[int] = set()
    hits: list[dict[str, Any]] = []
    misses: list[dict[str, Any]] = []

    for gt_sig in gt_filtered:
        match = _match_signal(gt_sig["bar_index"], tech_filtered, tolerance_bars, used)
        if match:
            used.add(match["candidate_index"])
            hits.append({**match, "gt_datetime": gt_sig["datetime"], "gt_price": gt_sig["price"]})
        else:
            misses.append(
                {
                    "gt_bar_index": gt_sig["bar_index"],
                    "gt_datetime": gt_sig["datetime"],
                    "gt_price": gt_sig["price"],
                }
            )

    gt_count = len(gt_filtered)
    hit_count = len(hits)
    recall = hit_count / gt_count if gt_count else 0.0
    precision = hit_count / len(tech_filtered) if tech_filtered else 0.0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
    avg_offset = sum(h["bar_offset"] for h in hits) / hit_count if hit_count else 0.0

    return {
        "side": side,
        "gt_count": gt_count,
        "tech_count": len(tech_filtered),
        "hit_count": hit_count,
        "miss_count": len(misses),
        "recall": round(recall, 4),
        "precision": round(precision, 4),
        "f1": round(f1, 4),
        "avg_bar_offset": round(avg_offset, 1),
        "hits": hits,
        "misses": misses,
    }


def align_legs(
    gt_legs: list[dict[str, Any]],
    tech_legs: list[dict[str, Any]],
    tolerance_bars: int,
) -> dict[str, Any]:
    """GT 레그와 기법 레그의 정합률을 계산한다."""
    captured: list[dict[str, Any]] = []
    missed: list[dict[str, Any]] = []
    used_tech: set[int] = set()

    for gt_leg in gt_legs:
        best_leg: dict[str, Any] | None = None
        best_score = tolerance_bars * 2 + 1
        best_id = -1

        for tech_leg in tech_legs:
            tid = int(tech_leg["leg_id"])
            if tid in used_tech:
                continue
            buy_dist = _bar_distance(gt_leg["buy_bar_index"], tech_leg["buy_bar_index"])
            sell_dist = _bar_distance(gt_leg["sell_bar_index"], tech_leg["sell_bar_index"])
            score = buy_dist + sell_dist
            if buy_dist <= tolerance_bars and sell_dist <= tolerance_bars and score < best_score:
                best_leg = tech_leg
                best_score = score
                best_id = tid

        if best_leg:
            used_tech.add(best_id)
            captured.append(
                {
                    "gt_leg_id": gt_leg["leg_id"],
                    "tech_leg_id": best_leg["leg_id"],
                    "gt_buy": gt_leg["buy_datetime"],
                    "tech_buy": best_leg["buy_datetime"],
                    "gt_sell": gt_leg["sell_datetime"],
                    "tech_sell": best_leg["sell_datetime"],
                    "buy_bar_offset": _bar_distance(gt_leg["buy_bar_index"], best_leg["buy_bar_index"]),
                    "sell_bar_offset": _bar_distance(gt_leg["sell_bar_index"], best_leg["sell_bar_index"]),
                    "gt_leg_pct": gt_leg["leg_pct"],
                    "tech_leg_pct": best_leg["leg_pct"],
                }
            )
        else:
            missed.append(
                {
                    "gt_leg_id": gt_leg["leg_id"],
                    "buy_datetime": gt_leg["buy_datetime"],
                    "sell_datetime": gt_leg["sell_datetime"],
                    "leg_pct": gt_leg["leg_pct"],
                }
            )

    gt_count = len(gt_legs)
    recall = len(captured) / gt_count if gt_count else 0.0

    return {
        "gt_leg_count": gt_count,
        "tech_leg_count": len(tech_legs),
        "captured_count": len(captured),
        "missed_count": len(missed),
        "leg_recall": round(recall, 4),
        "captured": captured,
        "missed": missed,
    }


def align_with_ground_truth(
    gt_result: dict[str, Any],
    technique_signals: list[dict[str, Any]],
    technique_legs: list[dict[str, Any]],
    tolerance_bars: int,
) -> dict[str, Any]:
    """GT 대비 기법 정합 결과 전체를 반환한다."""
    gt_signals = gt_result.get("signals", [])
    gt_legs = gt_result.get("legs", [])
    gt_pnl = gt_result.get("pnl", {})

    buy_align = align_signals(gt_signals, technique_signals, tolerance_bars, "buy")
    sell_align = align_signals(gt_signals, technique_signals, tolerance_bars, "sell")
    leg_align = align_legs(gt_legs, technique_legs, tolerance_bars)

    tech_return = 0.0
    if technique_legs:
        from deepcoin.ground_truth.pnl import simulate_gt_pnl

        tech_pnl = simulate_gt_pnl(
            technique_legs,
            initial_cash_krw=gt_pnl.get("initial_cash_krw", 400_000),
            fee_rate=gt_pnl.get("fee_rate", 0.0005),
        )
        tech_return = tech_pnl["total_return_pct"]

    gt_return = gt_pnl.get("total_return_pct", 0.0)
    return_capture = tech_return / gt_return if gt_return else 0.0

    return {
        "tolerance_bars": tolerance_bars,
        "buy": buy_align,
        "sell": sell_align,
        "legs": leg_align,
        "gt_return_pct": gt_return,
        "tech_return_pct": tech_return,
        "return_capture_ratio": round(return_capture, 4),
        "score": round(
            (
                buy_align["recall"] * 0.25
                + sell_align["recall"] * 0.25
                + leg_align["leg_recall"] * 0.35
                + min(return_capture, 1.0) * 0.15
            ),
            4,
        ),
    }
