"""Advanced factors for OpenClaw Trading.

This module provides paid advanced trading factors that require purchase.
These include machine learning-based prediction, sentiment analysis,
and sophisticated multi-factor combinations.
"""

from typing import Any, Optional

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler

from openclaw.factor.base import BuyFactor, Factor, SelectFactor, SellFactor
from openclaw.factor.types import (
    FactorCategory,
    FactorContext,
    FactorResult,
    FactorSignal,
    FactorType,
)
from openclaw.indicators.technical import ema, macd, rsi, sma


class MachineLearningFactor(BuyFactor):
    """Machine learning-based prediction factor.

    Uses a Random Forest classifier trained on technical indicators
    to predict price direction. This factor learns from historical
    patterns and adapts to market conditions.

    Parameters:
        lookback_period: Period for training data (default: 60)
        prediction_horizon: Bars ahead to predict (default: 5)
        min_samples: Minimum samples required for training (default: 30)
        model_params: RandomForest parameters
    """

    DEFAULT_PRICE = 100.0

    def __init__(
        self,
        parameters: Optional[dict[str, Any]] = None,
    ):
        super().__init__(
            name="ML Prediction",
            description="Machine learning-based price direction prediction",
            category=FactorCategory.ADVANCED,
            price=self.DEFAULT_PRICE,
            parameters=parameters,
        )
        self.lookback_period = self.parameters.get("lookback_period", 60)
        self.prediction_horizon = self.parameters.get("prediction_horizon", 5)
        self.min_samples = self.parameters.get("min_samples", 30)
        self.model_params = self.parameters.get(
            "model_params",
            {"n_estimators": 50, "max_depth": 5, "random_state": 42},
        )

        # ML components
        self._model: Optional[RandomForestClassifier] = None
        self._scaler = StandardScaler()
        self._is_trained = False

    def on_init(self) -> None:
        """Initialize ML model."""
        self._model = RandomForestClassifier(**self.model_params)

    def _extract_features(self, data: pd.DataFrame) -> np.ndarray:
        """Extract features from market data.

        Args:
            data: Market data DataFrame

        Returns:
            Feature array
        """
        close = data["close"]
        high = data["high"]
        low = data["low"]
        volume = data.get("volume", pd.Series([1] * len(data), index=data.index))

        features = []

        # Price-based features
        returns = close.pct_change().fillna(0)
        features.append(returns.iloc[-1])
        features.append(returns.iloc[-5:].mean() if len(returns) >= 5 else 0)
        features.append(returns.iloc[-10:].std() if len(returns) >= 10 else 0)

        # Technical indicators
        rsi_val = rsi(close, 14).iloc[-1] / 100.0
        features.append(rsi_val if not pd.isna(rsi_val) else 0.5)

        # Moving average positions
        sma_10 = sma(close, 10).iloc[-1]
        sma_30 = sma(close, 30).iloc[-1]
        features.append(
            (close.iloc[-1] - sma_10) / sma_10 if sma_10 > 0 else 0
        )
        features.append(
            (close.iloc[-1] - sma_30) / sma_30 if sma_30 > 0 else 0
        )

        # MACD
        macd_data = macd(close)
        features.append(
            macd_data["histogram"].iloc[-1] / close.iloc[-1]
            if not pd.isna(macd_data["histogram"].iloc[-1])
            else 0
        )

        # Volatility
        atr = (high - low).rolling(14).mean().iloc[-1]
        features.append(atr / close.iloc[-1] if not pd.isna(atr) else 0)

        # Volume features
        vol_sma = volume.rolling(20).mean().iloc[-1]
        features.append(
            volume.iloc[-1] / vol_sma if vol_sma > 0 else 1
        )

        return np.array(features).reshape(1, -1)

    def _train_model(self, data: pd.DataFrame) -> None:
        """Train the ML model on historical data.

        Args:
            data: Historical market data
        """
        if len(data) < self.lookback_period + self.prediction_horizon:
            return

        close = data["close"]
        X_list = []
        y_list = []

        # Generate training samples
        for i in range(
            self.lookback_period, len(data) - self.prediction_horizon
        ):
            window = data.iloc[i - self.lookback_period : i]
            features = self._extract_features(window).flatten()
            X_list.append(features)

            # Target: 1 if price goes up, 0 if down
            future_return = (
                close.iloc[i + self.prediction_horizon] - close.iloc[i]
            ) / close.iloc[i]
            y_list.append(1 if future_return > 0 else 0)

        if len(X_list) < self.min_samples:
            return

        X = np.array(X_list)
        y = np.array(y_list)

        # Scale features
        X_scaled = self._scaler.fit_transform(X)

        # Train model
        self._model.fit(X_scaled, y)
        self._is_trained = True

    def on_evaluate(self, context: FactorContext) -> Optional[FactorResult]:
        """Evaluate using ML prediction.

        Args:
            context: Factor context with market data

        Returns:
            FactorResult if prediction confidence is high, None otherwise
        """
        if not self._is_trained:
            self._train_model(context.data)
            return None

        # Extract features from recent data
        recent_data = context.data.iloc[-self.lookback_period :]
        if len(recent_data) < self.lookback_period:
            return None

        features = self._extract_features(recent_data)
        features_scaled = self._scaler.transform(features)

        # Predict
        prediction = self._model.predict(features_scaled)[0]
        proba = self._model.predict_proba(features_scaled)[0]
        confidence = max(proba)

        # Only generate signal if confidence is high enough
        if prediction == 1 and confidence > 0.6:
            return FactorResult(
                signal=FactorSignal.BUY,
                confidence=min(confidence, 0.95),
                symbol=context.symbol,
                metadata={
                    "prediction": "up",
                    "confidence": confidence,
                    "model_trained": self._is_trained,
                    "factor_name": self.metadata.name,
                },
            )

        return None


