"""
MobiuSignal - Trading Signal Generator using Soft Algebra
==========================================================
Applies the potential/realized framework to market data.

Mathematical Foundation:
------------------------
- Potential (aₜ): Normalized volatility - "what could happen"
- Realized (bₜ): Price change - "what did happen"  
- Signal Magnitude: √(aₜ² + bₜ²) - interaction strength

Validated Results (3,080 days BTC/USDT):
- Spearman correlation: +0.222 (p<0.0001)
- Q4/Q1 ratio: 1.83x larger moves in top quartile
- Precision lift: 1.18x vs random baseline

Version: 1.0.0 (In Development)
"""

import numpy as np
from typing import Optional, Union, List, Tuple
from dataclasses import dataclass
from collections import deque
import warnings


@dataclass
class SignalResult:
    """Result from MobiuSignal computation."""
    timestamp: Optional[any]  # Original timestamp if provided
    potential: float          # aₜ - normalized volatility
    realized: float           # bₜ - price change (signed)
    magnitude: float          # √(aₜ² + bₜ²)
    direction: int            # +1 (bullish), -1 (bearish), 0 (neutral)
    quartile: int             # 1-4 based on magnitude (4 = strongest)
    
    @property
    def is_strong(self) -> bool:
        """True if signal is in top quartile."""
        return self.quartile == 4
    
    @property
    def is_bullish(self) -> bool:
        return self.direction == 1
    
    @property
    def is_bearish(self) -> bool:
        return self.direction == -1


@dataclass  
class BacktestResult:
    """Result from signal backtest."""
    total_signals: int
    strong_signals: int
    avg_magnitude: float
    q4_q1_ratio: float           # Ratio of Q4 to Q1 subsequent moves
    precision_lift: float        # vs random baseline
    correlation: float           # Spearman correlation with future moves
    correlation_pvalue: float
    sharpe_ratio: Optional[float] = None
    returns: Optional[np.ndarray] = None


