"""Cup & Handle pattern signal evaluator.

Detects the Cup & Handle chart pattern — a bullish continuation pattern
consisting of a U-shaped price recovery (the cup) followed by a small
consolidation pullback (the handle).

Pattern detection algorithm:
1. Find the left rim (local high in the first third of bars).
2. Find the cup bottom (lowest low between left rim and right rim area).
3. Find the right rim (local high in the last third of bars, near left rim price).
4. Identify the handle as a small pullback after the right rim (last few bars).

Pattern completeness scoring:
- Cup depth: ``(left_rim - bottom) / left_rim`` — valid range 12–33%.
- Symmetry: how close left_rim and right_rim prices are (within 5% = perfect).
- Handle: small pullback (< 50% of cup depth) after right rim.

The signal is always BULLISH (cup & handle is a bullish continuation pattern).
"""

from __future__ import annotations

from services.signal_engine.models import OHLCVBar, SignalDirection, SignalResult
from services.signal_engine.signals.base import validate_lookback

# Default minimum number of bars required for cup & handle detection
DEFAULT_MIN_BARS: int = 30

# Cup depth valid range (as fraction of left rim price)
_CUP_DEPTH_MIN: float = 0.12  # 12%
_CUP_DEPTH_MAX: float = 0.33  # 33%

# Symmetry: maximum allowed difference between left and right rim prices
# as a fraction of left rim price for "perfect" symmetry
_SYMMETRY_PERFECT_PCT: float = 0.05  # 5%

# Handle: maximum pullback as fraction of cup depth
_HANDLE_MAX_RETRACE: float = 0.50  # 50% of cup depth

# Handle lookback: number of bars at the end to check for handle
_HANDLE_LOOKBACK_FRACTION: float = 0.15  # last 15% of bars

# Confidence multiplier
_CONFIDENCE_MULTIPLIER: float = 0.90


class CupHandleEvaluator:
    """Cup & Handle pattern 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 ``30``.
    """

    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 Cup & Handle pattern on *bars* for *timeframe*.

        Returns ``None`` when there are fewer than :pyattr:`min_bars` bars,
        or when no valid cup & handle pattern is detected.
        """
        if not validate_lookback(bars, self.min_bars):
            return None

        n = len(bars)

        # --- Step 1: Find the left rim (highest high in first third) ---
        first_third_end = n // 3
        if first_third_end < 1:
            return None

        left_rim_idx = 0
        left_rim_price = bars[0].high
        for i in range(1, first_third_end):
            if bars[i].high > left_rim_price:
                left_rim_idx = i
                left_rim_price = bars[i].high

        if left_rim_price <= 0:
            return None

        # --- Step 2: Find the right rim (highest high in last third) ---
        last_third_start = n - (n // 3)
        if last_third_start >= n:
            return None

        right_rim_idx = last_third_start
        right_rim_price = bars[last_third_start].high
        for i in range(last_third_start + 1, n):
            if bars[i].high > right_rim_price:
                right_rim_idx = i
                right_rim_price = bars[i].high

        # --- Step 3: Find the cup bottom (lowest low between rims) ---
        search_start = left_rim_idx + 1
        search_end = right_rim_idx
        if search_start >= search_end:
            return None

        bottom_idx = search_start
        bottom_price = bars[search_start].low
        for i in range(search_start + 1, search_end):
            if bars[i].low < bottom_price:
                bottom_idx = i
                bottom_price = bars[i].low

        # --- Step 4: Validate cup depth ---
        cup_depth = left_rim_price - bottom_price
        if cup_depth <= 0:
            return None

        cup_depth_pct = cup_depth / left_rim_price
        if cup_depth_pct < _CUP_DEPTH_MIN or cup_depth_pct > _CUP_DEPTH_MAX:
            return None

        # --- Step 5: Score symmetry (left rim vs right rim) ---
        rim_diff_pct = abs(left_rim_price - right_rim_price) / left_rim_price
        if rim_diff_pct <= _SYMMETRY_PERFECT_PCT:
            symmetry_score = 1.0
        else:
            # Linear decay from 1.0 at 5% to 0.0 at 20%
            max_diff = 0.20
            symmetry_score = max(0.0, 1.0 - (rim_diff_pct - _SYMMETRY_PERFECT_PCT) / (max_diff - _SYMMETRY_PERFECT_PCT))

        # Right rim must be at least close to left rim (within 20%)
        if symmetry_score <= 0.0:
            return None

        # --- Step 6: Detect and score the handle ---
        handle_lookback = max(2, int(n * _HANDLE_LOOKBACK_FRACTION))
        handle_bars = bars[-handle_lookback:]

        # Handle is a small pullback from the right rim
        handle_low = min(b.low for b in handle_bars)
        handle_depth = right_rim_price - handle_low

        if cup_depth <= 0:
            return None

        handle_retrace = handle_depth / cup_depth

        if handle_retrace > _HANDLE_MAX_RETRACE:
            # Handle is too deep — not a valid cup & handle
            return None

        # Handle score: 1.0 when handle is very shallow, decreasing as it deepens
        if handle_retrace <= 0:
            handle_score = 1.0
        else:
            handle_score = 1.0 - (handle_retrace / _HANDLE_MAX_RETRACE)

        # --- Step 7: Cup depth quality score ---
        # Ideal cup depth is around 20-25% — score peaks in the middle of valid range
        ideal_depth = (_CUP_DEPTH_MIN + _CUP_DEPTH_MAX) / 2.0  # 0.225
        depth_deviation = abs(cup_depth_pct - ideal_depth) / ((_CUP_DEPTH_MAX - _CUP_DEPTH_MIN) / 2.0)
        depth_score = max(0.0, 1.0 - depth_deviation)

        # --- Step 8: Compute overall completeness ---
        completeness = (
            0.35 * symmetry_score
            + 0.35 * depth_score
            + 0.30 * handle_score
        )
        completeness = max(0.0, min(1.0, completeness))

        # --- Step 9: Build signal result ---
        strength = completeness
        confidence = completeness * _CONFIDENCE_MULTIPLIER

        return SignalResult(
            signal_type="cup_handle",
            timeframe=timeframe,
            strength=strength,
            direction=SignalDirection.BULLISH,
            confidence=confidence,
            metadata={
                "left_rim": left_rim_price,
                "left_rim_idx": left_rim_idx,
                "right_rim": right_rim_price,
                "right_rim_idx": right_rim_idx,
                "bottom": bottom_price,
                "bottom_idx": bottom_idx,
                "cup_depth_pct": round(cup_depth_pct, 4),
                "handle_depth": round(handle_depth, 4),
                "handle_retrace_pct": round(handle_retrace, 4),
                "symmetry_score": round(symmetry_score, 4),
                "depth_score": round(depth_score, 4),
                "handle_score": round(handle_score, 4),
                "completeness": round(completeness, 4),
            },
        )
