"""Base protocol and common helpers for signal evaluators.

Defines the ``SignalEvaluator`` protocol that every signal in the Signal
Library must satisfy, plus shared utility functions for swing detection,
lookback validation, and simple moving average computation.
"""

from __future__ import annotations

from typing import Protocol

from services.signal_engine.models import OHLCVBar, SignalResult

# ---------------------------------------------------------------------------
# Signal evaluator protocol
# ---------------------------------------------------------------------------


class SignalEvaluator(Protocol):
    """Protocol for all signal evaluators in the Signal Library.

    Each evaluator receives a list of OHLCV bars for a single timeframe
    and returns a ``SignalResult`` when the signal triggers, or ``None``
    when insufficient data is available or the signal does not fire.
    """

    def evaluate(
        self,
        bars: list[OHLCVBar],
        timeframe: str,
    ) -> SignalResult | None:
        """Evaluate a signal on a single timeframe's bar data.

        Returns ``None`` when insufficient data is available.
        """
        ...


# ---------------------------------------------------------------------------
# Common helper functions
# ---------------------------------------------------------------------------


def find_swing_high(
    bars: list[OHLCVBar],
    lookback: int,
) -> tuple[int, float] | None:
    """Find the highest high in the last *lookback* bars.

    Args:
        bars: OHLCV bar series (oldest-first).
        lookback: Number of recent bars to search.

    Returns:
        ``(index, price)`` of the bar with the highest high within the
        lookback window, or ``None`` if *bars* has fewer than *lookback*
        entries.
    """
    if len(bars) < lookback or lookback <= 0:
        return None

    window = bars[-lookback:]
    offset = len(bars) - lookback

    best_idx = 0
    best_price = window[0].high
    for i, bar in enumerate(window):
        if bar.high >= best_price:
            best_idx = i
            best_price = bar.high

    return (offset + best_idx, best_price)


def find_swing_low(
    bars: list[OHLCVBar],
    lookback: int,
) -> tuple[int, float] | None:
    """Find the lowest low in the last *lookback* bars.

    Args:
        bars: OHLCV bar series (oldest-first).
        lookback: Number of recent bars to search.

    Returns:
        ``(index, price)`` of the bar with the lowest low within the
        lookback window, or ``None`` if *bars* has fewer than *lookback*
        entries.
    """
    if len(bars) < lookback or lookback <= 0:
        return None

    window = bars[-lookback:]
    offset = len(bars) - lookback

    best_idx = 0
    best_price = window[0].low
    for i, bar in enumerate(window):
        if bar.low <= best_price:
            best_idx = i
            best_price = bar.low

    return (offset + best_idx, best_price)


def validate_lookback(bars: list[OHLCVBar], min_bars: int) -> bool:
    """Return ``True`` if *bars* contains at least *min_bars* entries."""
    return len(bars) >= min_bars


def compute_sma(bars: list[OHLCVBar], period: int) -> float | None:
    """Compute the simple moving average of close prices over the last *period* bars.

    Args:
        bars: OHLCV bar series (oldest-first).
        period: Number of recent bars to average.

    Returns:
        The arithmetic mean of the last *period* close prices, or ``None``
        if *bars* has fewer than *period* entries or *period* is not
        positive.
    """
    if period <= 0 or len(bars) < period:
        return None

    total = sum(bar.close for bar in bars[-period:])
    return total / period
