"""RSI·MACD 다이버전스 매수·매도 타점 (Ground Truth, 사후 검증)."""

from __future__ import annotations

from dataclasses import dataclass

import pandas as pd

from deepcoin.techniques.indicators import macd, rsi


@dataclass(frozen=True)
class DivergenceSignal:
    """다이버전스 신호."""

    side: str  # buy | sell
    bar_index: int
    price: float
    datetime: pd.Timestamp
    indicator: str  # rsi | macd_hist
    price_prev: float
    price_curr: float
    ind_prev: float
    ind_curr: float


def find_divergence_signals(
    df: pd.DataFrame,
    local_order: int = 15,
    min_bars_between: int = 100,
    max_pair_lookback_bars: int = 5000,
    rsi_period: int = 14,
    min_rsi_diff: float = 2.0,
    min_macd_hist_diff: float = 0.0,
    min_price_move_pct: float = 1.5,
    future_bars: int = 2000,
    min_future_move_pct: float = 2.0,
) -> tuple[list[DivergenceSignal], list[DivergenceSignal]]:
    """가격·지표 다이버전스 매수·매도 후보를 찾는다.

    - 상승 다이버전스(매수): 가격 LL + RSI/MACD HL
    - 하락 다이버전스(매도): 가격 HH + RSI/MACD LH
    - 미래 데이터로 이후 유의미한 반등·하락을 사후 검증

    Args:
        df: OHLCV DataFrame.
        local_order: 국소 극값 반경(봉).
        min_bars_between: 연속 다이버전스 최소 간격(봉).
        max_pair_lookback_bars: 비교할 이전 극값 최대 거리(봉).
        rsi_period: RSI 기간.
        min_rsi_diff: RSI 다이버전스 최소 차이(포인트).
        min_macd_hist_diff: MACD 히스토그램 최소 차이.
        min_price_move_pct: 극값 간 최소 가격 변동(%).
        future_bars: 사후 검증 구간(봉).
        min_future_move_pct: 사후 최소 가격 변동(%).

    Returns:
        (매수 신호, 매도 신호) 리스트.
    """
    if len(df) < local_order * 4 + rsi_period:
        return [], []

    close = df["close"].astype(float)
    low = df["low"].astype(float)
    high = df["high"].astype(float)
    rsi_vals = rsi(close, period=rsi_period)
    _, _, macd_hist = macd(close)

    lows = _find_local_extrema(df, low, rsi_vals, macd_hist, local_order, "low")
    highs = _find_local_extrema(df, high, rsi_vals, macd_hist, local_order, "high")

    bull_rsi = _detect_bullish(
        df, lows, rsi_vals, "rsi", min_rsi_diff, min_price_move_pct,
        max_pair_lookback_bars, future_bars, min_future_move_pct, high,
    )
    bear_rsi = _detect_bearish(
        df, highs, rsi_vals, "rsi", min_rsi_diff, min_price_move_pct,
        max_pair_lookback_bars, future_bars, min_future_move_pct, low,
    )

    bull_macd: list[DivergenceSignal] = []
    bear_macd: list[DivergenceSignal] = []
    if min_macd_hist_diff > 0:
        bull_macd = _detect_bullish(
            df, lows, macd_hist, "macd_hist", min_macd_hist_diff, min_price_move_pct,
            max_pair_lookback_bars, future_bars, min_future_move_pct, high,
        )
        bear_macd = _detect_bearish(
            df, highs, macd_hist, "macd_hist", min_macd_hist_diff, min_price_move_pct,
            max_pair_lookback_bars, future_bars, min_future_move_pct, low,
        )

    buys = _dedupe_signals(
        _merge_same_bar(bull_rsi + bull_macd), min_bars_between, prefer_lower=True
    )
    sells = _dedupe_signals(
        _merge_same_bar(bear_rsi + bear_macd), min_bars_between, prefer_lower=False
    )
    return buys, sells


@dataclass
class _ExtremePoint:
    """국소 극값."""

    bar_index: int
    price: float
    rsi: float
    macd_hist: float
    datetime: pd.Timestamp


def _find_local_extrema(
    df: pd.DataFrame,
    series: pd.Series,
    rsi_vals: pd.Series,
    macd_hist: pd.Series,
    order: int,
    side: str,
) -> list[_ExtremePoint]:
    """국소 저점·고점을 수집한다."""
    points: list[_ExtremePoint] = []
    values = series.values

    for i in range(order, len(df) - order):
        window = values[i - order : i + order + 1]
        val = float(values[i])
        if side == "low" and val > window.min():
            continue
        if side == "high" and val < window.max():
            continue
        if pd.isna(rsi_vals.iloc[i]) or pd.isna(macd_hist.iloc[i]):
            continue
        points.append(
            _ExtremePoint(
                bar_index=i,
                price=val,
                rsi=float(rsi_vals.iloc[i]),
                macd_hist=float(macd_hist.iloc[i]),
                datetime=pd.Timestamp(df.iloc[i]["datetime"]),
            )
        )
    return points


