"""Field type classification for message format inference.

Provides entropy-based, pattern-based, and statistics-based field type
detectors along with the voting mechanism that combines their results.
"""

from __future__ import annotations

from typing import Any

import numpy as np
from numpy.typing import NDArray

from oscura.inference.message_format.entropy import calculate_entropy
from oscura.inference.message_format.types import InferredField


def extract_field_data(messages: list[NDArray[np.uint8]], offset: int, size: int) -> dict[str, Any]:
    """Extract field data for type detection.

    Args:
        messages: List of message arrays
        offset: Field offset
        size: Field size

    Returns:
        Dictionary with field values and statistics
    """
    values: list[int | tuple[int, ...]]
    if size <= 4:
        # Use integer representation for small fields
        int_values: list[int] = []
        for msg in messages:
            if size == 1:
                val_int = int(msg[offset])
            elif size == 2:
                val_int = int(msg[offset]) << 8 | int(msg[offset + 1])
            elif size == 4:
                val_int = (
                    int(msg[offset]) << 24
                    | int(msg[offset + 1]) << 16
                    | int(msg[offset + 2]) << 8
                    | int(msg[offset + 3])
                )
            else:  # size == 3
                val_int = int(msg[offset]) << 16 | int(msg[offset + 1]) << 8 | int(msg[offset + 2])
            int_values.append(val_int)
        values = list(int_values)
    else:
        # For larger fields, use bytes
        tuple_values: list[tuple[int, ...]] = []
        for msg in messages:
            val_tuple = tuple(int(b) for b in msg[offset : offset + size])
            tuple_values.append(val_tuple)
        values = list(tuple_values)

    # Calculate statistics
    if size > 4:
        # Bytes field - calculate entropy across all bytes
        all_bytes_list: list[int] = []
        for v in values:
            if isinstance(v, tuple):
                all_bytes_list.extend(v)
        all_bytes = np.array(all_bytes_list, dtype=np.uint8)
        entropy = calculate_entropy(all_bytes)
        variance = float(np.var(all_bytes))
    else:
        entropy = calculate_entropy(np.array(values, dtype=np.int64))
        variance = float(np.var(values))

    return {
        "values": values,
        "offset": offset,
        "size": size,
        "entropy": entropy,
        "variance": variance,
    }


def detect_type_entropy(field_data: dict[str, Any]) -> tuple[str, float]:
    """Detect field type using entropy analysis.

    : Entropy-based field type detection.

    Args:
        field_data: Field data dictionary

    Returns:
        Tuple of (field_type, confidence)
    """
    entropy = field_data["entropy"]
    values = field_data["values"]

    # Check if all values are identical (constant)
    if len(set(values)) == 1:
        return ("constant", 1.0)

    # Low entropy suggests constant or semi-constant
    if entropy < 1.0:
        return ("constant", 0.8)
    # Very high entropy suggests random data
    elif entropy > 7.0:
        return ("data", 0.7)
    # Medium entropy could be various types
    else:
        return ("unknown", 0.3)


def detect_type_patterns(
    field_data: dict[str, Any], offset: int, size: int, msg_len: int
) -> tuple[str, float]:
    """Detect field type using pattern matching.

    Detects counters, timestamps, lengths, checksums, and constants.

    Args:
        field_data: Field data dictionary with 'values' key.
        offset: Field offset in bytes.
        size: Field size in bytes.
        msg_len: Total message length in bytes.

    Returns:
        Tuple of (field_type, confidence_score).
    """
    values = field_data["values"]

    # Skip tuple values (byte arrays)
    if isinstance(values[0], tuple):
        return ("unknown", 0.3)

    int_values = [v for v in values if isinstance(v, int)]
    if not int_values:
        return ("unknown", 0.3)

    # Check for counter pattern
    if detect_counter_field(int_values):
        return ("counter", 0.9)

    # Check for timestamp pattern
    timestamp_result = check_timestamp_pattern(int_values)
    if timestamp_result is not None:
        return timestamp_result

    # Check for length field
    if is_likely_length_field(offset, size, msg_len, int_values):
        return ("length", 0.6)

    # Check for checksum field
    if is_likely_checksum_field(offset, size, msg_len):
        return ("checksum", 0.5)

    return ("unknown", 0.3)


