#!/usr/bin/env python3
"""
Frequency Fingerprinting for RF Device Identification

Uses center frequency and frequency bands as identification features.
Narrows candidate protocols by frequency before detailed timing analysis.

Key features:
  - Frequency band classification (315/433/868/915 MHz)
  - Protocol filtering by frequency tolerance
  - Frequency-based scoring
  - ISM band detection
"""

import numpy as np
from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
from collections import defaultdict


# ISM (Industrial, Scientific, Medical) band definitions
ISM_BANDS = {
    '315MHz': (314_000_000, 316_000_000),   # North America
    '433MHz': (433_050_000, 434_790_000),   # Europe (primary ISM)
    '868MHz': (868_000_000, 868_600_000),   # Europe SRD band
    '915MHz': (902_000_000, 928_000_000),   # North America ISM
}


@dataclass
class FrequencyFingerprint:
    """
    Frequency characteristics of a signal

    Attributes:
        center_freq: Center frequency in Hz
        bandwidth: Estimated bandwidth in Hz (optional)
        ism_band: Detected ISM band name ('315MHz', '433MHz', etc.)
        frequency_offset: Offset from standard band center
        confidence: Detection confidence (0.0-1.0)
    """
    center_freq: int
    bandwidth: Optional[int]
    ism_band: Optional[str]
    frequency_offset: int
    confidence: float


@dataclass
class FrequencyMatch:
    """
    Match result comparing signal frequency to protocol

    Attributes:
        protocol_name: Name of matched protocol
        frequency_error: Absolute error in Hz
        frequency_error_pct: Error as percentage
        within_tolerance: Whether within protocol's tolerance
        score: Match score (0.0-1.0)
        details: Additional match details
    """
    protocol_name: str
    frequency_error: int
    frequency_error_pct: float
    within_tolerance: bool
    score: float
    details: Dict


class FrequencyFingerprinter:
    """
    Frequency-based protocol filtering and scoring

    Workflow:
      1. Analyze signal frequency
      2. Determine ISM band
      3. Filter protocols by frequency band
      4. Score remaining protocols by frequency match
    """

    def __init__(self):
        """Initialize fingerprinter"""
        self.ism_bands = ISM_BANDS

    def analyze(self, frequency: int) -> FrequencyFingerprint:
        """
        Analyze frequency characteristics

        Args:
            frequency: Signal center frequency in Hz

        Returns:
            FrequencyFingerprint with detected characteristics
        """
        # Detect ISM band
        ism_band = self._detect_ism_band(frequency)

        # Calculate offset from band center
        offset = 0
        if ism_band:
            band_low, band_high = self.ism_bands[ism_band]
            band_center = (band_low + band_high) // 2
            offset = frequency - band_center

        # Confidence based on band detection
        confidence = 0.9 if ism_band else 0.6

        return FrequencyFingerprint(
            center_freq=frequency,
            bandwidth=None,  # Not calculated from single .sub file
            ism_band=ism_band,
            frequency_offset=offset,
            confidence=confidence
        )

    def filter_protocols_by_frequency(
        self,
        protocols: List,
        signal_frequency: int,
        tolerance_hz: int = 200_000  # ±200 kHz default
    ) -> List:
        """
        Filter protocols by frequency match

        Args:
            protocols: List of ProtocolSignature objects
            signal_frequency: Signal frequency in Hz
            tolerance_hz: Frequency tolerance in Hz

        Returns:
            Filtered list of protocols within frequency range
        """
        filtered = []

        for protocol in protocols:
            freq_error = abs(protocol.frequency - signal_frequency)

            # Use protocol's own tolerance if available, otherwise use default
            protocol_tolerance = getattr(protocol, 'frequency_tolerance', tolerance_hz)

            if freq_error <= protocol_tolerance:
                filtered.append(protocol)

        return filtered

    def score_frequency_match(
        self,
        signal_frequency: int,
        protocol_frequency: int,
        protocol_tolerance: int = 100_000
    ) -> FrequencyMatch:
        """
        Score frequency match between signal and protocol

        Args:
            signal_frequency: Signal frequency in Hz
            protocol_frequency: Protocol expected frequency in Hz
            protocol_tolerance: Protocol's frequency tolerance in Hz

        Returns:
            FrequencyMatch with score
        """
        freq_error = abs(signal_frequency - protocol_frequency)
        freq_error_pct = freq_error / protocol_frequency if protocol_frequency > 0 else 1.0

        within_tolerance = freq_error <= protocol_tolerance

        # Score calculation:
        # - Exact match = 1.0
        # - Within tolerance = 0.8-1.0 (linear falloff)
        # - Outside tolerance = 0.0-0.5 (steep falloff)
        if freq_error == 0:
            score = 1.0
        elif within_tolerance:
            # Linear falloff within tolerance
            normalized = freq_error / protocol_tolerance
            score = 1.0 - (normalized * 0.2)  # 1.0 → 0.8
        else:
            # Steep falloff outside tolerance
            excess = freq_error - protocol_tolerance
            # Penalty: 50% score at 2x tolerance, 0% at 4x tolerance
            if excess < protocol_tolerance:
                score = 0.5 * (1.0 - excess / protocol_tolerance)
            else:
                score = 0.0

        return FrequencyMatch(
            protocol_name="",  # Set by caller
            frequency_error=freq_error,
            frequency_error_pct=freq_error_pct,
            within_tolerance=within_tolerance,
            score=score,
            details={
                'signal_freq_mhz': f"{signal_frequency / 1_000_000:.3f}",
                'protocol_freq_mhz': f"{protocol_frequency / 1_000_000:.3f}",
                'error_khz': f"{freq_error / 1_000:.1f}",
                'tolerance_khz': f"{protocol_tolerance / 1_000:.1f}"
            }
        )

    def group_protocols_by_band(
        self,
        protocols: List
    ) -> Dict[str, List]:
        """
        Group protocols by ISM frequency band

        Args:
            protocols: List of ProtocolSignature objects

        Returns:
            Dict mapping band name to protocols
        """
        grouped = defaultdict(list)

        for protocol in protocols:
            band = self._detect_ism_band(protocol.frequency)
            if band:
                grouped[band].append(protocol)
            else:
                grouped['other'].append(protocol)

        return dict(grouped)

    def get_band_statistics(
        self,
        protocols: List
    ) -> Dict[str, int]:
        """
        Get protocol count statistics by band

        Args:
            protocols: List of ProtocolSignature objects

        Returns:
            Dict mapping band name to protocol count
        """
        grouped = self.group_protocols_by_band(protocols)
        return {band: len(protos) for band, protos in grouped.items()}

    # === Helper Methods ===

    def _detect_ism_band(self, frequency: int) -> Optional[str]:
        """
        Detect which ISM band a frequency belongs to

        Args:
            frequency: Frequency in Hz

        Returns:
            Band name ('315MHz', '433MHz', etc.) or None
        """
        for band_name, (low, high) in self.ism_bands.items():
            if low <= frequency <= high:
                return band_name

        return None

    def _get_band_center(self, band_name: str) -> int:
        """Get center frequency of an ISM band"""
        if band_name not in self.ism_bands:
            return 0

        low, high = self.ism_bands[band_name]
        return (low + high) // 2