def _detect_bullish(
    df: pd.DataFrame,
    lows: list[_ExtremePoint],
    indicator: pd.Series,
    indicator_name: str,
    min_ind_diff: float,
    min_price_move_pct: float,
    max_lookback: int,
    future_bars: int,
    min_future_move_pct: float,
    highs,
) -> list[DivergenceSignal]:
    """상승 다이버전스 매수를 탐지한다."""
    signals: list[DivergenceSignal] = []
    if len(lows) < 2:
        return signals

    for idx in range(1, len(lows)):
        curr = lows[idx]
        for prev in reversed(lows[:idx]):
            if curr.bar_index - prev.bar_index > max_lookback:
                break
            if curr.bar_index - prev.bar_index < 20:
                continue

            price_move = (prev.price - curr.price) / prev.price * 100.0
            if price_move < min_price_move_pct:
                continue

            ind_prev = float(indicator.iloc[prev.bar_index])
            ind_curr = float(indicator.iloc[curr.bar_index])
            if pd.isna(ind_prev) or pd.isna(ind_curr):
                continue
            if not (curr.price < prev.price and ind_curr > ind_prev + min_ind_diff):
                continue

            end = min(len(df), curr.bar_index + future_bars + 1)
            future_high = float(highs[curr.bar_index:end].max())
            rally = (future_high - curr.price) / curr.price * 100.0
            if rally < min_future_move_pct:
                continue

            signals.append(
                DivergenceSignal(
                    side="buy",
                    bar_index=curr.bar_index,
                    price=round(curr.price, 2),
                    datetime=curr.datetime,
                    indicator=indicator_name,
                    price_prev=round(prev.price, 2),
                    price_curr=round(curr.price, 2),
                    ind_prev=round(ind_prev, 4),
                    ind_curr=round(ind_curr, 4),
                )
            )
            break

    return signals


def _detect_bearish(
    df: pd.DataFrame,
    highs: list[_ExtremePoint],
    indicator: pd.Series,
    indicator_name: str,
    min_ind_diff: float,
    min_price_move_pct: float,
    max_lookback: int,
    future_bars: int,
    min_future_move_pct: float,
    lows,
) -> list[DivergenceSignal]:
    """하락 다이버전스 매도를 탐지한다."""
    signals: list[DivergenceSignal] = []
    if len(highs) < 2:
        return signals

    for idx in range(1, len(highs)):
        curr = highs[idx]
        for prev in reversed(highs[:idx]):
            if curr.bar_index - prev.bar_index > max_lookback:
                break
            if curr.bar_index - prev.bar_index < 20:
                continue

            price_move = (curr.price - prev.price) / prev.price * 100.0
            if price_move < min_price_move_pct:
                continue

            ind_prev = float(indicator.iloc[prev.bar_index])
            ind_curr = float(indicator.iloc[curr.bar_index])
            if pd.isna(ind_prev) or pd.isna(ind_curr):
                continue
            if not (curr.price > prev.price and ind_curr < ind_prev - min_ind_diff):
                continue

            end = min(len(df), curr.bar_index + future_bars + 1)
            future_low = float(lows[curr.bar_index:end].min())
            drop = (curr.price - future_low) / curr.price * 100.0
            if drop < min_future_move_pct:
                continue

            signals.append(
                DivergenceSignal(
                    side="sell",
                    bar_index=curr.bar_index,
                    price=round(curr.price, 2),
                    datetime=curr.datetime,
                    indicator=indicator_name,
                    price_prev=round(prev.price, 2),
                    price_curr=round(curr.price, 2),
                    ind_prev=round(ind_prev, 4),
                    ind_curr=round(ind_curr, 4),
                )
            )
            break

    return signals


def _merge_same_bar(signals: list[DivergenceSignal]) -> list[DivergenceSignal]:
    """동일 봉·동일 방향 신호를 하나로 합친다."""
    by_bar: dict[int, DivergenceSignal] = {}
    for signal in signals:
        prev = by_bar.get(signal.bar_index)
        if prev is None:
            by_bar[signal.bar_index] = signal
            continue
        prev_diff = abs(prev.ind_curr - prev.ind_prev)
        curr_diff = abs(signal.ind_curr - signal.ind_prev)
        if curr_diff > prev_diff:
            by_bar[signal.bar_index] = signal
    return sorted(by_bar.values(), key=lambda s: s.bar_index)


def _dedupe_signals(
    signals: list[DivergenceSignal],
    min_bars: int,
    prefer_lower: bool,
) -> list[DivergenceSignal]:
    """근접 신호를 병합한다."""
    if not signals:
        return []

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

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

    return merged
