#!/usr/bin/env python3
"""
Signal Formatter
================

Formats ML predictions from the metamodel and individual strategies
into a standardized format suitable for LLM consumption.

This module bridges the gap between raw ML outputs and the structured
format expected by the LLM trading agent.

Key Functions:
- Format metamodel predictions (direction, magnitude, confidence)
- Format individual strategy signals
- Add risk context from account information
- Serialize to dictionary for API transport

Author: ML-Specialist (NEXUS v4.0)
Version: 1.0.0
Created: 2026-01-25
"""

import numpy as np
from datetime import datetime
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, field, asdict
from loguru import logger


# ============================================================
# DATA CLASSES
# ============================================================

@dataclass
class FormattedStrategy:
    """Formatted output from a single strategy"""
    name: str
    direction: str  # "BULLISH", "BEARISH", "NEUTRAL"
    confidence: str  # "HIGH", "MEDIUM", "LOW" with percentage
    confidence_value: float  # Raw 0-1 value
    weight: float  # Strategy weight from metamodel
    details: Optional[Dict[str, Any]] = None

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


@dataclass
class FormattedSignal:
    """Fully formatted signal ready for LLM consumption"""
    # Identification
    symbol: str
    timestamp: str
    timeframe: str

    # Metamodel outputs
    direction: str  # "BULLISH", "BEARISH", "NEUTRAL"
    magnitude: str  # "+0.5%" or "-0.3%"
    magnitude_value: float  # Raw percentage value
    confidence: str  # "HIGH", "MEDIUM", "LOW" with percentage
    confidence_value: float  # Raw 0-1 value
    delta_high: float  # Predicted high delta
    delta_low: float  # Predicted low delta

    # Strategy weights
    strategy_weights: Dict[str, float] = field(default_factory=dict)

    # Individual strategies
    strategies: List[FormattedStrategy] = field(default_factory=list)

    # Market context
    current_price: float = 0.0
    atr: float = 0.0
    volatility_regime: str = "MEDIUM"
    market_phase: str = "unknown"
    trading_session: str = "unknown"

    # Risk context (optional, added via add_risk_context)
    account_balance: Optional[float] = None
    available_margin: Optional[float] = None
    current_drawdown: Optional[float] = None
    max_risk_per_trade: Optional[float] = None
    open_positions: Optional[int] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert to serializable dictionary"""
        result = {
            "symbol": self.symbol,
            "timestamp": self.timestamp,
            "timeframe": self.timeframe,
            "direction": self.direction,
            "magnitude": self.magnitude,
            "magnitude_value": self.magnitude_value,
            "confidence": self.confidence,
            "confidence_value": self.confidence_value,
            "delta_high": self.delta_high,
            "delta_low": self.delta_low,
            "strategy_weights": self.strategy_weights,
            "strategies": [s.to_dict() for s in self.strategies],
            "market_context": {
                "current_price": self.current_price,
                "atr": self.atr,
                "volatility_regime": self.volatility_regime,
                "market_phase": self.market_phase,
                "trading_session": self.trading_session
            }
        }

        # Add risk context if present
        if self.account_balance is not None:
            result["risk_context"] = {
                "account_balance": self.account_balance,
                "available_margin": self.available_margin,
                "current_drawdown": self.current_drawdown,
                "max_risk_per_trade": self.max_risk_per_trade,
                "open_positions": self.open_positions
            }

        return result


# ============================================================
# SIGNAL FORMATTER CLASS
# ============================================================

class SignalFormatter:
    """
    Formats ML predictions for LLM consumption.

    Converts raw numerical outputs from the metamodel and strategies
    into human-readable, structured formats that help the LLM make
    informed trading decisions.

    Usage:
        formatter = SignalFormatter()

        signal = formatter.format_for_llm(
            metamodel_prediction=metamodel_result,
            strategy_predictions=strategy_results,
            market_context=context_dict,
            symbol="XAUUSD"
        )

        # Add account info for risk context
        signal = formatter.add_risk_context(signal, account_info)

        # Serialize for API
        signal_dict = signal.to_dict()
    """

    # Confidence thresholds
    CONFIDENCE_HIGH_THRESHOLD = 0.70
    CONFIDENCE_MEDIUM_THRESHOLD = 0.50

    # Volatility thresholds (ATR as % of price)
    VOLATILITY_HIGH_THRESHOLD = 0.015  # 1.5%
    VOLATILITY_LOW_THRESHOLD = 0.005   # 0.5%

    def __init__(
        self,
        high_confidence_threshold: float = 0.70,
        medium_confidence_threshold: float = 0.50
    ):
        """
        Initialize the signal formatter.

        Args:
            high_confidence_threshold: Threshold for HIGH confidence (default 0.70)
            medium_confidence_threshold: Threshold for MEDIUM confidence (default 0.50)
        """
        self.high_threshold = high_confidence_threshold
        self.medium_threshold = medium_confidence_threshold

    def format_direction(self, value: float) -> str:
        """
        Format a directional score into a string.

        Args:
            value: Directional value (-1 to 1, or 0/1 for binary)
                   Positive = BULLISH, Negative = BEARISH, Near 0 = NEUTRAL

        Returns:
            "BULLISH", "BEARISH", or "NEUTRAL"
        """
        if value > 0.15:
            return "BULLISH"
        elif value < -0.15:
            return "BEARISH"
        else:
            return "NEUTRAL"

    def format_confidence(self, value: float) -> Tuple[str, str]:
        """
        Format a confidence value into a string with level.

        Args:
            value: Confidence value (0-1)

        Returns:
            Tuple of (formatted string, level)
            e.g., ("85.0% (HIGH)", "HIGH")
        """
        # Clamp to valid range
        value = max(0.0, min(1.0, value))

        if value >= self.high_threshold:
            level = "HIGH"
        elif value >= self.medium_threshold:
            level = "MEDIUM"
        else:
            level = "LOW"

        formatted = f"{value * 100:.1f}% ({level})"
        return formatted, level

    def format_magnitude(self, value: float) -> str:
        """
        Format a magnitude value into a signed percentage string.

        Args:
            value: Magnitude as percentage (e.g., 0.5 for 0.5%)

        Returns:
            Formatted string like "+0.50%" or "-0.30%"
        """
        if value >= 0:
            return f"+{value:.2f}%"
        else:
            return f"{value:.2f}%"

    def determine_volatility_regime(
        self,
        atr: float,
        current_price: float
    ) -> str:
        """
        Determine the volatility regime based on ATR.

        Args:
            atr: Average True Range value
            current_price: Current price

        Returns:
            "HIGH", "MEDIUM", or "LOW"
        """
        if current_price <= 0:
            return "MEDIUM"

        atr_pct = atr / current_price

        if atr_pct >= self.VOLATILITY_HIGH_THRESHOLD:
            return "HIGH"
        elif atr_pct <= self.VOLATILITY_LOW_THRESHOLD:
            return "LOW"
        else:
            return "MEDIUM"

    def determine_trading_session(self, timestamp: Optional[datetime] = None) -> str:
        """
        Determine the current trading session based on time.

        Args:
            timestamp: Datetime to check (uses current time if None)

        Returns:
            "asian", "london", "new_york", or "london_ny_overlap"
        """
        if timestamp is None:
            timestamp = datetime.utcnow()

        hour = timestamp.hour

        # Session times in UTC
        # Asian: 00:00 - 08:00
        # London: 08:00 - 16:00
        # New York: 13:00 - 22:00
        # Overlap: 13:00 - 16:00

        if 13 <= hour < 16:
            return "london_ny_overlap"
        elif 8 <= hour < 16:
            return "london"
        elif 13 <= hour < 22:
            return "new_york"
        else:
            return "asian"

    def format_for_llm(
        self,
        metamodel_prediction: Dict[str, Any],
        strategy_predictions: List[Dict[str, Any]],
        market_context: Dict[str, Any],
        symbol: str = "UNKNOWN",
        timeframe: str = "5m"
    ) -> FormattedSignal:
        """
        Format ML predictions for LLM consumption.

        This is the main method that converts raw ML outputs into
        a structured format suitable for the LLM trading agent.

        Args:
            metamodel_prediction: Dict with keys:
                - delta_high_final: float (predicted high delta)
                - delta_low_final: float (predicted low delta)
                - confidence: int (0 or 1)
                - confidence_proba: float (0-1)
                - strategy_weights: Dict[str, float] (optional)
            strategy_predictions: List of dicts with keys:
                - name: str (strategy name)
                - direction: float or str (-1/0/1 or "BULLISH"/"NEUTRAL"/"BEARISH")
                - confidence: float (0-1)
                - weight: float (optional, uses equal weight if missing)
            market_context: Dict with keys:
                - current_price: float
                - atr: float
                - market_phase: str (optional)
                - timestamp: str or datetime (optional)
            symbol: Trading symbol
            timeframe: Analysis timeframe

        Returns:
            FormattedSignal ready for LLM consumption
        """
        # Extract metamodel outputs
        delta_high = metamodel_prediction.get("delta_high_final", 0.0)
        delta_low = metamodel_prediction.get("delta_low_final", 0.0)
        confidence_proba = metamodel_prediction.get("confidence_proba", 0.5)
        strategy_weights = metamodel_prediction.get("strategy_weights", {})

        # Handle numpy arrays (take first element)
        if hasattr(delta_high, "__iter__") and not isinstance(delta_high, str):
            delta_high = float(delta_high[0]) if len(delta_high) > 0 else 0.0
        if hasattr(delta_low, "__iter__") and not isinstance(delta_low, str):
            delta_low = float(delta_low[0]) if len(delta_low) > 0 else 0.0
        if hasattr(confidence_proba, "__iter__") and not isinstance(confidence_proba, str):
            confidence_proba = float(confidence_proba[0]) if len(confidence_proba) > 0 else 0.5

        # Determine direction based on delta asymmetry
        if delta_high > delta_low * 1.2:
            direction = "BULLISH"
            magnitude = delta_high
        elif delta_low > delta_high * 1.2:
            direction = "BEARISH"
            magnitude = -delta_low
        else:
            direction = "NEUTRAL"
            magnitude = (delta_high + delta_low) / 2

        # Format confidence
        confidence_str, _ = self.format_confidence(confidence_proba)

        # Extract market context
        current_price = market_context.get("current_price", 0.0)
        atr = market_context.get("atr", 0.0)
        market_phase = market_context.get("market_phase", "unknown")

        # Determine volatility regime
        volatility_regime = self.determine_volatility_regime(atr, current_price)

        # Handle timestamp
        timestamp = market_context.get("timestamp")
        if isinstance(timestamp, datetime):
            timestamp_str = timestamp.isoformat()
            trading_session = self.determine_trading_session(timestamp)
        elif isinstance(timestamp, str):
            timestamp_str = timestamp
            trading_session = self.determine_trading_session()
        else:
            timestamp_str = datetime.utcnow().isoformat()
            trading_session = self.determine_trading_session()

        # Format individual strategies
        formatted_strategies = []
        n_strategies = len(strategy_predictions) if strategy_predictions else 1

        for i, strat in enumerate(strategy_predictions or []):
            strat_name = strat.get("name", f"Strategy_{i}")
            strat_direction = strat.get("direction", 0)
            strat_confidence = strat.get("confidence", 0.5)
            strat_weight = strat.get("weight", 1.0 / n_strategies)

            # Convert numeric direction to string
            if isinstance(strat_direction, (int, float)):
                strat_direction_str = self.format_direction(strat_direction)
            else:
                strat_direction_str = strat_direction.upper() if strat_direction else "NEUTRAL"

            # Format confidence
            conf_str, _ = self.format_confidence(strat_confidence)

            formatted_strategies.append(FormattedStrategy(
                name=strat_name,
                direction=strat_direction_str,
                confidence=conf_str,
                confidence_value=strat_confidence,
                weight=strat_weight,
                details=strat.get("details")
            ))

        # Build the formatted signal
        return FormattedSignal(
            symbol=symbol,
            timestamp=timestamp_str,
            timeframe=timeframe,
            direction=direction,
            magnitude=self.format_magnitude(magnitude),
            magnitude_value=magnitude,
            confidence=confidence_str,
            confidence_value=confidence_proba,
            delta_high=float(delta_high),
            delta_low=float(delta_low),
            strategy_weights=strategy_weights,
            strategies=formatted_strategies,
            current_price=current_price,
            atr=atr,
            volatility_regime=volatility_regime,
            market_phase=market_phase,
            trading_session=trading_session
        )

    def add_risk_context(
        self,
        signal: FormattedSignal,
        account_info: Dict[str, Any]
    ) -> FormattedSignal:
        """
        Add risk context from account information to a signal.

        Args:
            signal: FormattedSignal to augment
            account_info: Dict with account details:
                - balance: float
                - available_margin: float
                - current_drawdown: float (as decimal, e.g., 0.05 for 5%)
                - max_risk_per_trade: float (as percentage, e.g., 2.0 for 2%)
                - open_positions: int

        Returns:
            Signal with risk context added (mutates and returns same object)
        """
        signal.account_balance = account_info.get("balance")
        signal.available_margin = account_info.get("available_margin")
        signal.current_drawdown = account_info.get("current_drawdown")
        signal.max_risk_per_trade = account_info.get("max_risk_per_trade")
        signal.open_positions = account_info.get("open_positions")

        return signal

    @staticmethod
    def from_metamodel_output(
        metamodel_output: Any,
        symbol: str,
        current_price: float,
        atr: float,
        timeframe: str = "5m",
        strategy_names: Optional[List[str]] = None
    ) -> FormattedSignal:
        """
        Convenience method to create a FormattedSignal directly from a MetamodelPrediction.

        Args:
            metamodel_output: MetamodelPrediction or similar object with:
                - delta_high_final
                - delta_low_final
                - confidence
                - confidence_proba
            symbol: Trading symbol
            current_price: Current market price
            atr: Current ATR value
            timeframe: Analysis timeframe
            strategy_names: Optional list of strategy names

        Returns:
            FormattedSignal ready for LLM consumption
        """
        formatter = SignalFormatter()

        metamodel_dict = {
            "delta_high_final": getattr(metamodel_output, "delta_high_final", 0.0),
            "delta_low_final": getattr(metamodel_output, "delta_low_final", 0.0),
            "confidence": getattr(metamodel_output, "confidence", 0),
            "confidence_proba": getattr(metamodel_output, "confidence_proba", 0.5),
            "strategy_weights": getattr(metamodel_output, "strategy_weights", {})
        }

        # If alpha weights are available (from NeuralGating), use them
        if hasattr(metamodel_output, "alpha_high"):
            metamodel_dict["strategy_weights"] = {
                "5m_model": float(getattr(metamodel_output, "alpha_high", 0.5)),
                "15m_model": 1.0 - float(getattr(metamodel_output, "alpha_high", 0.5))
            }

        market_context = {
            "current_price": current_price,
            "atr": atr,
            "market_phase": "unknown"
        }

        # Create placeholder strategies if names provided
        strategy_predictions = []
        if strategy_names:
            n = len(strategy_names)
            for name in strategy_names:
                strategy_predictions.append({
                    "name": name,
                    "direction": 0,
                    "confidence": 0.5,
                    "weight": 1.0 / n
                })

        return formatter.format_for_llm(
            metamodel_prediction=metamodel_dict,
            strategy_predictions=strategy_predictions,
            market_context=market_context,
            symbol=symbol,
            timeframe=timeframe
        )


# ============================================================
# EXPORTS
# ============================================================

__all__ = [
    'SignalFormatter',
    'FormattedSignal',
    'FormattedStrategy',
]