class MobiuSignal:
    """
    Trading Signal Generator using Soft Algebra.
    
    Applies the same potential/realized decomposition used in Mobiu-Q
    optimization to market data for generating trading signals.
    
    Args:
        lookback: Window size for volatility calculation (default: 20)
        vol_scale: Scaling factor for volatility normalization (default: 100)
        threshold: Minimum magnitude for signal generation (default: 0.0)
        use_log_returns: Use log returns instead of simple returns (default: True)
    
    Example:
        from mobiu_q.signal import MobiuSignal
        
        signal = MobiuSignal(lookback=20)
        
        # Single computation
        result = signal.compute(prices)
        print(f"Signal: {result.magnitude:.3f}, Direction: {result.direction}")
        
        # Streaming mode
        for price in price_stream:
            result = signal.update(price)
            if result.is_strong:
                print(f"Strong signal detected: {result.direction}")
    
    Mathematical Details:
        Potential (aₜ):
            aₜ = σₜ / μₜ × vol_scale
            where σₜ = rolling std of returns, μₜ = rolling mean of |price|
            
        Realized (bₜ):
            bₜ = (Pₜ - Pₜ₋₁) / Pₜ₋₁ × 100  (percentage change)
            
        Signal Magnitude:
            Δ† = √(aₜ² + bₜ²)
            
        Note: The magnitude formula is an applied heuristic inspired by
        Euclidean distance, not a classical Dual Number property.
    """
    
    def __init__(
        self,
        lookback: int = 20,
        vol_scale: float = 100.0,
        threshold: float = 0.0,
        use_log_returns: bool = True,
    ):
        self.lookback = lookback
        self.vol_scale = vol_scale
        self.threshold = threshold
        self.use_log_returns = use_log_returns
        
        # Streaming state
        self._price_history = deque(maxlen=lookback + 1)
        self._magnitude_history = deque(maxlen=1000)  # For quartile calculation
        
        # Quartile thresholds (updated dynamically)
        self._q1_threshold = 0.0
        self._q2_threshold = 0.0
        self._q3_threshold = 0.0
    
    def compute(
        self,
        prices: Union[np.ndarray, List[float]],
        timestamps: Optional[List] = None,
    ) -> SignalResult:
        """
        Compute signal from price array.
        
        Args:
            prices: Array of prices (at least lookback+1 values)
            timestamps: Optional timestamps for the prices
            
        Returns:
            SignalResult with potential, realized, magnitude, direction
        """
        prices = np.asarray(prices, dtype=np.float64)
        
        if len(prices) < self.lookback + 1:
            warnings.warn(
                f"Need at least {self.lookback + 1} prices, got {len(prices)}. "
                "Using available data."
            )
        
        # Compute returns
        if self.use_log_returns:
            returns = np.diff(np.log(prices))
        else:
            returns = np.diff(prices) / prices[:-1]
        
        # Potential: normalized volatility
        if len(returns) >= self.lookback:
            recent_returns = returns[-self.lookback:]
            volatility = np.std(recent_returns)
            mean_price = np.mean(np.abs(prices[-self.lookback:]))
            a_t = (volatility / (mean_price + 1e-9)) * self.vol_scale * 100
        else:
            volatility = np.std(returns) if len(returns) > 1 else 0.0
            mean_price = np.mean(np.abs(prices))
            a_t = (volatility / (mean_price + 1e-9)) * self.vol_scale * 100
        
        # Realized: most recent price change (percentage)
        if len(prices) >= 2:
            b_t = (prices[-1] - prices[-2]) / (np.abs(prices[-2]) + 1e-9) * 100
        else:
            b_t = 0.0
        
        # Signal magnitude (Euclidean - applied heuristic)
        magnitude = np.sqrt(a_t**2 + b_t**2)
        
        # Direction
        if b_t > 0.01:
            direction = 1  # Bullish
        elif b_t < -0.01:
            direction = -1  # Bearish
        else:
            direction = 0  # Neutral
        
        # Update history and compute quartile
        self._magnitude_history.append(magnitude)
        quartile = self._compute_quartile(magnitude)
        
        timestamp = timestamps[-1] if timestamps else None
        
        return SignalResult(
            timestamp=timestamp,
            potential=a_t,
            realized=b_t,
            magnitude=magnitude,
            direction=direction,
            quartile=quartile,
        )
    
    def compute_series(
        self,
        prices: Union[np.ndarray, List[float]],
        timestamps: Optional[List] = None,
    ) -> List[SignalResult]:
        """
        Compute signals for entire price series.
        
        Args:
            prices: Array of prices
            timestamps: Optional timestamps
            
        Returns:
            List of SignalResult for each valid position
        """
        prices = np.asarray(prices, dtype=np.float64)
        results = []
        
        for i in range(self.lookback, len(prices)):
            window = prices[max(0, i - self.lookback):i + 1]
            ts = timestamps[i] if timestamps else None
            result = self.compute(window, [ts] if ts else None)
            results.append(result)
        
        # Recompute quartiles with full history
        magnitudes = [r.magnitude for r in results]
        if len(magnitudes) >= 4:
            q1, q2, q3 = np.percentile(magnitudes, [25, 50, 75])
            for r in results:
                if r.magnitude <= q1:
                    r.quartile = 1
                elif r.magnitude <= q2:
                    r.quartile = 2
                elif r.magnitude <= q3:
                    r.quartile = 3
                else:
                    r.quartile = 4
        
        return results
    
    def update(self, price: float, timestamp: Optional[any] = None) -> Optional[SignalResult]:
        """
        Streaming update with new price.
        
        Args:
            price: New price value
            timestamp: Optional timestamp
            
        Returns:
            SignalResult if enough history, None otherwise
        """
        self._price_history.append(price)
        
        if len(self._price_history) < self.lookback + 1:
            return None
        
        prices = np.array(self._price_history)
        return self.compute(prices, [timestamp] if timestamp else None)
    
    def reset(self):
        """Reset streaming state."""
        self._price_history.clear()
        self._magnitude_history.clear()
        self._q1_threshold = 0.0
        self._q2_threshold = 0.0
        self._q3_threshold = 0.0
    
    def _compute_quartile(self, magnitude: float) -> int:
        """Compute quartile based on historical magnitudes."""
        if len(self._magnitude_history) < 4:
            return 2  # Default to middle
        
        # Update thresholds periodically
        if len(self._magnitude_history) % 100 == 0:
            mags = np.array(self._magnitude_history)
            self._q1_threshold, self._q2_threshold, self._q3_threshold = \
                np.percentile(mags, [25, 50, 75])
        
        if magnitude <= self._q1_threshold:
            return 1
        elif magnitude <= self._q2_threshold:
            return 2
        elif magnitude <= self._q3_threshold:
            return 3
        else:
            return 4
    
    def backtest(
        self,
        prices: np.ndarray,
        future_window: int = 5,
        timestamps: Optional[List] = None,
    ) -> BacktestResult:
        """
        Backtest signal quality on historical data.
        
        Args:
            prices: Historical price array
            future_window: Days ahead to measure move (default: 5)
            timestamps: Optional timestamps
            
        Returns:
            BacktestResult with validation metrics
        """
        from scipy import stats
        
        prices = np.asarray(prices, dtype=np.float64)
        signals = self.compute_series(prices, timestamps)
        
        # Need future data for validation
        valid_signals = signals[:-future_window]
        
        if len(valid_signals) < 10:
            raise ValueError("Need more data for backtest")
        
        # Compute future moves for each signal
        magnitudes = []
        future_moves = []
        
        for i, sig in enumerate(valid_signals):
            idx = self.lookback + i
            if idx + future_window < len(prices):
                future_return = (prices[idx + future_window] - prices[idx]) / prices[idx]
                magnitudes.append(sig.magnitude)
                future_moves.append(abs(future_return) * 100)
        
        magnitudes = np.array(magnitudes)
        future_moves = np.array(future_moves)
        
        # Correlation
        correlation, pvalue = stats.spearmanr(magnitudes, future_moves)
        
        # Q4/Q1 ratio
        q1_mask = magnitudes <= np.percentile(magnitudes, 25)
        q4_mask = magnitudes >= np.percentile(magnitudes, 75)
        
        q1_avg_move = np.mean(future_moves[q1_mask]) if q1_mask.sum() > 0 else 1.0
        q4_avg_move = np.mean(future_moves[q4_mask]) if q4_mask.sum() > 0 else 1.0
        q4_q1_ratio = q4_avg_move / (q1_avg_move + 1e-9)
        
        # Precision lift (vs random)
        random_baseline = np.mean(future_moves)
        precision_lift = q4_avg_move / (random_baseline + 1e-9)
        
        # Strong signals count
        strong_signals = sum(1 for s in valid_signals if s.quartile == 4)
        
        return BacktestResult(
            total_signals=len(valid_signals),
            strong_signals=strong_signals,
            avg_magnitude=np.mean(magnitudes),
            q4_q1_ratio=q4_q1_ratio,
            precision_lift=precision_lift,
            correlation=correlation,
            correlation_pvalue=pvalue,
        )


