"""
Prediction Service
==================

Service that orchestrates ML predictions using real market data.
Connects Data Service, Feature Engineering, and ML Models.
"""

import os
import asyncio
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any, Tuple
from dataclasses import dataclass, asdict
from enum import Enum
import uuid
import pandas as pd
import numpy as np
from pathlib import Path
from loguru import logger

# Feature flags y configuración centralizada
from ..config.feature_flags import FeatureFlags
from ..training.symbol_timeframe_trainer import SymbolTimeframeTrainer, SYMBOL_CONFIGS

# Data imports
from ..data.data_service_client import (
    DataServiceManager,
    DataServiceClient,
    Timeframe
)
from ..data.features import FeatureEngineer
from ..data.indicators import TechnicalIndicators


class Direction(Enum):
    LONG = "long"
    SHORT = "short"
    NEUTRAL = "neutral"


class AMDPhase(Enum):
    ACCUMULATION = "accumulation"
    MANIPULATION = "manipulation"
    DISTRIBUTION = "distribution"
    UNKNOWN = "unknown"


class VolatilityRegime(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    EXTREME = "extreme"


@dataclass
class RangePrediction:
    """Range prediction result"""
    horizon: str
    delta_high: float
    delta_low: float
    delta_high_bin: Optional[int]
    delta_low_bin: Optional[int]
    confidence_high: float
    confidence_low: float


@dataclass
class TPSLPrediction:
    """TP/SL classification result"""
    prob_tp_first: float
    rr_config: str
    confidence: float
    calibrated: bool


@dataclass
class TradingSignal:
    """Complete trading signal"""
    signal_id: str
    symbol: str
    direction: Direction
    entry_price: float
    stop_loss: float
    take_profit: float
    risk_reward_ratio: float
    prob_tp_first: float
    confidence_score: float
    amd_phase: AMDPhase
    volatility_regime: VolatilityRegime
    range_prediction: RangePrediction
    timestamp: datetime
    valid_until: datetime
    metadata: Optional[Dict[str, Any]] = None


@dataclass
class AMDDetection:
    """AMD phase detection result"""
    phase: AMDPhase
    confidence: float
    start_time: datetime
    characteristics: Dict[str, float]
    signals: List[str]
    strength: float
    trading_bias: Dict[str, Any]


class PredictionService:
    """
    Main prediction service.

    Orchestrates:
    - Data fetching from Data Service
    - Feature engineering
    - Model inference
    - Signal generation
    """

    def __init__(
        self,
        data_service_url: Optional[str] = None,
        models_dir: str = "models"
    ):
        """
        Initialize prediction service.

        Args:
            data_service_url: URL of Data Service
            models_dir: Directory containing trained models
        """
        self.data_manager = DataServiceManager(
            DataServiceClient(base_url=data_service_url)
        )
        self.models_dir = models_dir
        self.feature_engineer = FeatureEngineer()
        self.indicators = TechnicalIndicators()

        # Model instances (loaded on demand)
        self._range_predictor = None
        self._tpsl_classifier = None
        self._amd_detector = None
        self._models_loaded = False

        # Symbol-specific trainers (nuevos modelos por símbolo/timeframe)
        self._symbol_trainers: Dict[str, SymbolTimeframeTrainer] = {}

        # Supported configurations
        self.supported_symbols = ["XAUUSD", "EURUSD", "GBPUSD", "BTCUSD", "ETHUSD"]
        self.supported_horizons = ["15m", "1h", "4h"]
        self.supported_rr_configs = ["rr_2_1", "rr_3_1"]

    async def initialize(self):
        """Load models and prepare service"""
        logger.info("Initializing PredictionService...")

        # Try to load models
        await self._load_models()

        logger.info("PredictionService initialized")

    async def _load_models(self):
        """Load ML models from disk"""
        try:
            # Import model classes
            from ..models.range_predictor import RangePredictor
            from ..models.tp_sl_classifier import TPSLClassifier
            from ..models.amd_detector import AMDDetector

            # Load Range Predictor
            range_path = os.path.join(self.models_dir, "range_predictor")
            if os.path.exists(range_path):
                self._range_predictor = RangePredictor()
                self._range_predictor.load(range_path)
                logger.info("✅ RangePredictor loaded")

            # Load TPSL Classifier
            tpsl_path = os.path.join(self.models_dir, "tpsl_classifier")
            if os.path.exists(tpsl_path):
                self._tpsl_classifier = TPSLClassifier()
                self._tpsl_classifier.load(tpsl_path)
                logger.info("✅ TPSLClassifier loaded")

            # Initialize AMD Detector (doesn't need pre-trained weights)
            self._amd_detector = AMDDetector()
            logger.info("✅ AMDDetector initialized")

            self._models_loaded = True

            # Cargar modelos por símbolo si el feature flag está activo
            if FeatureFlags.USE_SYMBOL_TRAINERS:
                self._load_symbol_trainers()

        except ImportError as e:
            logger.warning(f"Model import failed: {e}")
            self._models_loaded = False
        except Exception as e:
            logger.error(f"Model loading failed: {e}")
            self._models_loaded = False

    def _load_symbol_trainers(self):
        """
        Cargar modelos entrenados por símbolo desde múltiples directorios.

        Busca en los siguientes directorios (en orden de prioridad):
        1. models/symbol_timeframe_models/ (nuevos modelos con normalización ATR)
        2. models/ml_first/ (modelos legacy)

        Estructura esperada:
        models/symbol_timeframe_models/
        ├── XAUUSD_5m_high_h3.joblib
        ├── XAUUSD_5m_low_h3.joblib
        ├── trainer_metadata.joblib
        └── ...
        """
        # Lista de directorios a buscar (en orden de prioridad)
        model_dirs = [
            Path(self.models_dir) / 'symbol_timeframe_models',  # Nuevos modelos
            Path(self.models_dir) / 'ml_first',  # Modelos legacy
        ]

        loaded_count = 0

        for model_path in model_dirs:
            if not model_path.exists():
                logger.debug(f"Model directory not found: {model_path}")
                continue

            logger.info(f"Loading models from {model_path}")

            # Check if it's a flat structure (symbol_timeframe_models) or hierarchical (ml_first)
            metadata_path = model_path / 'trainer_metadata.joblib'

            if metadata_path.exists():
                # Flat structure: Load single trainer with all models
                try:
                    trainer = SymbolTimeframeTrainer()
                    trainer.load(str(model_path))

                    # Register trainer for all symbols it contains
                    for key in trainer.models.keys():
                        symbol = key.split('_')[0]
                        if symbol not in self._symbol_trainers:
                            self._symbol_trainers[symbol] = trainer
                            loaded_count += 1
                            logger.info(f"✅ Loaded symbol trainer for {symbol} from {model_path.name}")

                    # If we loaded models successfully, stop searching
                    if loaded_count > 0:
                        logger.info(f"Loaded {loaded_count} symbol trainers from {model_path}")
                        return

                except Exception as e:
                    logger.warning(f"Failed to load trainer from {model_path}: {e}")
                    continue
            else:
                # Hierarchical structure: Load from symbol subdirectories
                for symbol_dir in model_path.iterdir():
                    if not symbol_dir.is_dir():
                        continue

                    symbol = symbol_dir.name
                    if symbol not in SYMBOL_CONFIGS:
                        logger.debug(f"Skipping unknown symbol: {symbol}")
                        continue

                    if symbol in self._symbol_trainers:
                        # Already loaded from higher priority directory
                        continue

                    try:
                        trainer = SymbolTimeframeTrainer()
                        trainer.load(str(symbol_dir))
                        self._symbol_trainers[symbol] = trainer
                        loaded_count += 1
                        logger.info(f"✅ Loaded symbol trainer for {symbol} from {model_path.name}")
                    except Exception as e:
                        logger.warning(f"Failed to load trainer for {symbol}: {e}")

        if loaded_count == 0:
            logger.warning("No symbol-specific trainers loaded - using heuristic predictions")
        else:
            logger.info(f"Total loaded: {loaded_count} symbol-specific trainers")

    @property
    def models_loaded(self) -> bool:
        return self._models_loaded

    async def get_market_data(
        self,
        symbol: str,
        timeframe: str = "15m",
        lookback_periods: int = 500
    ) -> pd.DataFrame:
        """
        Get market data with features.

        Args:
            symbol: Trading symbol
            timeframe: Timeframe string
            lookback_periods: Number of periods

        Returns:
            DataFrame with OHLCV and features
        """
        tf = Timeframe(timeframe)

        async with self.data_manager.client:
            df = await self.data_manager.get_ml_features_data(
                symbol=symbol,
                timeframe=tf,
                lookback_periods=lookback_periods
            )

        if df.empty:
            logger.warning(f"No data available for {symbol}")
            return df

        # Add technical indicators
        df = self.indicators.add_all_indicators(df)

        return df

    async def predict_range(
        self,
        symbol: str,
        timeframe: str = "15m",
        horizons: Optional[List[str]] = None
    ) -> List[RangePrediction]:
        """
        Predict price ranges.

        Args:
            symbol: Trading symbol
            timeframe: Analysis timeframe
            horizons: Prediction horizons

        Returns:
            List of range predictions
        """
        horizons = horizons or self.supported_horizons[:2]

        # Get market data
        df = await self.get_market_data(symbol, timeframe)

        if df.empty:
            # Return default predictions
            return self._default_range_predictions(horizons)

        predictions = []

        for horizon in horizons:
            # Generate features
            features = self.feature_engineer.create_features(df)
            prediction_made = False

            # Prioridad 1: Usar symbol-specific trainer si está disponible
            if FeatureFlags.USE_SYMBOL_TRAINERS and symbol in self._symbol_trainers:
                try:
                    trainer = self._symbol_trainers[symbol]
                    pred = trainer.predict(df, symbol, timeframe)
                    logger.debug(f"Using symbol-specific trainer for {symbol}")
                    predictions.append(RangePrediction(
                        horizon=horizon,
                        delta_high=pred.get("delta_high", 0),
                        delta_low=pred.get("delta_low", 0),
                        delta_high_bin=pred.get("delta_high_bin"),
                        delta_low_bin=pred.get("delta_low_bin"),
                        confidence_high=pred.get("confidence_high", 0.7),
                        confidence_low=pred.get("confidence_low", 0.7)
                    ))
                    prediction_made = True
                except Exception as e:
                    logger.warning(f"Symbol trainer failed for {symbol}, falling back to legacy: {e}")

            # Prioridad 2: Usar modelo legacy
            if not prediction_made and self._range_predictor:
                # Use trained model
                pred = self._range_predictor.predict(features, horizon)
                predictions.append(RangePrediction(
                    horizon=horizon,
                    delta_high=pred.get("delta_high", 0),
                    delta_low=pred.get("delta_low", 0),
                    delta_high_bin=pred.get("delta_high_bin"),
                    delta_low_bin=pred.get("delta_low_bin"),
                    confidence_high=pred.get("confidence_high", 0.5),
                    confidence_low=pred.get("confidence_low", 0.5)
                ))
                prediction_made = True

            # Prioridad 3: Heurística basada en ATR
            if not prediction_made:
                # Heuristic-based prediction using ATR
                atr = df['atr'].iloc[-1] if 'atr' in df.columns else df['high'].iloc[-1] - df['low'].iloc[-1]
                multiplier = {"15m": 1.0, "1h": 1.5, "4h": 2.5}.get(horizon, 1.0)

                predictions.append(RangePrediction(
                    horizon=horizon,
                    delta_high=float(atr * multiplier * 0.8),
                    delta_low=float(atr * multiplier * 0.6),
                    delta_high_bin=None,
                    delta_low_bin=None,
                    confidence_high=0.6,
                    confidence_low=0.55
                ))

        return predictions

    async def predict_tpsl(
        self,
        symbol: str,
        timeframe: str = "15m",
        rr_config: str = "rr_2_1"
    ) -> TPSLPrediction:
        """
        Predict TP/SL probability.

        Args:
            symbol: Trading symbol
            timeframe: Analysis timeframe
            rr_config: Risk/Reward configuration

        Returns:
            TP/SL prediction
        """
        df = await self.get_market_data(symbol, timeframe)

        if df.empty or not self._tpsl_classifier:
            # Heuristic based on trend
            if not df.empty:
                sma_short = df['close'].rolling(10).mean().iloc[-1]
                sma_long = df['close'].rolling(20).mean().iloc[-1]
                trend_strength = (sma_short - sma_long) / sma_long

                prob = 0.5 + (trend_strength * 10)  # Adjust based on trend
                prob = max(0.3, min(0.7, prob))
            else:
                prob = 0.5

            return TPSLPrediction(
                prob_tp_first=prob,
                rr_config=rr_config,
                confidence=0.5,
                calibrated=False
            )

        # Use trained model
        features = self.feature_engineer.create_features(df)
        pred = self._tpsl_classifier.predict(features, rr_config)

        return TPSLPrediction(
            prob_tp_first=pred.get("prob_tp_first", 0.5),
            rr_config=rr_config,
            confidence=pred.get("confidence", 0.5),
            calibrated=pred.get("calibrated", False)
        )

    async def detect_amd_phase(
        self,
        symbol: str,
        timeframe: str = "15m",
        lookback_periods: int = 100
    ) -> AMDDetection:
        """
        Detect AMD phase.

        Args:
            symbol: Trading symbol
            timeframe: Analysis timeframe
            lookback_periods: Periods for analysis

        Returns:
            AMD phase detection
        """
        df = await self.get_market_data(symbol, timeframe, lookback_periods)

        if df.empty:
            return self._default_amd_detection()

        if self._amd_detector:
            # Use AMD detector
            detection = self._amd_detector.detect_phase(df)
            bias = self._amd_detector.get_trading_bias(detection.get("phase", "unknown"))

            return AMDDetection(
                phase=AMDPhase(detection.get("phase", "unknown")),
                confidence=detection.get("confidence", 0.5),
                start_time=datetime.utcnow(),
                characteristics=detection.get("characteristics", {}),
                signals=detection.get("signals", []),
                strength=detection.get("strength", 0.5),
                trading_bias=bias
            )

        # Heuristic AMD detection
        return self._heuristic_amd_detection(df)

    async def generate_signal(
        self,
        symbol: str,
        timeframe: str = "15m",
        rr_config: str = "rr_2_1"
    ) -> TradingSignal:
        """
        Generate complete trading signal.

        Args:
            symbol: Trading symbol
            timeframe: Analysis timeframe
            rr_config: Risk/Reward configuration

        Returns:
            Complete trading signal
        """
        # Get all predictions in parallel
        range_preds, tpsl_pred, amd_detection = await asyncio.gather(
            self.predict_range(symbol, timeframe, ["15m"]),
            self.predict_tpsl(symbol, timeframe, rr_config),
            self.detect_amd_phase(symbol, timeframe)
        )

        range_pred = range_preds[0] if range_preds else self._default_range_predictions(["15m"])[0]

        # Get current price
        current_price = await self.data_manager.get_latest_price(symbol)
        if not current_price:
            df = await self.get_market_data(symbol, timeframe, 10)
            current_price = df['close'].iloc[-1] if not df.empty else 0

        # Determine direction based on AMD phase and predictions
        direction = self._determine_direction(amd_detection, tpsl_pred)

        # Calculate entry, SL, TP
        entry, sl, tp = self._calculate_levels(
            current_price,
            direction,
            range_pred,
            rr_config
        )

        # Calculate confidence score
        confidence = self._calculate_confidence(
            range_pred,
            tpsl_pred,
            amd_detection
        )

        # Determine volatility regime
        volatility = self._determine_volatility(range_pred)

        now = datetime.utcnow()
        validity_minutes = {"15m": 15, "1h": 60, "4h": 240}.get(timeframe, 15)

        return TradingSignal(
            signal_id=f"SIG-{uuid.uuid4().hex[:8].upper()}",
            symbol=symbol,
            direction=direction,
            entry_price=entry,
            stop_loss=sl,
            take_profit=tp,
            risk_reward_ratio=float(rr_config.split("_")[1]),
            prob_tp_first=tpsl_pred.prob_tp_first,
            confidence_score=confidence,
            amd_phase=amd_detection.phase,
            volatility_regime=volatility,
            range_prediction=range_pred,
            timestamp=now,
            valid_until=now + timedelta(minutes=validity_minutes),
            metadata={
                "timeframe": timeframe,
                "rr_config": rr_config,
                "amd_signals": amd_detection.signals
            }
        )

    def _determine_direction(
        self,
        amd: AMDDetection,
        tpsl: TPSLPrediction
    ) -> Direction:
        """Determine trade direction based on analysis"""
        bias = amd.trading_bias.get("direction", "neutral")

        if bias == "long" and tpsl.prob_tp_first > 0.55:
            return Direction.LONG
        elif bias == "short" and tpsl.prob_tp_first > 0.55:
            return Direction.SHORT

        # Default based on AMD phase
        phase_bias = {
            AMDPhase.ACCUMULATION: Direction.LONG,
            AMDPhase.MANIPULATION: Direction.NEUTRAL,
            AMDPhase.DISTRIBUTION: Direction.SHORT,
            AMDPhase.UNKNOWN: Direction.NEUTRAL
        }

        return phase_bias.get(amd.phase, Direction.NEUTRAL)

    def _calculate_levels(
        self,
        current_price: float,
        direction: Direction,
        range_pred: RangePrediction,
        rr_config: str
    ) -> Tuple[float, float, float]:
        """Calculate entry, SL, TP levels"""
        rr_ratio = float(rr_config.split("_")[1])

        if direction == Direction.LONG:
            entry = current_price
            sl = current_price - range_pred.delta_low
            tp = current_price + (range_pred.delta_low * rr_ratio)
        elif direction == Direction.SHORT:
            entry = current_price
            sl = current_price + range_pred.delta_high
            tp = current_price - (range_pred.delta_high * rr_ratio)
        else:
            entry = current_price
            sl = current_price - range_pred.delta_low
            tp = current_price + range_pred.delta_high

        return round(entry, 2), round(sl, 2), round(tp, 2)

    def _calculate_confidence(
        self,
        range_pred: RangePrediction,
        tpsl: TPSLPrediction,
        amd: AMDDetection
    ) -> float:
        """Calculate overall confidence score"""
        weights = {"range": 0.3, "tpsl": 0.4, "amd": 0.3}

        range_conf = (range_pred.confidence_high + range_pred.confidence_low) / 2
        tpsl_conf = tpsl.confidence
        amd_conf = amd.confidence

        confidence = (
            weights["range"] * range_conf +
            weights["tpsl"] * tpsl_conf +
            weights["amd"] * amd_conf
        )

        return round(confidence, 3)

    def _determine_volatility(self, range_pred: RangePrediction) -> VolatilityRegime:
        """Determine volatility regime from range prediction"""
        avg_delta = (range_pred.delta_high + range_pred.delta_low) / 2

        # Thresholds (adjust based on asset)
        if avg_delta < 5:
            return VolatilityRegime.LOW
        elif avg_delta < 15:
            return VolatilityRegime.MEDIUM
        elif avg_delta < 30:
            return VolatilityRegime.HIGH
        else:
            return VolatilityRegime.EXTREME

    def _default_range_predictions(self, horizons: List[str]) -> List[RangePrediction]:
        """Return default range predictions"""
        return [
            RangePrediction(
                horizon=h,
                delta_high=10.0 * (i + 1),
                delta_low=8.0 * (i + 1),
                delta_high_bin=None,
                delta_low_bin=None,
                confidence_high=0.5,
                confidence_low=0.5
            )
            for i, h in enumerate(horizons)
        ]

    def _default_amd_detection(self) -> AMDDetection:
        """Return default AMD detection"""
        return AMDDetection(
            phase=AMDPhase.UNKNOWN,
            confidence=0.5,
            start_time=datetime.utcnow(),
            characteristics={},
            signals=[],
            strength=0.5,
            trading_bias={"direction": "neutral"}
        )

    def _heuristic_amd_detection(self, df: pd.DataFrame) -> AMDDetection:
        """Heuristic AMD detection using price action"""
        # Analyze recent price action
        recent = df.tail(20)
        older = df.tail(50).head(30)

        recent_range = recent['high'].max() - recent['low'].min()
        older_range = older['high'].max() - older['low'].min()
        range_compression = recent_range / older_range if older_range > 0 else 1

        # Volume analysis
        recent_vol = recent['volume'].mean() if 'volume' in recent.columns else 1
        older_vol = older['volume'].mean() if 'volume' in older.columns else 1
        vol_ratio = recent_vol / older_vol if older_vol > 0 else 1

        # Determine phase
        if range_compression < 0.5 and vol_ratio < 0.8:
            phase = AMDPhase.ACCUMULATION
            signals = ["range_compression", "low_volume"]
            bias = {"direction": "long", "position_size": 0.7}
        elif range_compression > 1.2 and vol_ratio > 1.2:
            phase = AMDPhase.MANIPULATION
            signals = ["range_expansion", "high_volume"]
            bias = {"direction": "neutral", "position_size": 0.3}
        elif vol_ratio > 1.5:
            phase = AMDPhase.DISTRIBUTION
            signals = ["high_volume", "potential_distribution"]
            bias = {"direction": "short", "position_size": 0.6}
        else:
            phase = AMDPhase.UNKNOWN
            signals = []
            bias = {"direction": "neutral", "position_size": 0.5}

        return AMDDetection(
            phase=phase,
            confidence=0.6,
            start_time=datetime.utcnow(),
            characteristics={
                "range_compression": range_compression,
                "volume_ratio": vol_ratio
            },
            signals=signals,
            strength=0.6,
            trading_bias=bias
        )


# Singleton instance
_prediction_service: Optional[PredictionService] = None


def get_prediction_service() -> PredictionService:
    """Get or create prediction service singleton"""
    global _prediction_service
    if _prediction_service is None:
        _prediction_service = PredictionService()
    return _prediction_service


async def initialize_prediction_service():
    """Initialize the prediction service"""
    service = get_prediction_service()
    await service.initialize()
    return service
