#!/usr/bin/env python3
"""
Pattern-Based Decoder for RF Signals

Decodes devices from single-transmission captures using timing patterns,
statistical fingerprints, and protocol library matching.

Designed specifically for short captures from Flipper Zero and LilyGo T-Embed
that don't have enough repetitions for RTL_433.
"""

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

from src.parser.sub_parser import SignalMetadata
from src.matcher.protocol_database import (
    ProtocolSignature,
    ProtocolDatabase,
    Encoding,
    get_protocol_database
)
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
from src.matcher.category_router import get_category_router
from src.matcher.category_classifier import get_category_classifier


@dataclass
class PulseStatistics:
    """Statistical characteristics of pulse data"""
    mean_pulse_width: float
    std_pulse_width: float
    mean_gap_width: float
    std_gap_width: float
    pulse_gap_ratio: float
    duty_cycle: float
    pulse_count: int
    min_pulse: int
    max_pulse: int
    min_gap: int
    max_gap: int


@dataclass
class TimingPattern:
    """Detected timing pattern in signal"""
    short_pulse_us: int
    long_pulse_us: int
    short_gap_us: int
    long_gap_us: int
    bit_pattern: str  # Binary string decoded from pulses
    confidence: float


@dataclass
class DeviceMatch:
    """Potential device match from pattern decoder"""
    protocol: ProtocolSignature
    confidence: float
    match_method: str  # 'timing', 'fingerprint', 'protocol'
    details: Dict

    @property
    def name(self) -> str:
        return self.protocol.name

    @property
    def manufacturer(self) -> Optional[str]:
        return self.protocol.manufacturer

    @property
    def category(self) -> str:
        return self.protocol.category


