"""Oscillators: RSI, Stochastic, MACD, CCI, Williams %R, Ultimate Oscillator, ROC."""

from __future__ import annotations

import numpy as np
from numpy.typing import NDArray

from pyalbedo.indicators.base import Indicator
from pyalbedo.indicators.moving_averages import EMA, SMA
from pyalbedo.utils.validation import ensure_float64_1d


class RSI(Indicator[NDArray[np.float64]]):
    """
    Relative Strength Index (Wilder smoothing).
    RSI = 100 - 100/(1 + RS), RS = avg_gain / avg_loss over period.
    """

    def __init__(self, period: int = 14) -> None:
        if period < 1:
            raise ValueError("period must be >= 1")
        self._period = period

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

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

    def calculate(self, *inputs: NDArray[np.float64]) -> NDArray[np.float64]:
        prices = ensure_float64_1d(inputs[0])
        n = len(prices)
        result = np.full(n, np.nan, dtype=np.float64)
        if n < self._period + 1:
            return result
        deltas = np.diff(prices)
        gains = np.where(deltas > 0, deltas, 0.0)
        losses = np.where(deltas < 0, -deltas, 0.0)
        for i in range(self._period, n):
            g = np.mean(gains[i - self._period : i])
            avg_loss = np.mean(losses[i - self._period : i])
            if avg_loss == 0:
                result[i] = 100.0
            else:
                rs = g / avg_loss
                result[i] = 100.0 - 100.0 / (1 + rs)
        return result


