from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Protocol

from kraken_bot.indicators import bollinger_bands, ema, macd, rsi, sma
from kraken_bot.models import Candle, Side, Signal


class Strategy(Protocol):
    name: str

    def generate(self, candles: list[Candle]) -> Signal:
        ...


def _closes(candles: list[Candle]) -> list[float]:
    return [candle.close for candle in candles]


def _crossed_above(prev_a: float, prev_b: float, curr_a: float, curr_b: float) -> bool:
    return prev_a <= prev_b and curr_a > curr_b


def _crossed_below(prev_a: float, prev_b: float, curr_a: float, curr_b: float) -> bool:
    return prev_a >= prev_b and curr_a < curr_b


@dataclass(frozen=True)
class SMACrossoverStrategy:
    short_period: int = 20
    long_period: int = 50
    name: str = "sma_crossover"

    def generate(self, candles: list[Candle]) -> Signal:
        closes = _closes(candles)
        short = sma(closes, self.short_period)
        long = sma(closes, self.long_period)
        if len(closes) < self.long_period + 1 or None in (short[-1], short[-2], long[-1], long[-2]):
            return Signal.hold("Not enough data for SMA crossover")
        if _crossed_above(short[-2], long[-2], short[-1], long[-1]):
            return Signal(Side.BUY, f"SMA {self.short_period} crossed above SMA {self.long_period}", 0.75)
        if _crossed_below(short[-2], long[-2], short[-1], long[-1]):
            return Signal(Side.SELL, f"SMA {self.short_period} crossed below SMA {self.long_period}", 0.75)
        return Signal.hold("SMA lines have not crossed")


@dataclass(frozen=True)
class RSIMeanReversionStrategy:
    period: int = 14
    oversold: float = 30.0
    overbought: float = 70.0
    name: str = "rsi_mean_reversion"

    def generate(self, candles: list[Candle]) -> Signal:
        values = rsi(_closes(candles), self.period)
        current = values[-1] if values else None
        if current is None:
            return Signal.hold("Not enough data for RSI")
        if current <= self.oversold:
            return Signal(Side.BUY, f"RSI {current:.2f} is oversold", 0.65, {"rsi": current})
        if current >= self.overbought:
            return Signal(Side.SELL, f"RSI {current:.2f} is overbought", 0.65, {"rsi": current})
        return Signal.hold(f"RSI {current:.2f} is neutral")


@dataclass(frozen=True)
class MACDTrendStrategy:
    fast_period: int = 12
    slow_period: int = 26
    signal_period: int = 9
    name: str = "macd_trend"

    def generate(self, candles: list[Candle]) -> Signal:
        macd_line, signal_line, _ = macd(_closes(candles), self.fast_period, self.slow_period, self.signal_period)
        if len(macd_line) < 2 or None in (macd_line[-1], macd_line[-2], signal_line[-1], signal_line[-2]):
            return Signal.hold("Not enough data for MACD")
        if _crossed_above(macd_line[-2], signal_line[-2], macd_line[-1], signal_line[-1]):
            return Signal(Side.BUY, "MACD crossed above signal", 0.7)
        if _crossed_below(macd_line[-2], signal_line[-2], macd_line[-1], signal_line[-1]):
            return Signal(Side.SELL, "MACD crossed below signal", 0.7)
        return Signal.hold("MACD has not crossed")


@dataclass(frozen=True)
class BollingerMeanReversionStrategy:
    period: int = 20
    stddev: float = 2.0
    name: str = "bollinger_mean_reversion"

    def generate(self, candles: list[Candle]) -> Signal:
        closes = _closes(candles)
        lower, _, upper = bollinger_bands(closes, self.period, self.stddev)
        if not closes or lower[-1] is None or upper[-1] is None:
            return Signal.hold("Not enough data for Bollinger Bands")
        if closes[-1] <= lower[-1]:
            return Signal(Side.BUY, "Close is below lower Bollinger Band", 0.62)
        if closes[-1] >= upper[-1]:
            return Signal(Side.SELL, "Close is above upper Bollinger Band", 0.62)
        return Signal.hold("Close is inside Bollinger Bands")


@dataclass(frozen=True)
class DonchianBreakoutStrategy:
    lookback: int = 20
    name: str = "donchian_breakout"

    def generate(self, candles: list[Candle]) -> Signal:
        if len(candles) <= self.lookback:
            return Signal.hold("Not enough data for Donchian breakout")
        prior = candles[-self.lookback - 1 : -1]
        high = max(candle.high for candle in prior)
        low = min(candle.low for candle in prior)
        close = candles[-1].close
        if close > high:
            return Signal(Side.BUY, f"Close broke above {self.lookback}-candle high", 0.68)
        if close < low:
            return Signal(Side.SELL, f"Close broke below {self.lookback}-candle low", 0.68)
        return Signal.hold("No Donchian breakout")


@dataclass(frozen=True)
class EMATrendStrategy:
    period: int = 50
    slope_candles: int = 3
    name: str = "ema_trend"

    def generate(self, candles: list[Candle]) -> Signal:
        closes = _closes(candles)
        trend = ema(closes, self.period)
        if len(trend) <= self.slope_candles or trend[-1] is None or trend[-self.slope_candles] is None:
            return Signal.hold("Not enough data for EMA trend")
        rising = trend[-1] > trend[-self.slope_candles]
        falling = trend[-1] < trend[-self.slope_candles]
        if closes[-1] > trend[-1] and rising:
            return Signal(Side.BUY, "Price is above a rising EMA", 0.64)
        if closes[-1] < trend[-1] and falling:
            return Signal(Side.SELL, "Price is below a falling EMA", 0.64)
        return Signal.hold("EMA trend is mixed")


STRATEGY_REGISTRY: dict[str, type[Strategy]] = {
    "sma_crossover": SMACrossoverStrategy,
    "rsi_mean_reversion": RSIMeanReversionStrategy,
    "macd_trend": MACDTrendStrategy,
    "bollinger_mean_reversion": BollingerMeanReversionStrategy,
    "donchian_breakout": DonchianBreakoutStrategy,
    "ema_trend": EMATrendStrategy,
}


def build_strategy(name: str, params: dict[str, Any] | None = None) -> Strategy:
    try:
        strategy_type = STRATEGY_REGISTRY[name]
    except KeyError as exc:
        available = ", ".join(sorted(STRATEGY_REGISTRY))
        raise ValueError(f"Unknown strategy '{name}'. Available: {available}") from exc
    return strategy_type(**(params or {}))