def check_timestamp_pattern(values: list[int]) -> tuple[str, float] | None:
    """Check if values follow timestamp pattern.

    Args:
        values: Integer values.

    Returns:
        ('timestamp', confidence) or None if not a timestamp.
    """
    if len(values) < 3:
        return None

    diffs = [values[i + 1] - values[i] for i in range(len(values) - 1)]
    positive_diffs = [d for d in diffs if d > 0]

    if len(positive_diffs) < len(diffs) * 0.7:
        return None

    avg_diff = sum(positive_diffs) / len(positive_diffs)
    if avg_diff > 100:
        return ("timestamp", 0.7)

    return None


def is_likely_length_field(offset: int, size: int, msg_len: int, values: list[int]) -> bool:
    """Check if field is likely a length field.

    Args:
        offset: Field offset.
        size: Field size.
        msg_len: Message length.
        values: Integer values.

    Returns:
        True if likely a length field.
    """
    if offset >= 8 or size > 2:
        return False
    max_val = max(values)
    return max_val < msg_len * 2


def is_likely_checksum_field(offset: int, size: int, msg_len: int) -> bool:
    """Check if field is likely a checksum.

    Args:
        offset: Field offset.
        size: Field size.
        msg_len: Message length.

    Returns:
        True if likely a checksum field.
    """
    return offset + size >= msg_len - 4 and offset > 0


def detect_type_statistics(field_data: dict[str, Any]) -> tuple[str, float]:
    """Detect field type using statistical properties.

    : Statistics-based field type detection.

    Args:
        field_data: Field data dictionary

    Returns:
        Tuple of (field_type, confidence)
    """
    variance = field_data["variance"]
    entropy = field_data["entropy"]
    values = field_data["values"]

    if len(set(values)) == 1:
        return ("constant", 0.9)
    elif variance < 10:
        return ("constant", 0.7)
    elif entropy > 6.0 and variance > 1000:
        return ("data", 0.6)
    else:
        return ("unknown", 0.4)


def vote_field_type(
    detections: list[tuple[str, float]],
) -> tuple[str, float, dict[str, bool]]:
    """Vote on field type from multiple detectors.

    : Voting mechanism for field type.

    Args:
        detections: List of (field_type, confidence) tuples from detectors

    Returns:
        Tuple of (field_type, confidence, evidence_dict)
    """
    votes: dict[str, float] = {}
    evidence: dict[str, bool] = {}
    detector_names = ["entropy", "patterns", "statistics"]

    for i, (field_type, confidence) in enumerate(detections):
        detector_name = detector_names[i] if i < len(detector_names) else f"detector_{i}"

        if field_type not in votes:
            votes[field_type] = 0.0

        votes[field_type] += confidence
        evidence[f"{detector_name}_voted_{field_type}"] = True

    if not votes:
        return ("unknown", 0.0, evidence)

    best_type = max(votes.items(), key=lambda x: x[1])
    field_type = best_type[0]
    total_confidence = best_type[1]

    total_possible = sum(conf for _, conf in detections)

    if total_possible > 0:
        normalized_confidence = total_confidence / total_possible
    else:
        normalized_confidence = 0.0

    return (field_type, normalized_confidence, evidence)


def detect_counter_field(values: list[int]) -> bool:
    """Check if values form a counter sequence.

    : Counter field detection.

    Args:
        values: List of integer values

    Returns:
        True if values appear to be a counter
    """
    if len(values) < 3:
        return False

    diffs = [values[i + 1] - values[i] for i in range(len(values) - 1)]
    diffs_filtered = [d for d in diffs if d >= 0]

    if len(diffs_filtered) < len(diffs) * 0.7:
        return False

    ones = sum(1 for d in diffs_filtered if d == 1)
    return ones >= len(diffs_filtered) * 0.7


def detect_checksum_field(
    messages: list[NDArray[np.uint8]], field_offset: int, field_size: int
) -> bool:
    """Check if field is likely a checksum.

    : Checksum field detection.

    Args:
        messages: List of message arrays
        field_offset: Offset of potential checksum field
        field_size: Size of potential checksum field

    Returns:
        True if field appears to be a checksum
    """
    if field_size not in [1, 2, 4]:
        return False

    for msg in messages[: min(5, len(messages))]:
        xor_sum = 0
        for i in range(field_offset):
            xor_sum ^= int(msg[i])

        if field_size == 1:
            checksum = int(msg[field_offset])
        elif field_size == 2:
            checksum = int(msg[field_offset]) << 8 | int(msg[field_offset + 1])
        else:
            checksum = (
                int(msg[field_offset]) << 24
                | int(msg[field_offset + 1]) << 16
                | int(msg[field_offset + 2]) << 8
                | int(msg[field_offset + 3])
            )

        if field_size == 1 and (xor_sum & 0xFF) == checksum:
            continue
        else:
            return False

    return True


