"""돌파 매수 타점 (Ground Truth 보조, 사후 최적)."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol

import pandas as pd

from deepcoin.ground_truth.zigzag import Pivot


class _LegLike(Protocol):
    """레그 최소 인터페이스."""

    leg_id: int
    buy_bar_index: int
    sell_bar_index: int
    sell_price: float


@dataclass(frozen=True)
class BreakoutBuy:
    """돌파 매수 타점."""

    bar_index: int
    price: float
    datetime: pd.Timestamp
    leg_id: int
    resistance_price: float


def find_breakout_buy_pivots(
    df: pd.DataFrame,
    legs: list[_LegLike],
    pullback_buys: list[Pivot],
    breakout_buffer_pct: float = 0.1,
    min_bars_after_anchor: int = 10,
    consolidation_bars: int = 200,
    leg_end_margin_bars: int = 80,
    min_rally_to_sell_pct: float = 2.0,
) -> list[BreakoutBuy]:
    """각 레그에서 횡보·눌림 후 저항 돌파 시점을 찾는다 (1단계 GT, 미래 허용).

    눌림목 이후 구간 고점을 저항으로 보고, 종가가 저항을 돌파한
    첫 봉을 레그당 돌파 매수 1개로 표시한다.

    Args:
        df: OHLCV DataFrame.
        legs: ZigZag 스윙 레그.
        pullback_buys: 눌림목 Pivot (레그별 매칭).
        breakout_buffer_pct: 저항 대비 돌파 확인 버퍼(%).
        min_bars_after_anchor: 눌림목/구간 시작 후 최소 대기 봉.
        consolidation_bars: 횡보 저항 산정 구간(봉).
        leg_end_margin_bars: 매도 직전 제외 봉.
        min_rally_to_sell_pct: 돌파 후 레그 고점까지 최소 추가 상승(%).

    Returns:
        BreakoutBuy 리스트.
    """
    if not legs or len(df) < 20:
        return []

    highs = df["high"].astype(float).values
    closes = df["close"].astype(float).values
    pullback_by_leg = _map_pullback_to_legs(legs, pullback_buys)
    breakouts: list[BreakoutBuy] = []
    buffer = breakout_buffer_pct / 100.0

    for leg in legs:
        anchor = pullback_by_leg.get(leg.leg_id)
        if anchor is not None:
            zone_start = anchor.bar_index
        else:
            leg_len = leg.sell_bar_index - leg.buy_bar_index
            zone_start = max(
                leg.buy_bar_index + 120,
                leg.sell_bar_index - int(leg_len * 0.35),
            )

        zone_end = min(
            zone_start + consolidation_bars,
            leg.sell_bar_index - leg_end_margin_bars - 5,
        )
        search_start = zone_start + min_bars_after_anchor
        search_end = leg.sell_bar_index - leg_end_margin_bars
        if zone_end <= zone_start or search_end <= search_start + 5:
            continue

        resistance = float(highs[zone_start:zone_end].max())
        if resistance <= 0:
            continue

        breakout = _first_breakout_above(
            df=df,
            highs=highs,
            closes=closes,
            search_start=max(search_start, zone_end),
            search_end=search_end,
            resistance=resistance,
            buffer=buffer,
            sell_price=float(leg.sell_price),
            min_rally_pct=min_rally_to_sell_pct,
        )
        if breakout is None:
            continue

        bar_idx, price, resistance = breakout
        breakouts.append(
            BreakoutBuy(
                bar_index=bar_idx,
                price=round(price, 2),
                datetime=pd.Timestamp(df.iloc[bar_idx]["datetime"]),
                leg_id=leg.leg_id,
                resistance_price=round(resistance, 2),
            )
        )

    return breakouts


def _map_pullback_to_legs(legs: list[_LegLike], pullback_buys: list[Pivot]) -> dict[int, Pivot]:
    """눌림목을 소속 레그에 매핑한다."""
    mapping: dict[int, Pivot] = {}
    for pivot in pullback_buys:
        for leg in legs:
            if leg.buy_bar_index < pivot.bar_index < leg.sell_bar_index:
                prev = mapping.get(leg.leg_id)
                if prev is None or pivot.bar_index > prev.bar_index:
                    mapping[leg.leg_id] = pivot
    return mapping


def _first_breakout_above(
    df: pd.DataFrame,
    highs,
    closes,
    search_start: int,
    search_end: int,
    resistance: float,
    buffer: float,
    sell_price: float,
    min_rally_pct: float,
) -> tuple[int, float, float] | None:
    """고정 저항선 상향 돌파 첫 봉을 반환한다."""
    threshold = resistance * (1 + buffer)

    for i in range(search_start, search_end):
        close_val = float(closes[i])
        if close_val <= threshold:
            continue

        future_max = float(highs[i : search_end + 1].max())
        rally_pct = (future_max - close_val) / close_val * 100.0
        sell_rally_pct = (sell_price - close_val) / close_val * 100.0

        if rally_pct < min_rally_pct or sell_rally_pct < min_rally_pct:
            continue

        return i, close_val, resistance

    return None
