#!/usr/bin/env python3
"""
Unified Device Identifier API

Production-ready RF device identification combining all scoring components:
1. Timing analysis (robust multi-method extraction)
2. Preamble detection (long burst, alternating, sync word)
3. Frequency fingerprinting (ISM band filtering)
4. Statistical classification (Bayesian scoring)
5. Pattern matching (bit patterns, protocol signatures)

Provides single `identify()` API that returns ranked device matches.
"""

from dataclasses import dataclass, field
from typing import List, Dict, Optional
from pathlib import Path

from src.parser.sub_parser import SignalMetadata, parse_sub_file
from src.matcher.pattern_decoder import get_pattern_decoder, DeviceMatch
from src.matcher.statistical_classifier import (
    get_statistical_classifier,
    FeatureVector,
    StatisticalScore
)
from src.matcher.timing_analyzer import get_timing_analyzer
from src.matcher.preamble_detector import get_preamble_detector
from src.matcher.frequency_fingerprint import get_frequency_fingerprinter


@dataclass
class UnknownDeviceClassification:
    """Classification for unknown/unidentified devices"""
    category: str  # Inferred category (weather_sensor, garage_door, etc.)
    features: Dict  # Extracted feature summary
    confidence: float  # Confidence in "unknown" classification
    suggestions: List[str]  # Possible similar devices


@dataclass
class IdentificationResult:
    """
    Complete identification result from unified API

    Attributes:
        matches: Ranked list of device matches (top 5)
        unknown_classification: Classification if device is unknown
        signal_metadata: Parsed signal metadata
        processing_time_ms: Total processing time
        method: Identification method used
    """
    matches: List[DeviceMatch] = field(default_factory=list)
    unknown_classification: Optional[UnknownDeviceClassification] = None
    signal_metadata: Optional[SignalMetadata] = None
    processing_time_ms: float = 0.0
    method: str = "hybrid"  # hybrid, statistical, heuristic

    @property
    def is_identified(self) -> bool:
        """Check if device was successfully identified"""
        return len(self.matches) > 0 and self.matches[0].confidence >= 0.5

    @property
    def top_match(self) -> Optional[DeviceMatch]:
        """Get top match (highest confidence)"""
        return self.matches[0] if self.matches else None

    @property
    def confidence_level(self) -> str:
        """Get confidence level of top match"""
        if not self.matches:
            return "none"

        conf = self.matches[0].confidence
        if conf >= 0.8:
            return "high"
        elif conf >= 0.5:
            return "medium"
        else:
            return "low"


