"""
Signature matching strategies
"""

from typing import List, Dict, Optional
from loguru import logger

from .engine import MatchStrategy, MatchResult
from ..parser.metadata import SignalMetadata


class ExactMatcher(MatchStrategy):
    """
    Exact matching strategy: protocol + frequency + bit length

    Highest confidence (1.0) for perfect matches
    """

    def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
        """Match by exact protocol, frequency, and bit length"""
        matches = []

        if not metadata.protocol or not metadata.bit_length:
            return matches

        # Query database for exact matches
        query = """
            SELECT DISTINCT
                d.id,
                d.manufacturer,
                d.model,
                s.protocol,
                s.frequency
            FROM devices d
            JOIN signatures s ON s.device_id = d.id
            WHERE s.protocol = %s
                AND s.frequency = %s
                AND (s.bit_length = %s OR s.bit_length IS NULL)
        """

        results = db.execute(query, (
            metadata.protocol,
            metadata.frequency,
            metadata.bit_length
        ))

        for row in results:
            matches.append(MatchResult(
                device_id=row['id'],
                device_name=row['model'],
                manufacturer=row['manufacturer'],
                confidence=1.0,
                match_method='exact',
                match_details={
                    'protocol': row['protocol'],
                    'frequency': row['frequency'],
                    'bit_length': metadata.bit_length
                }
            ))

        logger.debug(f"ExactMatcher found {len(matches)} matches")
        return matches


class PartialMatcher(MatchStrategy):
    """
    Partial matching: protocol + frequency only

    Confidence: 0.8
    """

    def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
        """Match by protocol and frequency only"""
        matches = []

        if not metadata.protocol:
            return matches

        query = """
            SELECT DISTINCT
                d.id,
                d.manufacturer,
                d.model,
                s.protocol,
                s.frequency
            FROM devices d
            JOIN signatures s ON s.device_id = d.id
            WHERE s.protocol = %s
                AND s.frequency = %s
        """

        results = db.execute(query, (metadata.protocol, metadata.frequency))

        for row in results:
            matches.append(MatchResult(
                device_id=row['id'],
                device_name=row['model'],
                manufacturer=row['manufacturer'],
                confidence=0.8,
                match_method='partial',
                match_details={
                    'protocol': row['protocol'],
                    'frequency': row['frequency']
                }
            ))

        logger.debug(f"PartialMatcher found {len(matches)} matches")
        return matches


