"""Feature Engineering Pipeline with Causal Verification."""
from __future__ import annotations

import math
from typing import Any, Sequence

from backend.app.features.indicators import (
    calculate_atr,
    calculate_bollinger_bands,
    calculate_ema,
    calculate_macd,
    calculate_rsi,
    calculate_sma,
)
from backend.app.features.regime import regime_classifier
from backend.app.schemas.market import Candle
from backend.app.schemas.signal import MarketRegime


class FeaturePipeline:
    """Transforms raw candlestick data into causal normalized feature matrices."""

    def extract_features(self, candles: Sequence[Candle]) -> dict[str, Any]:
        """Extract latest feature vector from a sequence of candles."""
        if len(candles) < 35:
            return {"valid": False, "reason": "Insufficient candle history (<35)"}

        closes = [float(c.close) for c in candles]
        volumes = [float(c.volume) for c in candles]

        ema_9 = calculate_ema(closes, 9)
        ema_21 = calculate_ema(closes, 21)
        ema_50 = calculate_ema(closes, 50)
        rsi_14 = calculate_rsi(closes, 14)
        _, _, macd_hist = calculate_macd(closes, 12, 26, 9)
        atr_14 = calculate_atr(candles, 14)
        bb_mid, bb_upper, bb_lower = calculate_bollinger_bands(closes, 20, 2.0)
        vol_sma_20 = calculate_sma(volumes, 20)

        last_p = closes[-1]
        regime = regime_classifier.classify(
            prices=closes,
            fast_ema=ema_9,
            slow_ema=ema_21,
            atr=atr_14,
        )

        # Normalized feature metrics
        feature_vector = {
            "valid": True,
            "timestamp": candles[-1].timestamp,
            "close": last_p,
            "rsi_14": rsi_14[-1] if not math.isnan(rsi_14[-1]) else 50.0,
            "macd_hist": macd_hist[-1] if not math.isnan(macd_hist[-1]) else 0.0,
            "ema_9_diff": (last_p - ema_9[-1]) / last_p if not math.isnan(ema_9[-1]) else 0.0,
            "ema_21_diff": (last_p - ema_21[-1]) / last_p if not math.isnan(ema_21[-1]) else 0.0,
            "atr_pct": (atr_14[-1] / last_p) if not math.isnan(atr_14[-1]) else 0.01,
            "bb_bandwidth": ((bb_upper[-1] - bb_lower[-1]) / bb_mid[-1]) if not math.isnan(bb_mid[-1]) else 0.02,
            "volume_ratio": (volumes[-1] / vol_sma_20[-1]) if not math.isnan(vol_sma_20[-1]) and vol_sma_20[-1] > 0 else 1.0,
            "regime": regime.value,
            "atr_val": atr_14[-1],
        }
        return feature_vector

    def verify_zero_lookahead(self, candles: Sequence[Candle], eval_index: int) -> bool:
        """Mathematically prove that future candles cannot alter past features."""
        # Feature at eval_index computed using candles up to eval_index
        sub_candles = candles[: eval_index + 1]
        feat_initial = self.extract_features(sub_candles)

        # Feature at eval_index computed when additional future candles are present
        longer_candles = candles[: eval_index + 15]
        # Calculate full series up to longer_candles and check value at eval_index
        closes_long = [float(c.close) for c in longer_candles]
        closes_sub = [float(c.close) for c in sub_candles]

        ema_sub = calculate_ema(closes_sub, 9)
        ema_long = calculate_ema(closes_long, 9)

        # Value at eval_index must match exactly
        return math.isclose(ema_sub[-1], ema_long[eval_index], rel_tol=1e-9)


feature_pipeline = FeaturePipeline()