class PatternDecoder:
    """
    Multi-strategy decoder for single-transmission RF captures

    Uses three complementary strategies:
    1. Timing Pattern Analysis - Identify SHORT/LONG pulses and decode bits
    2. Statistical Fingerprinting - Match signal characteristics
    3. Protocol Library Matching - Compare against known protocol signatures
    """

    def __init__(self, protocol_db: Optional[ProtocolDatabase] = None):
        self.protocol_db = protocol_db or get_protocol_database()
        self.timing_analyzer = get_timing_analyzer()
        self.preamble_detector = get_preamble_detector()
        self.frequency_fingerprinter = get_frequency_fingerprinter()
        self.category_router = get_category_router()
        self.ml_classifier = get_category_classifier()

    def decode(self, metadata: SignalMetadata) -> List[DeviceMatch]:
        """
        Decode device from signal metadata using multi-strategy approach

        Args:
            metadata: Parsed .sub file metadata with RAW_Data

        Returns:
            List of DeviceMatch sorted by confidence (highest first)
        """
        if not metadata.has_raw_data:
            # KEY files carry no pulses to time, but a decoded Protocol name is
            # itself identifying — route it instead of returning nothing.
            if metadata.protocol:
                return self._decode_from_key(metadata)
            return []

        pulses = metadata.raw_data
        frequency = metadata.frequency

        matches = []

        # Strategy 1: Timing Pattern Analysis
        timing_matches = self._decode_timing_patterns(pulses, frequency)
        matches.extend(timing_matches)

        # Strategy 2: Statistical Fingerprinting
        fingerprint = self._extract_fingerprint(pulses)
        fingerprint_matches = self._match_fingerprint(fingerprint, frequency)
        matches.extend(fingerprint_matches)

        # Strategy 3: Protocol Library Matching (combines timing + known protocols)
        # Already integrated into timing_matches above

        # Deduplicate and rank by confidence
        matches = self._rank_matches(matches)

        # Strategy 4 (Phase 3A): statistical category classifier. The trained
        # model predicts a device *category* far better than the heuristic
        # router on RAW captures, so when confident it owns the user-facing
        # category, re-ranks candidates toward that family, and — when no
        # protocol matched at all — still supplies a category-only result so
        # the upload isn't left unidentified. No-op if the model is absent.
        matches = self._apply_ml_category(matches, pulses, frequency)

        return matches

    def _decode_from_key(self, metadata: SignalMetadata) -> List[DeviceMatch]:
        """
        Identify a decoded (KEY) .sub file from its Protocol name.

        KEY files have no RAW pulses but carry a decoded `Protocol:` name,
        frequency and key. We route the protocol name to a device category and
        emit a match surfacing the real Flipper protocol + routed category, so
        decoded uploads are no longer silently unidentifiable.
        """
        pred = self.category_router.route_by_protocol(
            metadata.protocol, metadata.frequency
        )
        signature = ProtocolSignature(
            name=metadata.protocol,
            category=pred.primary_category,
            frequency=metadata.frequency or 433920000,
        )
        details = {
            "predicted_category": pred.primary_category,
            "allowed_categories": pred.allowed_categories,
            "routing_reason": pred.reasoning,
            "source": "decoded_key",
            "bit_length": metadata.bit_length,
        }
        return [DeviceMatch(
            protocol=signature,
            confidence=pred.confidence,
            match_method="decoded_key",
            details=details,
        )]

    def _identify_pulse_widths(self, pulses: List[int]) -> Tuple[int, int, int, int]:
        """
        Identify SHORT/LONG pulse and gap durations using robust timing analyzer

        Returns:
            (short_pulse, long_pulse, short_gap, long_gap) in microseconds
        """
        if not pulses:
            return (0, 0, 0, 0)

        # Use robust timing analyzer for improved extraction
        timing = self.timing_analyzer.extract_timing(pulses)

        return (
            timing.short_pulse_us,
            timing.long_pulse_us,
            timing.short_gap_us,
            timing.long_gap_us
        )

    def _cluster_durations(self, durations: List[int]) -> Tuple[int, int]:
        """
        Cluster durations into two groups (SHORT and LONG) using K-means

        Falls back to percentile-based approach if K-means unavailable
        """
        if not durations:
            return (0, 0)

        if len(durations) < 2:
            val = durations[0]
            return (val, val)

        # Try K-means clustering (sklearn)
        try:
            from sklearn.cluster import KMeans

            # Convert to 2D array for sklearn
            X = np.array(durations).reshape(-1, 1)

            # Cluster into 2 groups
            kmeans = KMeans(n_clusters=2, random_state=42, n_init=10)
            kmeans.fit(X)

            # Get cluster centers
            centers = sorted(kmeans.cluster_centers_.flatten())
            return (int(centers[0]), int(centers[1]))

        except ImportError:
            # Fallback: Use percentile-based approach
            sorted_durations = sorted(durations)
            median_idx = len(sorted_durations) // 2

            # Split at median
            lower_half = sorted_durations[:median_idx]
            upper_half = sorted_durations[median_idx:]

            short = int(np.mean(lower_half)) if lower_half else sorted_durations[0]
            long = int(np.mean(upper_half)) if upper_half else sorted_durations[-1]

            return (short, long)

    def _decode_timing_patterns(
        self,
        pulses: List[int],
        frequency: int
    ) -> List[DeviceMatch]:
        """
        Decode signal using timing pattern analysis with category routing.

        Steps:
        1. Extract SHORT/LONG pulse widths
        2. Route to device category (restricts protocol search space)
        3. Score filtered candidates with multi-factor scoring
        4. Apply confidence calibration
        """
        matches = []

        # Step 1: Identify pulse widths
        short_pulse, long_pulse, short_gap, long_gap = self._identify_pulse_widths(pulses)

        if short_pulse == 0 or long_pulse == 0:
            return []

        # Decode to binary pattern (using HIGH pulses only)
        bit_pattern = self._decode_to_bits(pulses, short_pulse, long_pulse)

        # Detect preamble
        detected_preamble = self.preamble_detector.detect(pulses, short_pulse, long_pulse)
        preamble_type = detected_preamble.type if detected_preamble else 'none'

        # Step 2: Category routing — restrict candidate set before expensive scoring
        category_pred = self.category_router.predict(
            frequency=frequency,
            short_pulse_us=short_pulse,
            long_pulse_us=long_pulse,
            pulse_count=len(pulses),
            preamble_type=preamble_type,
        )

        # Step 3: Frequency pre-filter (±500 kHz window)
        frequency_filtered = self.frequency_fingerprinter.filter_protocols_by_frequency(
            self.protocol_db.get_all(),
            frequency,
            tolerance_hz=500_000
        )

        # Apply category filter unless router says use full DB
        if not category_pred.use_full_db and category_pred.allowed_categories:
            category_filtered = [
                p for p in frequency_filtered
                if p.category in category_pred.allowed_categories
            ]
            # Safety net: if category filter eliminates everything, fall back
            if not category_filtered:
                category_filtered = frequency_filtered
        else:
            category_filtered = frequency_filtered

        # Further filter by timing match
        protocol_matches = [
            p for p in category_filtered
            if p.matches_timing(short_pulse, long_pulse)
        ]

        for proto in protocol_matches:
            # === Multi-Factor Scoring (Phase 0: Category-Aware) ===
            # Timing(40%) + Preamble(25%) + Ratio(20%) + Frequency(10%) + BitCount(5%)

            # 1. Timing accuracy (40%)
            short_timing_error = abs(proto.short_pulse_us - short_pulse) / max(proto.short_pulse_us, short_pulse)
            short_timing_confidence = max(0, 1.0 - short_timing_error)

            long_timing_error = abs(proto.long_pulse_us - long_pulse) / max(proto.long_pulse_us, long_pulse)
            long_timing_confidence = max(0, 1.0 - long_timing_error)

            timing_confidence = short_timing_confidence * 0.6 + long_timing_confidence * 0.4

            # 2. Preamble match (25%)
            preamble_match = self.preamble_detector.match_against_protocol(
                detected_preamble,
                proto.preamble_pattern,
                proto.sync_pattern
            )
            preamble_confidence = preamble_match.similarity

            # 3. Timing ratio match (20%)
            observed_ratio = long_pulse / short_pulse if short_pulse > 0 else 0
            protocol_ratio = proto.long_pulse_us / proto.short_pulse_us if proto.short_pulse_us > 0 else 0

            ratio_error = abs(observed_ratio - protocol_ratio) / max(observed_ratio, protocol_ratio)
            ratio_confidence = max(0, 1.0 - ratio_error)

            # 4. Frequency match (10%)
            freq_diff_khz = abs(frequency - proto.frequency) / 1000

            if freq_diff_khz <= 100:
                frequency_confidence = 1.0
            elif freq_diff_khz <= 500:
                frequency_confidence = 1.0 - (freq_diff_khz - 100) / 400 * 0.6
            else:
                frequency_confidence = 0.2

            # 5. Bit count match (5%)
            bit_count = len(bit_pattern)

            if proto.min_bits <= bit_count <= proto.max_bits:
                bit_confidence = 1.0
            elif bit_count < proto.min_bits:
                shortfall = (proto.min_bits - bit_count) / proto.min_bits
                bit_confidence = max(0.5, 1.0 - shortfall)
            else:
                excess = (bit_count - proto.max_bits) / proto.max_bits
                bit_confidence = max(0.5, 1.0 - excess)

            # Weighted composite score
            overall_confidence = (
                timing_confidence * 0.40 +
                preamble_confidence * 0.25 +
                ratio_confidence * 0.20 +
                frequency_confidence * 0.10 +
                bit_confidence * 0.05
            )

            # Uniqueness bonus (up to +20% for rare timing signatures)
            uniqueness_bonus = self._calculate_uniqueness_bonus(
                proto, short_pulse, protocol_matches
            )
            overall_confidence = min(1.0, overall_confidence * (1.0 + uniqueness_bonus))

            # Preamble boost: strong preamble is a reliable discriminator
            if preamble_confidence >= 0.9 and overall_confidence >= 0.8:
                overall_confidence = min(1.0, overall_confidence * 1.05)

            # ── Confidence calibration ─────────────────────────────────────
            # Category mismatch penalty: if this protocol's category wasn't
            # in the router's allowed set, reduce confidence (it's a fallback match)
            if (not category_pred.use_full_db
                    and category_pred.allowed_categories
                    and proto.category not in category_pred.allowed_categories):
                overall_confidence *= 0.65

            # Confidence level classification
            if overall_confidence >= 0.8:
                confidence_level = 'high'
            elif overall_confidence >= 0.5:
                confidence_level = 'medium'
            else:
                confidence_level = 'low'

            if overall_confidence >= proto.min_confidence:
                matches.append(DeviceMatch(
                    protocol=proto,
                    confidence=overall_confidence,
                    match_method='category_routed_v3',
                    details={
                        'short_pulse_us': short_pulse,
                        'long_pulse_us': long_pulse,
                        'observed_ratio': f"{observed_ratio:.2f}",
                        'protocol_ratio': f"{protocol_ratio:.2f}",
                        'bit_count': bit_count,
                        'bit_pattern': bit_pattern[:64],
                        'timing_score': f"{timing_confidence:.2%}",
                        'preamble_score': f"{preamble_confidence:.2%}",
                        'ratio_score': f"{ratio_confidence:.2%}",
                        'frequency_score': f"{frequency_confidence:.2%}",
                        'bit_count_score': f"{bit_confidence:.2%}",
                        'uniqueness_bonus': f"{uniqueness_bonus:.2%}",
                        'confidence_level': confidence_level,
                        'preamble_type': preamble_type,
                        'predicted_category': category_pred.primary_category,
                        'category_confidence': f"{category_pred.confidence:.2%}",
                        'category_reasoning': category_pred.reasoning,
                        'scoring_weights': 'T:40% P:25% R:20% F:10% B:5%',
                    }
                ))

        # ── Post-match calibration: spread penalty ─────────────────────────
        # If top-1 and top-2 scores are nearly identical, both are probably wrong.
        # Reduce their confidence to signal low discrimination.
        matches = self._apply_spread_penalty(matches)

        return matches

    def _apply_ml_category(
        self,
        matches: List[DeviceMatch],
        pulses: List[int],
        frequency: int,
    ) -> List[DeviceMatch]:
        """
        Apply the statistical category classifier to the RAW decode result.

        The classifier predicts a device *category* far more accurately than
        the heuristic router on RAW captures (Phase 3A: ~62.5% balanced vs
        ~30%), so when it is confident it:

        1. Becomes the source of the user-facing ``predicted_category`` on every
           match (the field the live upload path surfaces).
        2. Re-ranks candidate protocols toward the believed family (``BOOST``,
           0.25, mirrors the designed ensemble's statistical weight).
        3. When *no* protocol matched, emits a single category-only match so
           the capture still receives a device category instead of nothing —
           the whole point of Phase 3A category-level ID for unknown devices.

        Degrades to a pure no-op when the model is unavailable or too unsure
        (``top_prob`` below ``MIN_PROB``), preserving the heuristic path exactly.
        """
        pred = self.ml_classifier.predict(pulses, frequency)
        if not pred.available or not pred.top_category:
            return matches

        BOOST = 0.25            # statistical leg weight (ensemble design)
        MIN_PROB = 0.40         # ignore low-confidence predictions

        if pred.top_prob < MIN_PROB:
            return matches

        # No protocol matched: supply a category-only result (like KEY files).
        if not matches:
            signature = ProtocolSignature(
                name=f"Unknown ({pred.top_category})",
                category=pred.top_category,
                frequency=frequency or 433920000,
            )
            # Category-level ID without protocol confirmation — cap the reported
            # confidence so it never masquerades as a precise device match.
            CATEGORY_ONLY_CEIL = 0.75
            return [DeviceMatch(
                protocol=signature,
                confidence=round(min(pred.top_prob, CATEGORY_ONLY_CEIL), 3),
                match_method="ml_category",
                details={
                    "predicted_category": pred.top_category,
                    "category_source": "ml_statistical",
                    "ml_category_prob": f"{pred.top_prob:.2%}",
                    "source": "ml_category_only",
                },
            )]

        adjusted = []
        for m in matches:
            agree_prob = pred.probabilities.get(m.protocol.category, 0.0)
            # +top_prob if this protocol is in the predicted category; else a
            # soft penalty scaled by how much less mass the model gave it.
            if m.protocol.category == pred.top_category:
                factor = 1.0 + BOOST * pred.top_prob
            else:
                factor = 1.0 - BOOST * (pred.top_prob - agree_prob)
            new_conf = max(0.0, min(1.0, m.confidence * factor))
            heuristic_category = m.details.get("predicted_category")
            adjusted.append(DeviceMatch(
                protocol=m.protocol,
                confidence=new_conf,
                match_method=m.match_method,
                details={
                    **m.details,
                    # ML is the better RAW category predictor -> it owns the
                    # user-facing category; keep the heuristic one for audit.
                    "predicted_category": pred.top_category,
                    "heuristic_category": heuristic_category,
                    "category_source": "ml_statistical",
                    "ml_category_prob": f"{pred.top_prob:.2%}",
                    "ml_boost_applied": f"{factor:.3f}",
                },
            ))

        return sorted(adjusted, key=lambda m: m.confidence, reverse=True)

    def _apply_spread_penalty(self, matches: List[DeviceMatch]) -> List[DeviceMatch]:
        """
        Reduce confidence when top matches are too close together.

        When the matcher can't discriminate between candidates, confidence
        scores cluster near each other. This penalty surfaces that uncertainty
        rather than reporting a misleadingly high score.
        """
        if len(matches) < 2:
            return matches

        # Sort descending first
        matches = sorted(matches, key=lambda m: m.confidence, reverse=True)

        top1 = matches[0].confidence
        top2 = matches[1].confidence
        spread = top1 - top2

        # If top-1 and top-2 are within 5%, apply a penalty
        if spread < 0.05 and top1 > 0.60:
            penalty = 0.85  # 15% reduction
            calibrated = []
            for i, m in enumerate(matches):
                if i <= 1:  # Only penalise the ambiguous top pair
                    new_conf = m.confidence * penalty
                    calibrated.append(DeviceMatch(
                        protocol=m.protocol,
                        confidence=new_conf,
                        match_method=m.match_method,
                        details={**m.details, 'spread_penalty': f"{(1 - penalty):.0%}"},
                    ))
                else:
                    calibrated.append(m)
            return calibrated

        return matches

    def _detect_encoding_type(
        self,
        pulses: List[int],
        short_pulse: int,
        long_pulse: int
    ) -> Encoding:
        """
        Detect encoding type from pulse pattern

        Returns:
            Encoding.PWM for pulse-width modulation
            Encoding.MANCHESTER for Manchester encoding
            Encoding.PPM for pulse-position modulation
        """
        if not pulses or len(pulses) < 10:
            return Encoding.PWM  # Default

        # Analyze pulse pattern characteristics
        high_pulses = [abs(p) for p in pulses if p > 0]
        low_pulses = [abs(p) for p in pulses if p < 0]

        if not high_pulses or not low_pulses:
            return Encoding.PWM

        # Manchester: Equal-width pulses with phase transitions
        # Check if HIGH and LOW pulses are similar (within 30%)
        avg_high = np.mean(high_pulses)
        avg_low = np.mean(low_pulses)
        high_low_ratio = avg_high / avg_low if avg_low > 0 else 1.0

        if 0.7 <= high_low_ratio <= 1.3:
            # Similar HIGH and LOW durations suggest Manchester
            # Also check for consistent timing (low variance)
            std_high = np.std(high_pulses)
            cv_high = std_high / avg_high if avg_high > 0 else 1.0

            if cv_high < 0.3:  # Low coefficient of variation
                return Encoding.MANCHESTER

        # PWM: Variable pulse widths (SHORT vs LONG)
        # Check for bimodal distribution of HIGH pulses
        unique_durations = len(set([int(p / 100) * 100 for p in high_pulses]))

        if unique_durations >= 2:
            # Multiple pulse widths suggest PWM
            return Encoding.PWM

        # PPM: Fixed pulse width, variable gaps
        std_low = np.std(low_pulses)
        std_high = np.std(high_pulses)

        if std_low > std_high * 2:
            # Variable gaps, fixed pulses
            return Encoding.PPM

        # Default to PWM
        return Encoding.PWM

    def _calculate_uniqueness_bonus(
        self,
        protocol: ProtocolSignature,
        observed_short_pulse: int,
        all_candidates: List[ProtocolSignature]
    ) -> float:
        """
        Calculate uniqueness bonus for rare timing signatures

        If only 1-3 protocols have similar timing, boost confidence

        Args:
            protocol: The protocol being scored
            observed_short_pulse: Observed SHORT pulse timing
            all_candidates: All candidate protocols after filtering

        Returns:
            Bonus multiplier (0.0 to 0.2)
        """
        # Count how many protocols have similar SHORT pulse timing
        tolerance = 0.15  # ±15%
        similar_count = 0

        for candidate in all_candidates:
            timing_diff = abs(candidate.short_pulse_us - observed_short_pulse) / observed_short_pulse
            if timing_diff <= tolerance:
                similar_count += 1

        # Award bonus for uniqueness
        if similar_count == 1:
            return 0.20  # 20% bonus for unique timing
        elif similar_count == 2:
            return 0.15  # 15% bonus
        elif similar_count == 3:
            return 0.10  # 10% bonus
        elif similar_count <= 5:
            return 0.05  # 5% bonus
        else:
            return 0.0  # No bonus for common timing

    def _decode_to_bits(
        self,
        pulses: List[int],
        short_pulse: int,
        long_pulse: int
    ) -> str:
        """
        Decode pulse train to binary string

        Uses PWM encoding: SHORT=0, LONG=1
        """
        bits = []
        threshold = (short_pulse + long_pulse) / 2

        for pulse in pulses:
            if pulse > 0:  # Only decode HIGH pulses
                duration = abs(pulse)
                if duration < threshold:
                    bits.append('0')
                else:
                    bits.append('1')

        return ''.join(bits)

    def _extract_fingerprint(self, pulses: List[int]) -> PulseStatistics:
        """
        Extract statistical fingerprint from pulse data

        Returns metrics that characterize the signal's timing properties
        """
        if not pulses:
            return PulseStatistics(
                mean_pulse_width=0, std_pulse_width=0,
                mean_gap_width=0, std_gap_width=0,
                pulse_gap_ratio=0, duty_cycle=0,
                pulse_count=0,
                min_pulse=0, max_pulse=0,
                min_gap=0, max_gap=0
            )

        # Separate HIGH pulses and LOW gaps
        high_pulses = [p for p in pulses if p > 0]
        low_pulses = [abs(p) for p in pulses if p < 0]

        # Calculate statistics
        mean_pulse = np.mean(high_pulses) if high_pulses else 0
        std_pulse = np.std(high_pulses) if high_pulses else 0
        mean_gap = np.mean(low_pulses) if low_pulses else 0
        std_gap = np.std(low_pulses) if low_pulses else 0

        total_high = sum(high_pulses)
        total_low = sum(low_pulses)
        total_time = total_high + total_low

        pulse_gap_ratio = mean_pulse / mean_gap if mean_gap > 0 else 0
        duty_cycle = total_high / total_time if total_time > 0 else 0

        return PulseStatistics(
            mean_pulse_width=float(mean_pulse),
            std_pulse_width=float(std_pulse),
            mean_gap_width=float(mean_gap),
            std_gap_width=float(std_gap),
            pulse_gap_ratio=float(pulse_gap_ratio),
            duty_cycle=float(duty_cycle),
            pulse_count=len(pulses),
            min_pulse=min(high_pulses) if high_pulses else 0,
            max_pulse=max(high_pulses) if high_pulses else 0,
            min_gap=min(low_pulses) if low_pulses else 0,
            max_gap=max(low_pulses) if low_pulses else 0,
        )

    def _match_fingerprint(
        self,
        fingerprint: PulseStatistics,
        frequency: int
    ) -> List[DeviceMatch]:
        """
        Match statistical fingerprint against protocol database

        Uses signal characteristics to find similar protocols
        """
        matches = []

        # Get protocols near this frequency
        candidates = self.protocol_db.find_by_frequency(frequency)

        for proto in candidates:
            # Compare fingerprint characteristics
            # Use typical timing to estimate expected characteristics

            # Expected mean pulse ~ (short + long) / 2
            expected_mean_pulse = (proto.short_pulse_us + proto.long_pulse_us) / 2
            pulse_error = abs(expected_mean_pulse - fingerprint.mean_pulse_width) / expected_mean_pulse

            # Expected pulse count
            expected_count = proto.typical_pulse_count
            count_error = abs(expected_count - fingerprint.pulse_count) / expected_count

            # Similarity score (lower error = higher confidence)
            pulse_similarity = max(0, 1.0 - pulse_error)
            count_similarity = max(0, 1.0 - count_error)

            overall_confidence = (pulse_similarity * 0.6 + count_similarity * 0.4)

            # Lower threshold for fingerprint matches (less precise)
            if overall_confidence >= 0.4:
                matches.append(DeviceMatch(
                    protocol=proto,
                    confidence=overall_confidence * 0.8,  # Scale down (less confident)
                    match_method='fingerprint',
                    details={
                        'mean_pulse_us': f"{fingerprint.mean_pulse_width:.1f}",
                        'pulse_count': fingerprint.pulse_count,
                        'duty_cycle': f"{fingerprint.duty_cycle:.2%}",
                        'pulse_error': f"{pulse_error:.2%}",
                        'count_error': f"{count_error:.2%}",
                    }
                ))

        return matches

    def _rank_matches(self, matches: List[DeviceMatch]) -> List[DeviceMatch]:
        """
        Deduplicate and rank matches by confidence

        If same protocol matched by multiple methods, keep highest confidence
        """
        # Group by protocol name
        by_protocol: Dict[str, List[DeviceMatch]] = {}
        for match in matches:
            name = match.protocol.name
            if name not in by_protocol:
                by_protocol[name] = []
            by_protocol[name].append(match)

        # Keep best match per protocol
        best_matches = []
        for name, protocol_matches in by_protocol.items():
            best = max(protocol_matches, key=lambda m: m.confidence)
            best_matches.append(best)

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

    def get_version(self) -> str:
        """Get decoder version"""
        return "PatternDecoder v1.0"


