"""Volume: OBV, VWAP, MFI, VWMA."""

from __future__ import annotations

import numpy as np
from numpy.typing import NDArray

from pyalbedo.indicators.base import Indicator
from pyalbedo.utils.validation import ensure_float64_1d


class OBV(Indicator[NDArray[np.float64]]):
    """On-Balance Volume: cumulative signed volume (sign from close vs prev close)."""

    @property
    def name(self) -> str:
        return "OBV"

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

    def calculate(
        self,
        close: NDArray[np.float64],
        volume: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        close = ensure_float64_1d(close)
        volume = ensure_float64_1d(volume)
        n = len(close)
        if n != len(volume):
            raise ValueError("close and volume must have same length")
        result = np.full(n, np.nan, dtype=np.float64)
        result[0] = volume[0]
        for i in range(1, n):
            if close[i] > close[i - 1]:
                result[i] = result[i - 1] + volume[i]
            elif close[i] < close[i - 1]:
                result[i] = result[i - 1] - volume[i]
            else:
                result[i] = result[i - 1]
        return result


class VWAP(Indicator[NDArray[np.float64]]):
    """
    Volume-Weighted Average Price: cumsum(typical_price * volume) / cumsum(volume).
    Typical price = (high + low + close) / 3.
    """

    @property
    def name(self) -> str:
        return "VWAP"

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

    def calculate(
        self,
        high: NDArray[np.float64],
        low: NDArray[np.float64],
        close: NDArray[np.float64],
        volume: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        high = ensure_float64_1d(high)
        low = ensure_float64_1d(low)
        close = ensure_float64_1d(close)
        volume = ensure_float64_1d(volume)
        n = len(close)
        if n != len(volume) or n != len(high) or n != len(low):
            raise ValueError("high, low, close, volume must have same length")
        tp = (high + low + close) / 3.0
        pv = np.cumsum(tp * volume)
        vol_cum = np.cumsum(volume)
        result = np.where(vol_cum > 0, pv / vol_cum, np.nan)
        return result.astype(np.float64)


class MFI(Indicator[NDArray[np.float64]]):
    """
    Money Flow Index (14-period typical). Raw money flow = typical_price * volume;
    positive flow when typical_price > prev, else negative. MFI = 100 - 100/(1 + MR)
    where MR = sum(positive flow) / sum(negative flow) 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"MFI({self._period})"

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

    def calculate(
        self,
        high: NDArray[np.float64],
        low: NDArray[np.float64],
        close: NDArray[np.float64],
        volume: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        high = ensure_float64_1d(high)
        low = ensure_float64_1d(low)
        close = ensure_float64_1d(close)
        volume = ensure_float64_1d(volume)
        n = len(close)
        tp = (high + low + close) / 3.0
        raw = tp * volume
        result = np.full(n, np.nan, dtype=np.float64)
        for i in range(self._period, n):
            pos = 0.0
            neg = 0.0
            for j in range(i - self._period + 1, i + 1):
                if tp[j] > tp[j - 1]:
                    pos += raw[j]
                elif tp[j] < tp[j - 1]:
                    neg += raw[j]
            if neg > 0:
                mr = pos / neg
                result[i] = 100.0 - 100.0 / (1 + mr)
            else:
                result[i] = 100.0
        return result


class VWMA(Indicator[NDArray[np.float64]]):
    """Volume-Weighted Moving Average: sum(close * volume) / sum(volume) over period."""

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

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

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

    def calculate(
        self,
        close: NDArray[np.float64],
        volume: NDArray[np.float64],
    ) -> NDArray[np.float64]:
        close = ensure_float64_1d(close)
        volume = ensure_float64_1d(volume)
        n = len(close)
        if n != len(volume):
            raise ValueError("close and volume must have same length")
        result = np.full(n, np.nan, dtype=np.float64)
        for i in range(self._period - 1, n):
            c = close[i - self._period + 1 : i + 1]
            v = volume[i - self._period + 1 : i + 1]
            vsum = np.sum(v)
            if vsum > 0:
                result[i] = np.sum(c * v) / vsum
            else:
                result[i] = np.mean(c)
        return result
