"""Fibonacci retracement signal evaluator.

Computes retracement levels using ``L(r) = SH - r * (SH - SL)`` for the
standard ratios [0.236, 0.382, 0.5, 0.618, 0.786] and produces a signal
based on the proximity of the current price to the nearest level.
"""

from __future__ import annotations

from services.signal_engine.models import OHLCVBar, SignalDirection, SignalResult
from services.signal_engine.signals.base import (
    find_swing_high,
    find_swing_low,
    validate_lookback,
)

# Standard Fibonacci retracement ratios
RETRACEMENT_RATIOS: list[float] = [0.236, 0.382, 0.5, 0.618, 0.786]

# Ratios considered "key" levels — proximity to these yields higher confidence
_KEY_RATIOS: set[float] = {0.5, 0.618}

# Default minimum number of bars required for evaluation
DEFAULT_MIN_BARS: int = 20


class FibonacciEvaluator:
    """Fibonacci retracement signal evaluator.

    Satisfies the :class:`~services.signal_engine.signals.base.SignalEvaluator`
    protocol.

    Parameters
    ----------
    min_bars:
        Minimum number of OHLCV bars required before the evaluator will
        produce a signal.  Defaults to ``20``.
    """

    def __init__(self, min_bars: int = DEFAULT_MIN_BARS) -> None:
        self.min_bars = min_bars

    # ------------------------------------------------------------------
    # Public API (SignalEvaluator protocol)
    # ------------------------------------------------------------------

    def evaluate(
        self,
        bars: list[OHLCVBar],
        timeframe: str,
    ) -> SignalResult | None:
        """Evaluate Fibonacci retracement on *bars* for *timeframe*.

        Returns ``None`` when there are fewer than :pyattr:`min_bars` bars,
        or when the swing high equals the swing low (flat market — no valid
        retracement).
        """
        if not validate_lookback(bars, self.min_bars):
            return None

        # Detect swing high / swing low within the evaluation window
        sh_result = find_swing_high(bars, self.min_bars)
        sl_result = find_swing_low(bars, self.min_bars)

        if sh_result is None or sl_result is None:
            return None

        _sh_idx, sh_price = sh_result
        _sl_idx, sl_price = sl_result

        # SH must be strictly greater than SL for a valid retracement range
        if sh_price <= sl_price:
            return None

        price_range = sh_price - sl_price
        current_price = bars[-1].close

        # Compute retracement levels: L(r) = SH - r * (SH - SL)
        levels: dict[float, float] = {
            r: sh_price - r * price_range for r in RETRACEMENT_RATIOS
        }

        # Find the nearest retracement level to the current price
        nearest_ratio: float = RETRACEMENT_RATIOS[0]
        nearest_level: float = levels[nearest_ratio]
        min_distance: float = abs(current_price - nearest_level)

        for ratio in RETRACEMENT_RATIOS[1:]:
            distance = abs(current_price - levels[ratio])
            if distance < min_distance:
                min_distance = distance
                nearest_ratio = ratio
                nearest_level = levels[ratio]

        # Signal strength: 1.0 - (distance / range), clamped to [0, 1]
        raw_strength = 1.0 - (min_distance / price_range)
        strength = max(0.0, min(1.0, raw_strength))

        # Direction: BULLISH if price is near a retracement level and above SL
        # (potential bounce off support).  Otherwise BEARISH.
        if current_price >= sl_price:
            direction = SignalDirection.BULLISH
        else:
            direction = SignalDirection.BEARISH

        # Confidence: higher when the nearest level is a key ratio (0.618, 0.5)
        if nearest_ratio in _KEY_RATIOS:
            confidence = min(1.0, strength * 1.2)
        else:
            confidence = strength * 0.8

        return SignalResult(
            signal_type="fibonacci",
            timeframe=timeframe,
            strength=strength,
            direction=direction,
            confidence=confidence,
            metadata={
                "swing_high": sh_price,
                "swing_low": sl_price,
                "retracement_levels": levels,
                "nearest_ratio": nearest_ratio,
                "nearest_level": nearest_level,
                "distance_to_nearest": min_distance,
                "current_price": current_price,
            },
        )