# Global instance
_decoder: Optional[PatternDecoder] = None


def get_pattern_decoder() -> PatternDecoder:
    """Get singleton pattern decoder instance"""
    global _decoder
    if _decoder is None:
        _decoder = PatternDecoder()
    return _decoder


if __name__ == '__main__':
    # Test pattern decoder with sample data
    from src.parser.sub_parser import parse_sub_file
    from pathlib import Path

    print("=== Pattern Decoder Test ===")
    print()

    # Try to load a test 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()

        metadata = parse_sub_file(str(test_file))
        decoder = get_pattern_decoder()

        print(f"Signal Details:")
        print(f"  Frequency: {metadata.frequency / 1_000_000:.3f} MHz")
        print(f"  Pulse Count: {metadata.pulse_count}")
        print()

        print("Running pattern decoder...")
        matches = decoder.decode(metadata)

        print(f"Found {len(matches)} potential matches:")
        print()

        for i, match in enumerate(matches[:5], 1):  # Show top 5
            print(f"Match #{i}:")
            print(f"  Device: {match.name}")
            print(f"  Manufacturer: {match.manufacturer or 'Unknown'}")
            print(f"  Category: {match.category}")
            print(f"  Confidence: {match.confidence:.2%}")
            print(f"  Method: {match.match_method}")
            print(f"  Details: {match.details}")
            print()

    else:
        print(f"❌ Test file not found: {test_file}")
        print("   Run download script first to get test data")