# === Integration with Protocol Database ===

class FrequencyFilterStrategy:
    """
    Matching strategy that pre-filters by frequency

    Reduces search space before expensive timing analysis
    """

    def __init__(self, protocol_db):
        """
        Initialize strategy

        Args:
            protocol_db: Protocol database instance
        """
        self.protocol_db = protocol_db
        self.fingerprinter = FrequencyFingerprinter()

    def filter_candidates(
        self,
        signal_frequency: int,
        max_candidates: int = 50
    ) -> List:
        """
        Get candidate protocols filtered by frequency

        Args:
            signal_frequency: Signal frequency in Hz
            max_candidates: Maximum protocols to return

        Returns:
            List of candidate protocols
        """
        # Get all protocols
        all_protocols = self.protocol_db.get_all()

        # Filter by frequency (±200 kHz default tolerance)
        candidates = self.fingerprinter.filter_protocols_by_frequency(
            all_protocols,
            signal_frequency,
            tolerance_hz=200_000
        )

        # If too many, narrow further
        if len(candidates) > max_candidates:
            # Sort by frequency error, keep closest
            candidates.sort(
                key=lambda p: abs(p.frequency - signal_frequency)
            )
            candidates = candidates[:max_candidates]

        return candidates

    def get_frequency_score(
        self,
        signal_frequency: int,
        protocol_frequency: int,
        protocol_tolerance: int
    ) -> float:
        """
        Get frequency match score

        Args:
            signal_frequency: Signal frequency in Hz
            protocol_frequency: Protocol frequency in Hz
            protocol_tolerance: Protocol tolerance in Hz

        Returns:
            Score between 0.0 and 1.0
        """
        match = self.fingerprinter.score_frequency_match(
            signal_frequency,
            protocol_frequency,
            protocol_tolerance
        )
        return match.score


# Singleton instance
_fingerprinter: Optional[FrequencyFingerprinter] = None


def get_frequency_fingerprinter() -> FrequencyFingerprinter:
    """Get singleton frequency fingerprinter instance"""
    global _fingerprinter
    if _fingerprinter is None:
        _fingerprinter = FrequencyFingerprinter()
    return _fingerprinter