class Stochastic(Indicator[NDArray[np.float64]]):
    """
    Stochastic Oscillator: %K and %D.
    %K = (close - low_n) / (high_n - low_n) * 100, %D = SMA(%K, smooth_k).
    Returns (percent_k, percent_d) as a 2D array of shape (n, 2).
    """

    def __init__(self, k_period: int = 14, d_period: int = 3) -> None:
        if k_period < 1 or d_period < 1:
            raise ValueError("periods must be >= 1")
        self._k_period = k_period
        self._d_period = d_period
        self._sma = SMA(d_period)

    @property
    def name(self) -> str:
        return f"Stochastic({self._k_period},{self._d_period})"

    @property
    def lookback(self) -> int:
        return self._k_period + self._d_period - 1

    def calculate(
        self,
        high: NDArray[np.float64],
        low: NDArray[np.float64],
        close: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        high = ensure_float64_1d(high)
        low = ensure_float64_1d(low)
        close = ensure_float64_1d(close)
        n = len(close)
        pk = np.full(n, np.nan, dtype=np.float64)
        for i in range(self._k_period - 1, n):
            h = high[i - self._k_period + 1 : i + 1]
            lo = low[i - self._k_period + 1 : i + 1]
            c = close[i]
            r = np.max(h) - np.min(lo)
            if r > 0:
                pk[i] = 100.0 * (c - np.min(lo)) / r
            else:
                pk[i] = 50.0
        pd = self._sma.calculate(pk)
        return np.column_stack((pk, pd))


class MACD(Indicator[NDArray[np.float64]]):
    """
    MACD: line = EMA(fast) - EMA(slow), signal = EMA(line, signal_period), histogram = line - signal.
    Returns (macd_line, signal_line, histogram) as 2D array shape (n, 3).
    """

    def __init__(
        self,
        fast_period: int = 12,
        slow_period: int = 26,
        signal_period: int = 9,
    ) -> None:
        if fast_period < 1 or slow_period < fast_period or signal_period < 1:
            raise ValueError("invalid MACD periods")
        self._fast_period = fast_period
        self._slow_period = slow_period
        self._signal_period = signal_period
        self._ema_fast = EMA(fast_period)
        self._ema_slow = EMA(slow_period)
        self._ema_signal = EMA(signal_period)

    @property
    def name(self) -> str:
        return f"MACD({self._fast_period},{self._slow_period},{self._signal_period})"

    @property
    def lookback(self) -> int:
        return self._slow_period + self._signal_period - 1

    def calculate(self, *inputs: NDArray[np.float64]) -> NDArray[np.float64]:
        prices = ensure_float64_1d(inputs[0])
        line = self._ema_fast.calculate(prices) - self._ema_slow.calculate(prices)
        signal = self._ema_signal.calculate(line)
        hist = line - signal
        return np.column_stack((line, signal, hist))


class CCI(Indicator[NDArray[np.float64]]):
    """
    Commodity Channel Index. Typical price = (H+L+C)/3; CCI = (TP - SMA(TP)) / (0.015 * MD).
    MD = mean absolute deviation of TP over period.
    """

    def __init__(self, period: int = 20) -> None:
        if period < 1:
            raise ValueError("period must be >= 1")
        self._period = period
        self._sma = SMA(period)

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

    @property
    def lookback(self) -> int:
        return self._period

    def calculate(
        self,
        high: NDArray[np.float64],
        low: NDArray[np.float64],
        close: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        high = ensure_float64_1d(high)
        low = ensure_float64_1d(low)
        close = ensure_float64_1d(close)
        tp = np.asarray((high + low + close) / 3.0, dtype=np.float64)
        sma_tp = self._sma.calculate(tp)
        n = len(tp)
        result = np.full(n, np.nan, dtype=np.float64)
        for i in range(self._period - 1, n):
            slice_tp = tp[i - self._period + 1 : i + 1]
            md = np.mean(np.abs(slice_tp - sma_tp[i]))
            if md > 0:
                result[i] = (tp[i] - sma_tp[i]) / (0.015 * md)
            else:
                result[i] = 0.0
        return result


class WilliamsR(Indicator[NDArray[np.float64]]):
    """Williams %R: -100 * (high_n - close) / (high_n - low_n)."""

    def __init__(self, period: int = 14) -> None:
        if period < 1:
            raise ValueError("period must be >= 1")
        self._period = period

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

    @property
    def lookback(self) -> int:
        return self._period

    def calculate(
        self,
        high: NDArray[np.float64],
        low: NDArray[np.float64],
        close: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        high = ensure_float64_1d(high)
        low = ensure_float64_1d(low)
        close = ensure_float64_1d(close)
        n = len(close)
        result = np.full(n, np.nan, dtype=np.float64)
        for i in range(self._period - 1, n):
            h = np.max(high[i - self._period + 1 : i + 1])
            lo = np.min(low[i - self._period + 1 : i + 1])
            r = h - lo
            if r > 0:
                result[i] = -100.0 * (h - close[i]) / r
            else:
                result[i] = -50.0
        return result


class UltimateOscillator(Indicator[NDArray[np.float64]]):
    """
    Ultimate Oscillator: weighted average of RSI-like ratios over 7, 14, 28 periods.
    BP = close - min(low, prev_close), TR = max(high, prev_close) - min(low, prev_close).
    """

    def __init__(self) -> None:
        self._periods = (7, 14, 28)

    @property
    def name(self) -> str:
        return "UltimateOscillator(7,14,28)"

    @property
    def lookback(self) -> int:
        return 29

    def calculate(
        self,
        high: NDArray[np.float64],
        low: NDArray[np.float64],
        close: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        high = ensure_float64_1d(high)
        low = ensure_float64_1d(low)
        close = ensure_float64_1d(close)
        n = len(close)
        result = np.full(n, np.nan, dtype=np.float64)
        prev_close = np.roll(close, 1)
        prev_close[0] = close[0]
        bp = close - np.minimum(low, prev_close)
        tr = np.maximum(high, prev_close) - np.minimum(low, prev_close)
        weights = (4.0, 2.0, 1.0)
        for i in range(28, n):
            avg = 0.0
            denom = 0.0
            for period, w in zip(self._periods, weights, strict=True):
                bp_sum = np.sum(bp[i - period + 1 : i + 1])
                tr_sum = np.sum(tr[i - period + 1 : i + 1])
                if tr_sum > 0:
                    avg += w * (100.0 * bp_sum / tr_sum)
                denom += w
            if denom > 0:
                result[i] = avg / denom
        return result


class ROC(Indicator[NDArray[np.float64]]):
    """Rate of Change: (close - close[n-period]) / close[n-period] * 100."""

    def __init__(self, period: int = 10) -> None:
        if period < 1:
            raise ValueError("period must be >= 1")
        self._period = period

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

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

    def calculate(self, *inputs: NDArray[np.float64]) -> NDArray[np.float64]:
        prices = ensure_float64_1d(inputs[0])
        n = len(prices)
        result = np.full(n, np.nan, dtype=np.float64)
        for i in range(self._period, n):
            prev = prices[i - self._period]
            if prev != 0:
                result[i] = 100.0 * (prices[i] - prev) / prev
            else:
                result[i] = 0.0
        return result