class PatternMatcher(MatchStrategy):
    """
    Bit pattern matching with masks

    Confidence: 0.7-0.9 based on pattern similarity
    """

    def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
        """Match by bit pattern similarity"""
        matches = []

        if not metadata.key_data:
            return matches

        # Query signatures with bit patterns
        query = """
            SELECT DISTINCT
                d.id,
                d.manufacturer,
                d.model,
                s.bit_pattern,
                s.bit_mask,
                s.weight,
                s.protocol,
                s.frequency
            FROM devices d
            JOIN signatures s ON s.device_id = d.id
            WHERE s.bit_pattern IS NOT NULL
                AND s.frequency = %s
        """

        results = db.execute(query, (metadata.frequency,))

        for row in results:
            # Apply mask and compare
            if row['bit_mask']:
                similarity = self._compare_with_mask(
                    metadata.key_data,
                    row['bit_pattern'],
                    row['bit_mask']
                )
            else:
                similarity = self._compare_bytes(
                    metadata.key_data,
                    row['bit_pattern']
                )

            if similarity > 0.5:  # Minimum threshold
                confidence = similarity * row.get('weight', 1.0) * 0.9

                matches.append(MatchResult(
                    device_id=row['id'],
                    device_name=row['model'],
                    manufacturer=row['manufacturer'],
                    confidence=confidence,
                    match_method='pattern',
                    match_details={
                        'protocol': row['protocol'],
                        'frequency': row['frequency'],
                        'similarity': similarity
                    }
                ))

        logger.debug(f"PatternMatcher found {len(matches)} matches")
        return matches

    def _compare_with_mask(self, data1: bytes, data2: bytes, mask: bytes) -> float:
        """
        Compare two byte arrays with a mask

        Args:
            data1: First byte array
            data2: Second byte array
            mask: Mask (1=compare, 0=ignore)

        Returns:
            Similarity score (0.0 to 1.0)
        """
        if not data1 or not data2 or not mask:
            return 0.0

        # Ensure same length
        min_len = min(len(data1), len(data2), len(mask))

        matching_bits = 0
        total_bits = 0

        for i in range(min_len):
            mask_byte = mask[i] if i < len(mask) else 0xFF

            # Count bits to compare (1s in mask)
            bits_to_check = bin(mask_byte).count('1')
            total_bits += bits_to_check

            # Compare masked bytes
            masked1 = data1[i] & mask_byte
            masked2 = data2[i] & mask_byte

            # Count matching bits
            xor_result = masked1 ^ masked2
            matching_bits += bits_to_check - bin(xor_result).count('1')

        if total_bits == 0:
            return 0.0

        return matching_bits / total_bits

    def _compare_bytes(self, data1: bytes, data2: bytes) -> float:
        """
        Compare two byte arrays directly

        Returns:
            Similarity score (0.0 to 1.0)
        """
        if not data1 or not data2:
            return 0.0

        min_len = min(len(data1), len(data2))
        matches = sum(1 for i in range(min_len) if data1[i] == data2[i])

        return matches / max(len(data1), len(data2))


class TimingMatcher(MatchStrategy):
    """
    Timing pattern matching for RAW signals

    Confidence: 0.6-0.8 based on timing similarity
    """

    def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
        """Match by timing pattern characteristics"""
        matches = []

        if not metadata.raw_data or not metadata.avg_pulse_width:
            return matches

        # Query signatures with timing information
        query = """
            SELECT DISTINCT
                d.id,
                d.manufacturer,
                d.model,
                s.timing_min,
                s.timing_max,
                s.protocol,
                s.frequency
            FROM devices d
            JOIN signatures s ON s.device_id = d.id
            WHERE s.timing_min IS NOT NULL
                AND s.frequency = %s
        """

        results = db.execute(query, (metadata.frequency,))

        avg_pulse = metadata.avg_pulse_width

        for row in results:
            timing_min = row['timing_min']
            timing_max = row['timing_max']

            # Check if average pulse width falls within range
            if timing_min <= avg_pulse <= timing_max:
                # Calculate confidence based on how centered the value is
                range_size = timing_max - timing_min
                center = (timing_max + timing_min) / 2
                distance_from_center = abs(avg_pulse - center)

                # Confidence decreases as we move away from center
                confidence = 0.8 * (1.0 - (distance_from_center / (range_size / 2)))
                confidence = max(0.6, min(0.8, confidence))

                matches.append(MatchResult(
                    device_id=row['id'],
                    device_name=row['model'],
                    manufacturer=row['manufacturer'],
                    confidence=confidence,
                    match_method='timing',
                    match_details={
                        'protocol': row['protocol'],
                        'frequency': row['frequency'],
                        'avg_pulse_width': avg_pulse,
                        'timing_range': (timing_min, timing_max)
                    }
                ))

        logger.debug(f"TimingMatcher found {len(matches)} matches")
        return matches