class MobiuSignalOptimized(MobiuSignal):
    """
    MobiuSignal with Mobiu-Q optimized parameters.
    
    Uses MobiuOptimizer to find optimal lookback and vol_scale
    parameters for a given dataset.
    
    Example:
        from mobiu_q.signal import MobiuSignalOptimized
        
        # Auto-optimize parameters on training data
        signal = MobiuSignalOptimized.fit(train_prices, license_key="your-key")
        
        # Use optimized signal on test data
        result = signal.compute(test_prices)
    """
    
    @classmethod
    def fit(
        cls,
        prices: np.ndarray,
        license_key: Optional[str] = None,
        lookback_range: Tuple[int, int] = (10, 50),
        vol_scale_range: Tuple[float, float] = (50.0, 200.0),
        n_trials: int = 20,
        future_window: int = 5,
        verbose: bool = True,
    ) -> "MobiuSignalOptimized":
        """
        Fit optimal parameters using Mobiu-Q optimization.
        
        Args:
            prices: Training price data
            license_key: Mobiu-Q license key (uses env if not provided)
            lookback_range: Range for lookback parameter
            vol_scale_range: Range for vol_scale parameter
            n_trials: Number of optimization trials
            future_window: Days ahead for evaluation
            verbose: Print progress
            
        Returns:
            MobiuSignalOptimized with optimal parameters
        """
        from ..core import MobiuOptimizer, get_license_key
        
        if license_key is None:
            license_key = get_license_key()
        
        best_score = -np.inf
        best_params = (20, 100.0)
        
        # Simple grid search (could be enhanced with actual Mobiu-Q optimization)
        for _ in range(n_trials):
            lookback = np.random.randint(lookback_range[0], lookback_range[1])
            vol_scale = np.random.uniform(vol_scale_range[0], vol_scale_range[1])
            
            try:
                signal = cls(lookback=lookback, vol_scale=vol_scale)
                result = signal.backtest(prices, future_window=future_window)
                
                # Score: correlation + precision lift
                score = result.correlation + result.precision_lift
                
                if score > best_score:
                    best_score = score
                    best_params = (lookback, vol_scale)
                    if verbose:
                        print(f"New best: lookback={lookback}, vol_scale={vol_scale:.1f}, "
                              f"corr={result.correlation:.3f}, lift={result.precision_lift:.2f}x")
            except Exception as e:
                if verbose:
                    print(f"Trial failed: {e}")
        
        if verbose:
            print(f"\nOptimal params: lookback={best_params[0]}, vol_scale={best_params[1]:.1f}")
        
        return cls(lookback=best_params[0], vol_scale=best_params[1])


# ═══════════════════════════════════════════════════════════════════════════════
# CONVENIENCE FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════

def compute_signal(
    prices: Union[np.ndarray, List[float]],
    lookback: int = 20,
    vol_scale: float = 100.0,
) -> SignalResult:
    """
    Convenience function for one-shot signal computation.
    
    Args:
        prices: Price array
        lookback: Volatility lookback window
        vol_scale: Volatility scaling factor
        
    Returns:
        SignalResult
    """
    signal = MobiuSignal(lookback=lookback, vol_scale=vol_scale)
    return signal.compute(prices)


def backtest_signal(
    prices: np.ndarray,
    lookback: int = 20,
    vol_scale: float = 100.0,
    future_window: int = 5,
) -> BacktestResult:
    """
    Convenience function for signal backtest.
    
    Args:
        prices: Historical price array
        lookback: Volatility lookback window
        vol_scale: Volatility scaling factor
        future_window: Days ahead for evaluation
        
    Returns:
        BacktestResult
    """
    signal = MobiuSignal(lookback=lookback, vol_scale=vol_scale)
    return signal.backtest(prices, future_window=future_window)