class DeviceIdentifier:
    """
    Unified device identification API

    Combines all scoring components for production-ready identification.
    """

    def __init__(self):
        # Initialize all components
        self.pattern_decoder = get_pattern_decoder()
        self.statistical_classifier = get_statistical_classifier()
        self.timing_analyzer = get_timing_analyzer()
        self.preamble_detector = get_preamble_detector()
        self.frequency_fingerprinter = get_frequency_fingerprinter()

    def identify(
        self,
        signal_data: SignalMetadata,
        top_k: int = 5,
        use_statistical: bool = True
    ) -> IdentificationResult:
        """
        Identify RF device from signal data

        Args:
            signal_data: Parsed signal metadata (from .sub file)
            top_k: Number of top matches to return (default: 5)
            use_statistical: Enable statistical classifier (default: True)

        Returns:
            IdentificationResult with ranked matches
        """
        import time

        t0 = time.time()
        result = IdentificationResult(signal_metadata=signal_data)

        if not signal_data.has_raw_data and not signal_data.protocol:
            # No raw data AND no decoded protocol name - cannot identify
            result.method = "none"
            return result

        # Step 1: Pattern-based decoding (heuristic). For KEY files this routes
        # by the decoded protocol name; for RAW files it times the pulses.
        heuristic_matches = self.pattern_decoder.decode(signal_data)

        # Step 2: Statistical classification (needs RAW pulses; skip on KEY)
        if use_statistical and heuristic_matches and signal_data.has_raw_data:
            statistical_matches = self._apply_statistical_scoring(
                signal_data,
                heuristic_matches
            )

            # Combine heuristic and statistical scores
            combined_matches = self._combine_scores(
                heuristic_matches,
                statistical_matches
            )

            result.matches = combined_matches[:top_k]
            result.method = "hybrid"
        else:
            # Use heuristic only
            result.matches = heuristic_matches[:top_k]
            result.method = "heuristic"

        # Step 3: Unknown device classification
        if not result.is_identified:
            result.unknown_classification = self._classify_unknown(signal_data)

        result.processing_time_ms = (time.time() - t0) * 1000
        return result

    def identify_from_file(
        self,
        filepath: str,
        top_k: int = 5
    ) -> IdentificationResult:
        """
        Identify device from .sub file

        Args:
            filepath: Path to .sub file
            top_k: Number of top matches to return

        Returns:
            IdentificationResult with ranked matches
        """
        # Parse file
        signal_data = parse_sub_file(filepath)

        # Identify
        return self.identify(signal_data, top_k=top_k)

    def _apply_statistical_scoring(
        self,
        signal_data: SignalMetadata,
        heuristic_matches: List[DeviceMatch]
    ) -> List[StatisticalScore]:
        """Apply statistical classifier to refine matches"""
        if not signal_data.has_raw_data:
            return []

        # Extract timing
        timing = self.timing_analyzer.extract_timing(signal_data.raw_data)

        # Detect preamble
        preamble = self.preamble_detector.detect(
            signal_data.raw_data,
            timing.short_pulse_us,
            timing.long_pulse_us
        )
        preamble_type = preamble.type if preamble else 'none'

        # Count bits
        bit_count = signal_data.pulse_count // 2  # Approximate

        # Extract features
        signal_features = self.statistical_classifier.extract_signal_features(
            timing=timing,
            frequency=signal_data.frequency,
            preamble_type=preamble_type,
            bit_count=bit_count,
            pulse_count=signal_data.pulse_count
        )

        # Get candidate protocols from heuristic matches
        candidates = [match.protocol for match in heuristic_matches]

        # Classify
        statistical_scores = self.statistical_classifier.classify(
            signal_features,
            candidates
        )

        return statistical_scores

    def _combine_scores(
        self,
        heuristic_matches: List[DeviceMatch],
        statistical_scores: List[StatisticalScore]
    ) -> List[DeviceMatch]:
        """
        Combine heuristic and statistical scores

        Weighted combination:
        - Heuristic: 60% (multi-factor timing, preamble, frequency)
        - Statistical: 40% (Bayesian feature similarity)
        """
        # Create lookup for statistical scores
        stat_lookup = {score.protocol_name: score for score in statistical_scores}

        combined = []

        for match in heuristic_matches:
            # Get statistical score if available
            stat_score = stat_lookup.get(match.name)

            if stat_score:
                # Combine scores (weighted average)
                combined_confidence = (
                    match.confidence * 0.60 +
                    stat_score.confidence * 0.40
                )

                # Update match with combined score
                updated_match = DeviceMatch(
                    protocol=match.protocol,
                    confidence=combined_confidence,
                    match_method='hybrid',
                    details={
                        **match.details,
                        'heuristic_score': f"{match.confidence:.1%}",
                        'statistical_score': f"{stat_score.confidence:.1%}",
                        'statistical_likelihood': f"{stat_score.likelihood:.4f}",
                        'feature_distance': f"{stat_score.feature_distance:.3f}",
                    }
                )
                combined.append(updated_match)
            else:
                # No statistical score - use heuristic only
                combined.append(match)

        # Sort by combined confidence
        return sorted(combined, key=lambda m: m.confidence, reverse=True)

    def _classify_unknown(
        self,
        signal_data: SignalMetadata
    ) -> UnknownDeviceClassification:
        """
        Classify unknown device by inferring category from features

        Args:
            signal_data: Signal metadata

        Returns:
            UnknownDeviceClassification with inferred category
        """
        # Extract features
        timing = self.timing_analyzer.extract_timing(signal_data.raw_data)
        frequency = signal_data.frequency

        # Infer category from frequency and timing
        category = "unknown"
        suggestions = []

        # Frequency-based inference
        if 433_000_000 <= frequency <= 434_000_000:
            # 433 MHz - likely weather sensor or remote control
            if timing.pulse_ratio >= 1.8 and timing.pulse_ratio <= 2.2:
                category = "weather_sensor"
                suggestions = ["Weather sensor (PWM encoding)", "Temperature/humidity monitor"]
            else:
                category = "remote_control"
                suggestions = ["Remote control", "Wireless switch", "Doorbell"]

        elif 314_000_000 <= frequency <= 316_000_000:
            # 315 MHz - likely garage door or TPMS
            if timing.short_pulse_us < 150:
                category = "tire_pressure"
                suggestions = ["TPMS (Tire Pressure Monitoring)", "Vehicle sensor"]
            else:
                category = "garage_door"
                suggestions = ["Garage door opener", "Gate remote", "Car key fob"]

        elif 868_000_000 <= frequency <= 869_000_000:
            # 868 MHz - likely security sensor
            category = "security"
            suggestions = ["Security sensor", "Alarm system", "Motion detector"]

        elif 902_000_000 <= frequency <= 928_000_000:
            # 915 MHz - North America ISM
            category = "iot_sensor"
            suggestions = ["IoT sensor", "Smart home device", "Wireless meter"]

        # Extract feature summary
        features = {
            'frequency_mhz': f"{frequency / 1_000_000:.3f}",
            'short_pulse_us': timing.short_pulse_us,
            'long_pulse_us': timing.long_pulse_us,
            'pulse_ratio': f"{timing.pulse_ratio:.2f}",
            'pulse_count': signal_data.pulse_count,
            'duty_cycle': f"{timing.duty_cycle:.2%}",
        }

        return UnknownDeviceClassification(
            category=category,
            features=features,
            confidence=0.3,  # Low confidence for unknown
            suggestions=suggestions
        )