class SentimentMomentumFactor(BuyFactor):
    """Sentiment and momentum-based factor.

    Combines price momentum with market sentiment indicators to
    identify high-probability entry points. Simulates sentiment
    using volume and price action analysis.

    Parameters:
        momentum_period: Period for momentum calculation (default: 20)
        volume_threshold: Volume spike threshold multiplier (default: 2.0)
        sentiment_window: Window for sentiment analysis (default: 10)
    """

    DEFAULT_PRICE = 75.0

    def __init__(
        self,
        parameters: Optional[dict[str, Any]] = None,
    ):
        super().__init__(
            name="Sentiment Momentum",
            description="Combines price momentum with market sentiment",
            category=FactorCategory.ADVANCED,
            price=self.DEFAULT_PRICE,
            parameters=parameters,
        )
        self.momentum_period = self.parameters.get("momentum_period", 20)
        self.volume_threshold = self.parameters.get("volume_threshold", 2.0)
        self.sentiment_window = self.parameters.get("sentiment_window", 10)

    def _calculate_momentum(self, data: pd.DataFrame) -> float:
        """Calculate price momentum score.

        Args:
            data: Market data

        Returns:
            Momentum score (-1 to 1)
        """
        close = data["close"]

        # Price momentum
        returns = close.pct_change(self.momentum_period).iloc[-1]
        momentum = np.tanh(returns * 10)  # Normalize to -1 to 1

        return momentum

    def _calculate_sentiment(self, data: pd.DataFrame) -> float:
        """Calculate market sentiment from price/volume action.

        Args:
            data: Market data

        Returns:
            Sentiment score (-1 to 1)
        """
        close = data["close"]
        high = data["high"]
        low = data["low"]
        volume = data.get("volume", pd.Series([1] * len(data), index=data.index))

        window = min(self.sentiment_window, len(data))

        # Volume sentiment (volume spikes indicate interest)
        vol_ma = volume.rolling(window).mean().iloc[-1]
        vol_current = volume.iloc[-1]
        vol_sentiment = (
            (vol_current / vol_ma - 1) / self.volume_threshold
            if vol_ma > 0
            else 0
        )
        vol_sentiment = np.clip(vol_sentiment, -1, 1)

        # Price action sentiment
        # Bullish: close near high, Bearish: close near low
        recent = data.iloc[-window:]
        bullish_candles = sum(
            recent["close"] > (recent["high"] + recent["low"]) / 2
        )
        candle_sentiment = (bullish_candles / window - 0.5) * 2

        # Combine sentiments
        sentiment = 0.6 * vol_sentiment + 0.4 * candle_sentiment

        return np.clip(sentiment, -1, 1)

    def on_evaluate(self, context: FactorContext) -> Optional[FactorResult]:
        """Evaluate sentiment and momentum.

        Args:
            context: Factor context with market data

        Returns:
            FactorResult if both momentum and sentiment are positive
        """
        if len(context.data) < self.momentum_period + self.sentiment_window:
            return None

        momentum = self._calculate_momentum(context.data)
        sentiment = self._calculate_sentiment(context.data)

        # Require both positive momentum and sentiment
        if momentum > 0.2 and sentiment > 0.2:
            # Combined confidence
            combined = (momentum + sentiment) / 2
            confidence = min(0.6 + combined * 0.3, 0.9)

            return FactorResult(
                signal=FactorSignal.BUY,
                confidence=confidence,
                symbol=context.symbol,
                metadata={
                    "momentum": momentum,
                    "sentiment": sentiment,
                    "combined_score": combined,
                    "factor_name": self.metadata.name,
                },
            )

        return None


