"""Random Forest signal classifier.

Input: AgentSignals + derived features → buy/sell/hold classification.
Labels derived from outcome: did price go up/down in next N hours?
Replaces hardcoded thresholds in strategies.
"""

import os
from pathlib import Path

import numpy as np

MODEL_DIR = Path(os.getenv("MODEL_DIR", "models"))

# Feature keys matching FeatureVector from Node.js side
FEATURE_KEYS = [
    "rsi", "macdHistogram", "bollingerPercentB", "ema12", "ema26",
    "atr", "obv", "fundingRate", "fearGreed", "priceChange24h",
    "rsiSlope", "volumeRatio", "emaCrossoverPct", "atrPct",
]


class SignalClassifier:
    def __init__(self):
        self.version = "0.1.0"
        self.is_loaded = False
        self.last_trained: str | None = None
        self.accuracy: float | None = None
        self.model = None
        self.xgb_models: dict = {}  # profile → XGBClassifier

    def load(self):
        """Load trained model: prefer XGBoost > Random Forest > heuristic."""
        # Try XGBoost direction models first (one per profile)
        self._load_xgboost_models()
        if self.xgb_models:
            self.is_loaded = True
            self.version = "0.2.0-xgboost"
            return

        # Fall back to Random Forest
        model_path = MODEL_DIR / "signal_classifier.joblib"
        if model_path.exists():
            try:
                import joblib
                self.model = joblib.load(model_path)
                self.is_loaded = True
                self.last_trained = str(model_path.stat().st_mtime)
            except Exception:
                self._init_heuristic()
        else:
            self._init_heuristic()

    def _load_xgboost_models(self):
        """Load XGBoost direction models for each horizon profile."""
        for profile in ("scalp", "standard", "position"):
            model_path = MODEL_DIR / f"xgb_direction_{profile}.json"
            if model_path.exists():
                try:
                    import xgboost as xgb
                    m = xgb.XGBClassifier()
                    m.load_model(str(model_path))
                    self.xgb_models[profile] = m
                    self.last_trained = str(model_path.stat().st_mtime)
                except Exception:
                    pass  # Skip this profile, fall through to RF or heuristic

    def _init_heuristic(self):
        self.is_loaded = True
        self.version = "0.1.0-heuristic"

    def predict(self, features: dict) -> dict:
        """Classify signals into buy/sell/hold.

        Returns:
            dict with keys: direction, probability, model
        """
        # Prefer XGBoost when available for the given horizon profile
        if self.xgb_models:
            return self._predict_xgboost(features)
        if self.model is not None:
            return self._predict_model(features)
        return self._predict_heuristic(features)

    def _predict_xgboost(self, features: dict) -> dict:
        """Run inference through trained XGBoost direction model."""
        horizon = features.get("horizon", "4h")
        # Determine profile from horizon
        scalp = {"5m", "15m", "30m"}
        position = {"1d", "7d"}
        if horizon in scalp:
            profile = "scalp"
        elif horizon in position:
            profile = "position"
        else:
            profile = "standard"

        model = self.xgb_models.get(profile) or self.xgb_models.get("standard")
        if model is None:
            return self._predict_heuristic(features)

        from ..training.train_direction import ALL_FEATURE_KEYS
        X = np.array([[features.get(k, 0) for k in ALL_FEATURE_KEYS]])
        proba = model.predict_proba(X)[0]
        idx = int(np.argmax(proba))
        direction_map = {0: "down", 1: "sideways", 2: "up"}
        return {
            "direction": direction_map.get(idx, "sideways"),
            "probability": float(proba[idx]),
            "model": f"xgb-direction-{profile}",
        }

    def _predict_model(self, features: dict) -> dict:
        """Run inference through trained Random Forest."""
        X = np.array([[features.get(k, 0) for k in FEATURE_KEYS]])
        proba = self.model.predict_proba(X)[0]
        classes = self.model.classes_
        idx = int(np.argmax(proba))
        direction_map = {"buy": "up", "sell": "down", "hold": "sideways"}
        direction = direction_map.get(classes[idx], "sideways")
        return {
            "direction": direction,
            "probability": float(proba[idx]),
            "model": f"signal-classifier-{self.version}",
        }

    def _predict_heuristic(self, features: dict) -> dict:
        """Rule-based classification until model is trained.

        Conservative by design: high direction threshold (±40) and capped
        probability (0.70) so the heuristic doesn't feed overconfident
        directional signal into the CF algebra ensemble.
        """
        rsi = features.get("rsi", 50)
        macd = features.get("macdHistogram", 0)
        bb_pct = features.get("bollingerPercentB", 0.5)
        funding = features.get("fundingRate", 0)
        fg = features.get("fearGreed", 50)
        rsi_slope = features.get("rsiSlope", 0)
        vol_ratio = features.get("volumeRatio", 1)
        ema_cross = features.get("emaCrossoverPct", 0)

        score = 0.0

        # RSI signal — only extreme values trigger
        if rsi < 25:
            score += 25
        elif rsi < 35:
            score += 10
        elif rsi > 75:
            score -= 25
        elif rsi > 65:
            score -= 10

        # MACD — capped contribution to ±15
        macd_contrib = max(-15, min(15, macd * 10))
        score += macd_contrib

        # Bollinger Bands — only extremes
        if bb_pct < 0.1:
            score += 12
        elif bb_pct > 0.9:
            score -= 12

        # Funding rate (contrarian)
        if funding > 0.0005:
            score -= 8
        elif funding < -0.0003:
            score += 8

        # Fear & Greed (contrarian at extremes only)
        if fg < 15:
            score += 8
        elif fg > 85:
            score -= 8

        # Momentum signals — reduced contribution
        score += rsi_slope * 1.2
        if vol_ratio > 2.5:
            score += 6 if score > 0 else -6
        score += ema_cross * 3

        # Flatter probability curve: divisor 300 → max ~0.70 at extreme scores
        probability = min(0.70, max(0.3, 0.5 + abs(score) / 300))

        # Higher direction threshold: ±40 → more sideways classifications
        if score > 40:
            return {"direction": "up", "probability": probability, "model": "rf-heuristic"}
        elif score < -40:
            return {"direction": "down", "probability": probability, "model": "rf-heuristic"}
        return {"direction": "sideways", "probability": probability, "model": "rf-heuristic"}
