"""국소 극값 기반 매수·매도 (눌림목·반등 고점)."""

from __future__ import annotations

import pandas as pd

from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueSignal


class LocalExtremaTechnique(BaseTechnique):
    """좌우 N봉 대비 국소 저점·고점에서 신호 (인과: 확정 지연 order봉)."""

    technique_id = "local_extrema"
    technique_name = "국소 극값"
    category = "swing"
    causal = True
    description = "국소 저점 매수·고점 매도 (눌림목 유형 포착, 9/27 구간 등)"

    def default_extra_params(self) -> dict:
        return {"order": 20, "min_swing_pct": 2.0, "min_bars_between": 30}

    def generate_signals(self, df: pd.DataFrame, params: TechniqueParams) -> list[TechniqueSignal]:
        order = int(params.extra.get("order", 20))
        min_swing_pct = float(params.extra.get("min_swing_pct", 2.0))
        min_bars = int(params.extra.get("min_bars_between", 30))
        buys = _find_local_low_signals(df, order=order, min_swing_pct=min_swing_pct)
        sells = _find_local_high_signals(df, order=order, min_swing_pct=min_swing_pct)
        return _dedupe_signals(buys + sells, min_bars=min_bars)


def _find_local_low_signals(
    df: pd.DataFrame,
    order: int,
    min_swing_pct: float,
) -> list[TechniqueSignal]:
    """국소 저점 매수 신호 (order봉 후 확정)."""
    signals: list[TechniqueSignal] = []
    lookback = order * 4

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

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

        confirm_idx = pivot_idx + order
        signals.append(
            TechniqueSignal(
                side="buy",
                bar_index=confirm_idx,
                price=round(low_val, 2),
                datetime=pd.Timestamp(df.iloc[confirm_idx]["datetime"]).strftime("%Y-%m-%d %H:%M:%S"),
                pivot_bar_index=pivot_idx,
                confidence=min(1.0, drop_pct / 10.0),
                reason="local_low",
            )
        )
    return signals


def _find_local_high_signals(
    df: pd.DataFrame,
    order: int,
    min_swing_pct: float,
) -> list[TechniqueSignal]:
    """국소 고점 매도 신호 (order봉 후 확정)."""
    signals: list[TechniqueSignal] = []
    lookback = order * 4

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

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

        confirm_idx = pivot_idx + order
        signals.append(
            TechniqueSignal(
                side="sell",
                bar_index=confirm_idx,
                price=round(high_val, 2),
                datetime=pd.Timestamp(df.iloc[confirm_idx]["datetime"]).strftime("%Y-%m-%d %H:%M:%S"),
                pivot_bar_index=pivot_idx,
                confidence=min(1.0, rise_pct / 10.0),
                reason="local_high",
            )
        )
    return signals


def _dedupe_signals(signals: list[TechniqueSignal], min_bars: int) -> list[TechniqueSignal]:
    """동일 방향 근접 신호를 병합한다."""
    if not signals:
        return []

    sorted_signals = sorted(signals, key=lambda s: s.bar_index)
    merged: list[TechniqueSignal] = [sorted_signals[0]]

    for signal in sorted_signals[1:]:
        last = merged[-1]
        if signal.side == last.side and signal.bar_index - last.bar_index < min_bars:
            if signal.side == "buy" and signal.price < last.price:
                merged[-1] = signal
            elif signal.side == "sell" and signal.price > last.price:
                merged[-1] = signal
        else:
            merged.append(signal)

    return sorted(merged, key=lambda s: s.bar_index)