class FrequencyMatcher(MatchStrategy):
    """
    Fuzzy frequency matching (within tolerance)

    Confidence: 0.5-0.7 based on frequency proximity
    """

    def __init__(self, tolerance_hz: int = 5000):
        """
        Initialize frequency matcher

        Args:
            tolerance_hz: Frequency tolerance in Hz (default 5kHz)
        """
        self.tolerance = tolerance_hz

    def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
        """Match by frequency proximity"""
        matches = []

        query = """
            SELECT DISTINCT
                d.id,
                d.manufacturer,
                d.model,
                s.frequency,
                s.protocol
            FROM devices d
            JOIN signatures s ON s.device_id = d.id
            WHERE s.frequency BETWEEN %s AND %s
                AND (s.protocol IS NULL OR s.protocol = 'Unknown')
        """

        freq_min = metadata.frequency - self.tolerance
        freq_max = metadata.frequency + self.tolerance

        results = db.execute(query, (freq_min, freq_max))

        for row in results:
            # Calculate confidence based on frequency difference
            freq_diff = abs(row['frequency'] - metadata.frequency)
            confidence = 0.7 * (1.0 - (freq_diff / self.tolerance))
            confidence = max(0.5, min(0.7, confidence))

            matches.append(MatchResult(
                device_id=row['id'],
                device_name=row['model'],
                manufacturer=row['manufacturer'],
                confidence=confidence,
                match_method='frequency',
                match_details={
                    'frequency': row['frequency'],
                    'frequency_diff_hz': freq_diff
                }
            ))

        logger.debug(f"FrequencyMatcher found {len(matches)} matches")
        return matches


class RTL433DecoderStrategy(MatchStrategy):
    """
    RTL_433 decoder strategy - highest accuracy for supported protocols

    Uses actual RTL_433 binary to decode RAW signals
    Confidence: 0.95 for successful decodes
    """

    def __init__(self):
        from .rtl433_decoder import get_decoder
        self.decoder = get_decoder()
        self._decoder_available = self.decoder._check_rtl433_available()

    def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
        """
        Decode using RTL_433 and convert to MatchResult

        Args:
            metadata: Signal metadata with RAW_Data
            db: Database connection (for storing decoded data)

        Returns:
            List of MatchResult objects
        """
        matches = []

        if not self._decoder_available:
            logger.debug("RTL_433 not available, skipping")
            return matches

        if not metadata.has_raw_data:
            logger.debug("No RAW data for RTL_433 decoder")
            return matches

        # Try RTL_433 decoding
        try:
            devices = self.decoder.decode(metadata, enable_all_protocols=True)

            for device in devices:
                # Find or create device in database
                device_entry = self._find_or_create_device(
                    db,
                    device.model,
                    device.manufacturer
                )

                matches.append(MatchResult(
                    device_id=device_entry['id'],
                    device_name=device.model,
                    manufacturer=device.manufacturer or 'Unknown',
                    confidence=device.confidence,
                    match_method='rtl433_decode',
                    match_details={
                        'protocol_id': device.protocol_id,
                        'device_id': device.device_id,
                        'rtl433_data': device.raw_json
                    }
                ))

            logger.debug(f"RTL433DecoderStrategy found {len(matches)} matches")

        except Exception as e:
            logger.error(f"RTL_433 decoder error: {e}")

        return matches

    def _find_or_create_device(self, db, model: str, manufacturer: Optional[str]) -> Dict:
        """
        Find device in database or create new entry

        Args:
            db: Database connection
            model: Device model name
            manufacturer: Manufacturer name

        Returns:
            Device record dict
        """
        # Search existing devices
        query = """
            SELECT id, manufacturer, model
            FROM devices
            WHERE model = %s
            AND (manufacturer = %s OR %s IS NULL OR manufacturer IS NULL)
            LIMIT 1
        """

        try:
            results = db.execute(query, (model, manufacturer, manufacturer))

            if results and len(results) > 0:
                return {
                    'id': results[0]['id'],
                    'manufacturer': results[0]['manufacturer'],
                    'model': results[0]['model']
                }
        except Exception as e:
            logger.warning(f"Database query error: {e}")

        # Create new device
        try:
            insert_query = """
                INSERT INTO devices (manufacturer, model, device_type, category)
                VALUES (%s, %s, 'rf_device', 'rtl433_decoded')
                RETURNING id, manufacturer, model
            """

            result = db.execute(insert_query, (manufacturer, model))
            if result and len(result) > 0:
                logger.info(f"Created new device: {manufacturer} {model} (ID={result[0]['id']})")
                return {
                    'id': result[0]['id'],
                    'manufacturer': result[0]['manufacturer'],
                    'model': result[0]['model']
                }
        except Exception as e:
            logger.error(f"Failed to create device: {e}")

        # Fallback - return dummy entry
        return {
            'id': -1,
            'manufacturer': manufacturer,
            'model': model
        }