# Singleton instance
_identifier: Optional[DeviceIdentifier] = None


def get_device_identifier() -> DeviceIdentifier:
    """Get singleton device identifier instance"""
    global _identifier
    if _identifier is None:
        _identifier = DeviceIdentifier()
    return _identifier


# Convenience function
def identify(signal_data: SignalMetadata, top_k: int = 5) -> IdentificationResult:
    """
    Convenience function for device identification

    Args:
        signal_data: Parsed signal metadata
        top_k: Number of top matches to return

    Returns:
        IdentificationResult with ranked matches
    """
    identifier = get_device_identifier()
    return identifier.identify(signal_data, top_k=top_k)


def identify_from_file(filepath: str, top_k: int = 5) -> IdentificationResult:
    """
    Convenience function to identify from .sub file

    Args:
        filepath: Path to .sub file
        top_k: Number of top matches to return

    Returns:
        IdentificationResult with ranked matches
    """
    identifier = get_device_identifier()
    return identifier.identify_from_file(filepath, top_k=top_k)


if __name__ == '__main__':
    # Test unified identifier
    from pathlib import Path

    print("=== Unified Device Identifier Test ===")
    print()

    # Test with sample file
    test_file = Path("data/test_rtl433_real/LaCrosse-TX141TH-BV2-raw.sub")

    if test_file.exists():
        print(f"Testing with: {test_file.name}")
        print()

        result = identify_from_file(str(test_file))

        print(f"Identification Method: {result.method}")
        print(f"Processing Time: {result.processing_time_ms:.2f} ms")
        print(f"Confidence Level: {result.confidence_level}")
        print()

        if result.is_identified:
            print(f"✓ IDENTIFIED: {result.top_match.name}")
            print(f"  Confidence: {result.top_match.confidence:.1%}")
            print()

            print("Top 5 Matches:")
            for i, match in enumerate(result.matches, 1):
                print(f"{i}. {match.name} ({match.confidence:.1%}) - {match.match_method}")

        else:
            print("✗ UNKNOWN DEVICE")
            if result.unknown_classification:
                unk = result.unknown_classification
                print(f"  Inferred Category: {unk.category}")
                print(f"  Suggestions: {', '.join(unk.suggestions)}")
                print(f"  Features: {unk.features}")

    else:
        print(f"❌ Test file not found: {test_file}")