def classify_field(
    values: list[int | tuple[int, ...]],
    offset: int,
    size: int,
    messages: list[NDArray[np.uint8]],
) -> tuple[str, float]:
    """Classify field type based on patterns.

    Args:
        values: Field values across all messages.
        offset: Field offset in bytes.
        size: Field size in bytes.
        messages: Original message byte arrays.

    Returns:
        Tuple of (field_type, confidence_score).
    """
    if isinstance(values[0], tuple):
        return classify_byte_array_field(values)
    return classify_scalar_field(values, offset, size, messages)


def classify_byte_array_field(values: list[int | tuple[int, ...]]) -> tuple[str, float]:
    """Classify byte array field (multi-byte fields).

    Args:
        values: List of byte tuples.

    Returns:
        Tuple of (field_type, confidence).
    """
    if len(set(values)) == 1:
        return ("constant", 1.0)

    all_bytes = np.concatenate([np.array(v) for v in values])
    entropy = calculate_entropy(all_bytes)

    if entropy < 1.0:
        return ("constant", 0.9)
    elif entropy > 7.0:
        return ("data", 0.6)
    else:
        return ("data", 0.5)


def classify_scalar_field(
    values: list[int | tuple[int, ...]],
    offset: int,
    size: int,
    messages: list[NDArray[np.uint8]],
) -> tuple[str, float]:
    """Classify scalar (integer) field.

    Args:
        values: List of integer values.
        offset: Field offset.
        size: Field size.
        messages: Original messages.

    Returns:
        Tuple of (field_type, confidence).
    """
    if len(set(values)) == 1:
        return ("constant", 1.0)

    int_values = [v for v in values if isinstance(v, int)]
    if detect_counter_field(int_values):
        return ("counter", 0.9)

    msg_len = len(messages[0])
    if offset + size >= msg_len - 4:
        if detect_checksum_field(messages, offset, size):
            return ("checksum", 0.8)

    if offset < 8 and size <= 2:
        max_val = max(int_values)
        if max_val < msg_len * 2:
            return ("length", 0.6)

    return classify_by_statistics(values)


def classify_by_statistics(values: list[int | tuple[int, ...]]) -> tuple[str, float]:
    """Classify field by statistical properties.

    Args:
        values: Field values.

    Returns:
        Tuple of (field_type, confidence).
    """
    variance = np.var(values)
    entropy = calculate_entropy(np.array(values))

    if variance < 10:
        return ("constant", 0.6)
    elif entropy > 6.0:
        return ("data", 0.7)
    else:
        return ("unknown", 0.5)


def classify_field_ensemble(
    field_data: dict[str, Any],
    offset: int,
    size: int,
    msg_len: int,
    min_confidence: float,
) -> InferredField | None:
    """Classify field using ensemble of detectors.

    Args:
        field_data: Extracted field data dictionary.
        offset: Field offset in bytes.
        size: Field size in bytes.
        msg_len: Total message length.
        min_confidence: Minimum confidence threshold.

    Returns:
        InferredField or None if confidence too low.
    """
    entropy_type, entropy_conf = detect_type_entropy(field_data)
    pattern_type, pattern_conf = detect_type_patterns(field_data, offset, size, msg_len)
    stats_type, stats_conf = detect_type_statistics(field_data)
    field_type, confidence, evidence = vote_field_type(
        [
            (entropy_type, entropy_conf),
            (pattern_type, pattern_conf),
            (stats_type, stats_conf),
        ]
    )
    if confidence < min_confidence:
        return None
    return InferredField(
        name="",  # Will be set by caller
        offset=offset,
        size=size,
        field_type=field_type,  # type: ignore[arg-type]
        entropy=float(field_data["entropy"]),
        variance=float(field_data["variance"]),
        confidence=confidence,
        values_seen=field_data["values"][:5],
        evidence=evidence,
    )