class PatternBasedStrategy(MatchStrategy):
    """
    Pattern-based decoder strategy for single-transmission captures

    Uses timing patterns, statistical fingerprinting, and protocol library
    matching to identify devices from short captures (Flipper Zero, LilyGo T-Embed).

    This strategy is designed specifically for captures that don't have enough
    repetitions for RTL_433 decoding.

    Confidence: 0.5-0.9 based on pattern match quality
    """

    def __init__(self):
        """Initialize pattern decoder"""
        from src.matcher.pattern_decoder import get_pattern_decoder
        self.decoder = get_pattern_decoder()

    def match(self, metadata: SignalMetadata, db) -> List[MatchResult]:
        """
        Match using pattern-based decoder

        Args:
            metadata: Signal metadata with RAW_Data
            db: Database connection

        Returns:
            List of MatchResult sorted by confidence
        """
        matches = []

        # Only works with RAW data
        if not metadata.has_raw_data:
            return matches

        # Run pattern decoder
        try:
            device_matches = self.decoder.decode(metadata)

            for device_match in device_matches:
                # Find or create device in database
                device_entry = self._find_or_create_device(
                    db,
                    device_match.name,
                    device_match.manufacturer
                )

                # Convert to MatchResult
                matches.append(MatchResult(
                    device_id=device_entry['id'],
                    device_name=device_match.name,
                    manufacturer=device_match.manufacturer or 'Unknown',
                    confidence=device_match.confidence,
                    match_method=f'pattern_{device_match.match_method}',
                    match_details={
                        'category': device_match.category,
                        'method': device_match.match_method,
                        **device_match.details
                    }
                ))

        except Exception as e:
            logger.error(f"PatternBasedStrategy error: {e}")

        logger.debug(f"PatternBasedStrategy found {len(matches)} matches")
        return matches

    def _find_or_create_device(self, db, model: str, manufacturer: Optional[str]) -> Dict:
        """
        Find existing device or create new one

        Args:
            db: Database connection
            model: Device model name
            manufacturer: Optional manufacturer name

        Returns:
            Device entry dict with id, manufacturer, model
        """
        # Try to find existing device
        try:
            query = """
                SELECT id, manufacturer, model
                FROM devices
                WHERE model = %s
                AND (manufacturer = %s OR manufacturer IS NULL)
                LIMIT 1
            """

            results = db.execute(query, (model, manufacturer))
            if results and len(results) > 0:
                return {
                    'id': results[0]['id'],
                    'manufacturer': results[0]['manufacturer'],
                    'model': results[0]['model']
                }
        except Exception as e:
            logger.warning(f"Database query error: {e}")

        # Create new device
        try:
            insert_query = """
                INSERT INTO devices (manufacturer, model, device_type, category)
                VALUES (%s, %s, 'rf_device', 'pattern_decoded')
                RETURNING id, manufacturer, model
            """

            result = db.execute(insert_query, (manufacturer, model))
            if result and len(result) > 0:
                logger.info(f"Created new device: {manufacturer} {model} (ID={result[0]['id']})")
                return {
                    'id': result[0]['id'],
                    'manufacturer': result[0]['manufacturer'],
                    'model': result[0]['model']
                }
        except Exception as e:
            logger.error(f"Failed to create device: {e}")

        # Fallback - return dummy entry
        return {
            'id': -1,
            'manufacturer': manufacturer,
            'model': model
        }
