#!/usr/bin/env python3
"""
detectors/tilt_detector.py
Detects tilted or fragile market conditions.

A market is "tilted" when:
- Emotion channel dominates observation
- Pattern channel is outdated (regime changed but not detected)
- Information sources conflict sharply
- Confidence exceeds evidence
- Single factor drives multiple channels (fragile integration)
"""

import numpy as np
from dataclasses import dataclass
from typing import Dict, Any, List, Tuple
from core_v2 import ChannelObservation, DiagnosticRegime


@dataclass
class TiltSignal:
    is_tilted: bool
    tilt_severity: float  # 0.0-1.0
    primary_cause: str
    secondary_causes: List[str]
    fragility_score: float  # How likely is regime flip imminent?
    recommendations: List[str]
    metadata: Dict[str, Any]


class TiltDetector:
    """
    Identifies conditions where state estimation is unreliable.
    
    High tilt = low confidence in current diagnosis
    Low tilt = reliable diagnosis
    """
    
    def __init__(self, verbosity: int = 0):
        self.verbosity = verbosity
    
    def detect(self, channels: List[ChannelObservation]) -> TiltSignal:
        """
        Analyze channels for tilted conditions.
        
        Returns: TiltSignal with severity and recommendations
        """
        
        if not channels:
            return TiltSignal(
                is_tilted=True,
                tilt_severity=1.0,
                primary_cause="No channels provided",
                secondary_causes=[],
                fragility_score=1.0,
                recommendations=["Provide valid channels"],
                metadata={}
            )
        
        # Collect raw signals
        signals = {
            "coherence_breaks": self._check_coherence_breaks(channels),
            "extreme_divergence": self._check_extreme_divergence(channels),
            "confidence_misalignment": self._check_confidence_misalignment(channels),
            "correlation_shocks": self._check_correlation_shocks(channels),
            "single_factor_domination": self._check_single_factor_domination(channels),
        }
        
        # Determine primary cause
        active_signals = [(k, v) for k, v in signals.items() if v.get("active", False)]
        
        if not active_signals:
            return TiltSignal(
                is_tilted=False,
                tilt_severity=0.0,
                primary_cause="No tilt detected",
                secondary_causes=[],
                fragility_score=0.0,
                recommendations=[],
                metadata=signals
            )
        
        # Rank signals by severity
        active_signals.sort(key=lambda x: x[1].get("severity", 0), reverse=True)
        primary = active_signals[0][0]
        secondary = [s[0] for s in active_signals[1:]]
        
        # Aggregate severity
        tilt_severity = self._aggregate_severity([s for _, s in active_signals])
        fragility = self._estimate_fragility(channels, signals, tilt_severity)
        
        # Generate recommendations
        recommendations = self._generate_recommendations(primary, secondary, channels)
        
        return TiltSignal(
            is_tilted=tilt_severity > 0.3,
            tilt_severity=tilt_severity,
            primary_cause=primary,
            secondary_causes=secondary,
            fragility_score=fragility,
            recommendations=recommendations,
            metadata=signals
        )
    
    # ============================================================
    # TILT DETECTION CHECKS
    # ============================================================
    
    def _check_coherence_breaks(self, channels: List[ChannelObservation]) -> Dict[str, Any]:
        """
        Vectors within a channel disagree sharply (low coherence).
        Indicates observation noise or regime ambiguity.
        """
        
        coherence_values = [ch.coherence for ch in channels]
        mean_coherence = np.mean(coherence_values)
        min_coherence = np.min(coherence_values)
        
        # Severe: any channel with coherence < 0.3
        severe = any(c < 0.3 for c in coherence_values)
        
        # Moderate: mean coherence < 0.5
        moderate = mean_coherence < 0.5
        
        severity = 0.0
        if severe:
            severity = 0.8
        elif moderate:
            severity = 0.4
        
        return {
            "active": severity > 0.0,
            "severity": severity,
            "mean_coherence": mean_coherence,
            "min_coherence": min_coherence,
            "broken_channels": [i for i, c in enumerate(coherence_values) if c < 0.3],
        }
    
    def _check_extreme_divergence(self, channels: List[ChannelObservation]) -> Dict[str, Any]:
        """
        Channels disagree sharply on regime direction.
        Indicates conflicting evidence or incomplete information.
        """
        
        values = [ch.aggregate_value for ch in channels]
        mean_val = np.mean(values)
        std_val = np.std(values)
        
        # Extreme if std > 0.6 (channels pointing opposite directions)
        severe = std_val > 0.6
        moderate = std_val > 0.4
        
        severity = 0.0
        if severe:
            severity = 0.7
        elif moderate:
            severity = 0.35
        
        return {
            "active": severity > 0.0,
            "severity": severity,
            "mean_value": mean_val,
            "std_value": std_val,
            "channel_spread": (max(values) - min(values)),
        }
    
    def _check_confidence_misalignment(self, channels: List[ChannelObservation]) -> Dict[str, Any]:
        """
        Coherence is low but we're still confident.
        High confidence + low internal agreement = risky.
        """
        
        coherence_values = [ch.coherence for ch in channels]
        mean_coherence = np.mean(coherence_values)
        
        # We don't have a single "confidence" metric per channel yet,
        # but we can infer from coherence + vector count
        vector_counts = [len(ch.vectors) for ch in channels]
        mean_vector_count = np.mean(vector_counts)
        
        # Misalignment if few vectors (less data) but high coherence (appear confident)
        # OR many vectors (should be confident) but low coherence (aren't)
        
        suspicious = (mean_vector_count < 3 and mean_coherence > 0.7) or \
                     (mean_vector_count >= 3 and mean_coherence < 0.4)
        
        severity = 0.5 if suspicious else 0.0
        
        return {
            "active": suspicious,
            "severity": severity,
            "mean_vector_count": mean_vector_count,
            "mean_coherence": mean_coherence,
        }
    
    def _check_correlation_shocks(self, channels: List[ChannelObservation]) -> Dict[str, Any]:
        """
        Expected correlations between channels are broken.
        Indicates regime transition or data quality issue.
        """
        
        # Collect all vector correlations
        correlations = []
        for ch in channels:
            if ch.vector_corr_summary:
                correlations.extend(ch.vector_corr_summary.values())
        
        if not correlations:
            return {"active": False, "severity": 0.0, "mean_correlation": None}
        
        mean_corr = np.mean(correlations)
        std_corr = np.std(correlations)
        
        # Shock if std is very high (correlations wildly inconsistent)
        severe = std_corr > 0.7
        moderate = std_corr > 0.5
        
        severity = 0.0
        if severe:
            severity = 0.6
        elif moderate:
            severity = 0.3
        
        return {
            "active": severity > 0.0,
            "severity": severity,
            "mean_correlation": mean_corr,
            "std_correlation": std_corr,
        }
    
    def _check_single_factor_domination(self, channels: List[ChannelObservation]) -> Dict[str, Any]:
        """
        One vector or channel drives the diagnosis.
        Fragile integration — if that one thing changes, diagnosis collapses.
        """
        
        # Collect vector values across all channels
        all_vectors = []
        for ch in channels:
            for vec in ch.vectors:
                all_vectors.append((vec.vector_id, abs(vec.value)))
        
        if not all_vectors:
            return {"active": False, "severity": 0.0, "domination_ratio": None}
        
        sorted_vecs = sorted(all_vectors, key=lambda x: x[1], reverse=True)
        
        # If top vector is > 2x the next strongest, it's dominating
        if len(sorted_vecs) > 1:
            ratio = sorted_vecs[0][1] / sorted_vecs[1][1] if sorted_vecs[1][1] > 0 else float('inf')
            severe = ratio > 3.0
            moderate = ratio > 2.0
        else:
            ratio = float('inf')
            severe = len(sorted_vecs) == 1
            moderate = False
        
        severity = 0.0
        if severe:
            severity = 0.65
        elif moderate:
            severity = 0.3
        
        return {
            "active": severity > 0.0,
            "severity": severity,
            "domination_ratio": ratio,
            "top_vector": sorted_vecs[0][0] if sorted_vecs else None,
        }
    
    # ============================================================
    # AGGREGATION & SCORING
    # ============================================================
    
    def _aggregate_severity(self, signals: List[Dict[str, Any]]) -> float:
        """Combine multiple signals into overall severity."""
        if not signals:
            return 0.0
        
        # Weighted mean of active severities
        severities = [s.get("severity", 0) for s in signals]
        return float(np.mean(severities))
    
    def _estimate_fragility(
        self,
        channels: List[ChannelObservation],
        signals: Dict[str, Dict[str, Any]],
        tilt_severity: float,
    ) -> float:
        """
        Estimate how likely regime is to flip soon.
        
        High fragility = high tilt + low coherence + recent shocks
        """
        
        coherence_values = [ch.coherence for ch in channels]
        mean_coherence = np.mean(coherence_values)
        
        # Base fragility from tilt
        fragility = tilt_severity
        
        # Amplify if coherence is low (fragile integration)
        if mean_coherence < 0.4:
            fragility += 0.3
        
        # Amplify if correlation shocks detected
        if signals["correlation_shocks"].get("active", False):
            fragility += 0.2
        
        return min(fragility, 1.0)
    
    def _generate_recommendations(
        self,
        primary: str,
        secondary: List[str],
        channels: List[ChannelObservation],
    ) -> List[str]:
        """Generate actionable recommendations based on tilt signals."""
        
        recommendations = []
        
        if primary == "coherence_breaks":
            recommendations.append("Check vector data quality and noise levels")
            recommendations.append("Consider regime has changed; update baseline expectations")
        
        if primary == "extreme_divergence":
            recommendations.append("Channels pointing opposite directions — conflicting evidence")
            recommendations.append("Require additional observation before committing to action")
        
        if primary == "single_factor_domination":
            recommendations.append("Diagnosis depends too heavily on one factor")
            recommendations.append("Seek independent confirmation from other channels")
        
        if primary == "correlation_shocks":
            recommendations.append("Inter-vector correlations are breaking down")
            recommendations.append("Regime transition likely — increase sampling frequency")
        
        if "coherence_breaks" in secondary:
            recommendations.append("Multiple channels showing internal inconsistency")
        
        return recommendations
