"""Mean reversion signal generators: RSI, Bollinger, CCI."""

from __future__ import annotations

import numpy as np

from pyalbedo.core.types import Direction, Signal
from pyalbedo.core.window import MarketDataWindow
from pyalbedo.indicators.oscillators import CCI, RSI
from pyalbedo.indicators.volatility import BollingerBands
from pyalbedo.signals.base import SignalGenerator


class RSIMeanReversion(SignalGenerator):
    """Long when RSI < oversold; short when RSI > overbought. Strength from distance from threshold."""

    def __init__(
        self,
        period: int = 14,
        oversold: float = 30.0,
        overbought: float = 70.0,
    ) -> None:
        if oversold >= overbought:
            raise ValueError("oversold must be < overbought")
        self._period = period
        self._oversold = oversold
        self._overbought = overbought
        self._rsi = RSI(period=period)

    @property
    def name(self) -> str:
        return f"RSIMeanReversion({self._period},{self._oversold},{self._overbought})"

    @property
    def required_lookback(self) -> int:
        return self._rsi.lookback + 1

    def generate(self, window: MarketDataWindow) -> Signal | None:
        if len(window) < self.required_lookback:
            return None
        rsi_series = self._rsi.calculate(window.close)
        r = rsi_series[-1]
        if np.isnan(r):
            return None
        if r < self._oversold:
            strength = (self._oversold - r) / self._oversold
            strength = min(1.0, max(0.0, strength))
            return Signal(
                direction=Direction.LONG,
                strength=strength,
                timestamp=window.last_timestamp,
                source=self.name,
                metadata={"rsi": float(r), "price": float(window.close[-1])},
            )
        if r > self._overbought:
            strength = (r - self._overbought) / (100 - self._overbought)
            strength = min(1.0, max(0.0, strength))
            return Signal(
                direction=Direction.SHORT,
                strength=strength,
                timestamp=window.last_timestamp,
                source=self.name,
                metadata={"rsi": float(r), "price": float(window.close[-1])},
            )
        return None


class BollingerMeanReversion(SignalGenerator):
    """Long when price at/below lower band; short at/above upper band. Optional RSI filter."""

    def __init__(
        self,
        period: int = 20,
        num_std: float = 2.0,
        use_rsi_filter: bool = False,
        rsi_period: int = 14,
        rsi_oversold: float = 30.0,
        rsi_overbought: float = 70.0,
    ) -> None:
        self._period = period
        self._num_std = num_std
        self._use_rsi_filter = use_rsi_filter
        self._bb = BollingerBands(period=period, num_std=num_std)
        self._rsi = RSI(period=rsi_period) if use_rsi_filter else None
        self._rsi_oversold = rsi_oversold
        self._rsi_overbought = rsi_overbought

    @property
    def name(self) -> str:
        return f"BollingerMeanReversion({self._period},{self._num_std})"

    @property
    def required_lookback(self) -> int:
        lb = self._bb.lookback + 1
        if self._rsi is not None:
            lb = max(lb, self._rsi.lookback + 1)
        return lb

    def generate(self, window: MarketDataWindow) -> Signal | None:
        if len(window) < self.required_lookback:
            return None
        bb_arr = self._bb.calculate(window.close)
        upper = bb_arr[:, 0]
        middle = bb_arr[:, 1]
        lower = bb_arr[:, 2]
        price = window.close[-1]
        if np.isnan(upper[-1]) or np.isnan(lower[-1]):
            return None
        if price <= lower[-1]:
            if self._use_rsi_filter and self._rsi is not None:
                rsi_series = self._rsi.calculate(window.close)
                r = rsi_series[-1]
                if np.isnan(r) or r > self._rsi_oversold:
                    return None
            strength = (lower[-1] - price) / (middle[-1] - lower[-1]) if middle[-1] != lower[-1] else 1.0
            strength = min(1.0, max(0.0, strength))
            return Signal(
                direction=Direction.LONG,
                strength=strength,
                timestamp=window.last_timestamp,
                source=self.name,
                metadata={
                    "price": float(price),
                    "lower_band": float(lower[-1]),
                    "middle": float(middle[-1]),
                },
            )
        if price >= upper[-1]:
            if self._use_rsi_filter and self._rsi is not None:
                rsi_series = self._rsi.calculate(window.close)
                r = rsi_series[-1]
                if np.isnan(r) or r < self._rsi_overbought:
                    return None
            strength = (price - upper[-1]) / (upper[-1] - middle[-1]) if upper[-1] != middle[-1] else 1.0
            strength = min(1.0, max(0.0, strength))
            return Signal(
                direction=Direction.SHORT,
                strength=strength,
                timestamp=window.last_timestamp,
                source=self.name,
                metadata={
                    "price": float(price),
                    "upper_band": float(upper[-1]),
                    "middle": float(middle[-1]),
                },
            )
        return None


class CCIMeanReversion(SignalGenerator):
    """Long when CCI < -100; short when CCI > +100. Configurable levels."""

    def __init__(
        self,
        period: int = 20,
        long_level: float = -100.0,
        short_level: float = 100.0,
    ) -> None:
        if long_level >= short_level:
            raise ValueError("long_level must be < short_level")
        self._period = period
        self._long_level = long_level
        self._short_level = short_level
        self._cci = CCI(period=period)

    @property
    def name(self) -> str:
        return f"CCIMeanReversion({self._period},{self._long_level},{self._short_level})"

    @property
    def required_lookback(self) -> int:
        return self._cci.lookback + 1

    def generate(self, window: MarketDataWindow) -> Signal | None:
        if len(window) < self.required_lookback:
            return None
        cci_series = self._cci.calculate(window.high, window.low, window.close)
        c = cci_series[-1]
        if np.isnan(c):
            return None
        if c < self._long_level:
            strength = min(1.0, abs(c - self._long_level) / 200.0)
            return Signal(
                direction=Direction.LONG,
                strength=strength,
                timestamp=window.last_timestamp,
                source=self.name,
                metadata={"cci": float(c), "price": float(window.close[-1])},
            )
        if c > self._short_level:
            strength = min(1.0, abs(c - self._short_level) / 200.0)
            return Signal(
                direction=Direction.SHORT,
                strength=strength,
                timestamp=window.last_timestamp,
                source=self.name,
                metadata={"cci": float(c), "price": float(window.close[-1])},
            )
        return None