class MultiFactorCombination(BuyFactor):
    """Multi-factor combination using weighted ensemble.

    Combines multiple individual factors using a weighted voting
    mechanism. Weights are dynamically adjusted based on recent
    performance of each sub-factor.

    Parameters:
        factors: List of factor configurations
        min_agreement: Minimum factor agreement required (default: 0.5)
        lookback: Period for weight adjustment (default: 20)
    """

    DEFAULT_PRICE = 150.0

    def __init__(
        self,
        parameters: Optional[dict[str, Any]] = None,
    ):
        super().__init__(
            name="Multi-Factor Ensemble",
            description="Weighted combination of multiple factors",
            category=FactorCategory.PREMIUM,
            price=self.DEFAULT_PRICE,
            parameters=parameters,
        )
        self.min_agreement = self.parameters.get("min_agreement", 0.5)
        self.lookback = self.parameters.get("lookback", 20)

        # Sub-factors
        self._factors: list[Factor] = []
        self._weights: list[float] = []
        self._factor_performance: dict[str, list[float]] = {}

    def on_init(self) -> None:
        """Initialize sub-factors."""
        from openclaw.factor.basic import (
            MACDCrossoverFactor,
            MovingAverageCrossoverFactor,
            RSIOversoldFactor,
        )

        # Create default sub-factors
        self._factors = [
            MovingAverageCrossoverFactor(
                {"fast_period": 5, "slow_period": 20}
            ),
            RSIOversoldFactor({"period": 14, "oversold_threshold": 30}),
            MACDCrossoverFactor(),
        ]

        # Initialize equal weights
        self._weights = [1.0 / len(self._factors)] * len(self._factors)

        # Initialize performance tracking
        for factor in self._factors:
            self._factor_performance[factor.id] = []
            factor.initialize()
            factor.unlock()

    def _update_weights(self) -> None:
        """Update factor weights based on recent performance."""
        if not self._factor_performance:
            return

        # Calculate average performance for each factor
        avg_performances = []
        for factor in self._factors:
            perf = self._factor_performance.get(factor.id, [])
            if perf:
                # Use recent performance only
                recent_perf = perf[-self.lookback :]
                avg_performances.append(sum(recent_perf) / len(recent_perf))
            else:
                avg_performances.append(0.5)  # Neutral default

        # Normalize to weights
        total = sum(avg_performances)
        if total > 0:
            self._weights = [p / total for p in avg_performances]
        else:
            self._weights = [1.0 / len(self._factors)] * len(self._factors)

    def on_evaluate(self, context: FactorContext) -> Optional[FactorResult]:
        """Evaluate using multi-factor ensemble.

        Args:
            context: Factor context with market data

        Returns:
            FactorResult if ensemble confidence is high, None otherwise
        """
        if not self._factors:
            return None

        # Collect signals from all sub-factors
        signals = []
        confidences = []

        for factor in self._factors:
            try:
                result = factor.evaluate(context)
                if result:
                    signals.append(1 if result.signal == FactorSignal.BUY else 0)
                    confidences.append(result.confidence)
                else:
                    signals.append(0)
                    confidences.append(0)
            except Exception:
                signals.append(0)
                confidences.append(0)

        # Calculate weighted agreement
        weighted_agreement = sum(
            s * w for s, w in zip(signals, self._weights)
        )

        # Calculate weighted confidence
        weighted_confidence = sum(
            c * w for c, w in zip(confidences, self._weights)
        )

        # Require minimum agreement and confidence
        if (
            weighted_agreement >= self.min_agreement
            and weighted_confidence > 0.55
        ):
            return FactorResult(
                signal=FactorSignal.BUY,
                confidence=min(weighted_confidence + 0.1, 0.95),
                symbol=context.symbol,
                metadata={
                    "weighted_agreement": weighted_agreement,
                    "weighted_confidence": weighted_confidence,
                    "factor_signals": {
                        f.metadata.name: s
                        for f, s in zip(self._factors, signals)
                    },
                    "weights": {
                        f.metadata.name: w
                        for f, w in zip(self._factors, self._weights)
                    },
                    "factor_name": self.metadata.name,
                },
            )

        return None

    def reset(self) -> None:
        """Reset ensemble and sub-factors."""
        super().reset()
        for factor in self._factors:
            factor.reset()
        self._weights = [1.0 / len(self._factors)] * len(self._factors)
        self._factor_performance.clear()
