"""독립 매수·매도 스윙 타점 탐지 (레그 1:1 강제 없음)."""

from __future__ import annotations

from dataclasses import dataclass

import pandas as pd

from deepcoin.ground_truth.zigzag import Pivot, find_zigzag_pivots


@dataclass(frozen=True)
class SwingSignal:
    """매수 또는 매도 타점."""

    signal_id: int
    side: str  # buy | sell
    bar_index: int
    price: float
    datetime: pd.Timestamp
    source: str  # minor_zigzag | local_extrema
    swing_pct: float


def find_swing_signals(
    df: pd.DataFrame,
    minor_reversal_pct: float = 2.5,
    local_order: int = 20,
    min_swing_pct: float = 2.0,
    min_bars_between: int = 30,
) -> tuple[list[SwingSignal], list[SwingSignal]]:
    """적절한 매수·매도 타점을 독립적으로 찾는다.

    - 소형 ZigZag(되돌림 2.5% 등): 눌림목·반등 고점
    - 국소 극값: 횡보 후 돌파 전 저점(빨간 원 구간 유형) 보완
    - 매수·매도를 1:1 레그로 묶지 않음

    Args:
        df: OHLCV DataFrame.
        minor_reversal_pct: 소형 ZigZag 되돌림 %.
        local_order: 국소 극값 탐지 반경(봉).
        min_swing_pct: 국소 극값 인정 최소 변동 %.
        min_bars_between: 동일 방향 신호 최소 간격(봉).

    Returns:
        (매수 신호 리스트, 매도 신호 리스트) — 각각 시간순.
    """
    minor_pivots = find_zigzag_pivots(df, reversal_pct=minor_reversal_pct)
    local_lows = _find_local_lows(df, order=local_order, min_swing_pct=min_swing_pct)
    local_highs = _find_local_highs(df, order=local_order, min_swing_pct=min_swing_pct)

    buy_candidates = [p for p in minor_pivots if p.side == "low"]
    sell_candidates = [p for p in minor_pivots if p.side == "high"]

    buy_candidates.extend(local_lows)
    sell_candidates.extend(local_highs)

    buys = _to_signals(_dedupe_pivots(buy_candidates, min_bars_between, prefer="low"), "buy")
    sells = _to_signals(_dedupe_pivots(sell_candidates, min_bars_between, prefer="high"), "sell")

    return buys, sells


def _find_local_lows(
    df: pd.DataFrame,
    order: int,
    min_swing_pct: float,
) -> list[Pivot]:
    """최근 고점 대비 되돌림 후 형성된 국소 저점."""
    pivots: list[Pivot] = []
    lookback = order * 4

    for i in range(order, len(df) - order):
        low_val = float(df.iloc[i]["low"])
        window = df["low"].iloc[i - order : i + order + 1]
        if low_val > window.min():
            continue

        start = max(0, i - lookback)
        recent_high = float(df["high"].iloc[start : i + 1].max())
        if recent_high <= 0:
            continue
        drop_pct = (recent_high - low_val) / recent_high * 100.0
        if drop_pct < min_swing_pct:
            continue

        pivots.append(
            Pivot(
                bar_index=i,
                side="low",
                price=low_val,
                datetime=pd.Timestamp(df.iloc[i]["datetime"]),
            )
        )
    return pivots


def _find_local_highs(
    df: pd.DataFrame,
    order: int,
    min_swing_pct: float,
) -> list[Pivot]:
    """최근 저점 대비 반등 후 형성된 국소 고점."""
    pivots: list[Pivot] = []
    lookback = order * 4

    for i in range(order, len(df) - order):
        high_val = float(df.iloc[i]["high"])
        window = df["high"].iloc[i - order : i + order + 1]
        if high_val < window.max():
            continue

        start = max(0, i - lookback)
        recent_low = float(df["low"].iloc[start : i + 1].min())
        if recent_low <= 0:
            continue
        rise_pct = (high_val - recent_low) / recent_low * 100.0
        if rise_pct < min_swing_pct:
            continue

        pivots.append(
            Pivot(
                bar_index=i,
                side="high",
                price=high_val,
                datetime=pd.Timestamp(df.iloc[i]["datetime"]),
            )
        )
    return pivots


def _dedupe_pivots(
    pivots: list[Pivot],
    min_bars: int,
    prefer: str,
) -> list[Pivot]:
    """가까운 동일 방향 피벗을 병합한다."""
    if not pivots:
        return []

    sorted_pivots = sorted(pivots, key=lambda p: p.bar_index)
    merged: list[Pivot] = [sorted_pivots[0]]

    for pivot in sorted_pivots[1:]:
        last = merged[-1]
        if pivot.bar_index - last.bar_index < min_bars:
            if prefer == "low" and pivot.price < last.price:
                merged[-1] = pivot
            elif prefer == "high" and pivot.price > last.price:
                merged[-1] = pivot
        else:
            merged.append(pivot)

    return merged


def _to_signals(pivots: list[Pivot], side: str) -> list[SwingSignal]:
    """Pivot 리스트를 SwingSignal로 변환한다."""
    source = "local_extrema"
    signals: list[SwingSignal] = []
    for idx, pivot in enumerate(pivots, start=1):
        signals.append(
            SwingSignal(
                signal_id=idx,
                side=side,
                bar_index=pivot.bar_index,
                price=round(pivot.price, 2),
                datetime=pivot.datetime,
                source=source,
                swing_pct=0.0,
            )
        )
    return signals
