"""Small LibreVNA protocol subset used by synchronized multi-device sweeps."""

from __future__ import annotations

import struct
from collections.abc import Sequence
from dataclasses import dataclass

from python_app.hardware_full.librevna_multi_device_driver.models import SweepConfiguration


class PacketType:
    """Packet identifiers used by the multi-device sweep controller."""

    SWEEP_SETTINGS = 2
    ACKNOWLEDGE = 7
    NEGATIVE_ACKNOWLEDGE = 10
    REFERENCE_SETTINGS = 11
    SET_IDLE = 20
    VNA_DATAPOINT = 27


SYNC_MODE_HARDWARE_TRIGGER = 3
FRAME_HEADER_MAGIC = 0x5A


@dataclass(frozen=True, slots=True)
class ParsedVnaDatapoint:
    """Decoded contents of one VNADatapoint payload."""

    frequency_hz: int
    point_index: int
    receiver_values_by_description_mask: dict[int, complex]


def build_reference_settings_payload(output_frequency_hz: int, force_external_reference: bool) -> bytes:
    """Build the ReferenceSettings payload."""
    automatic_reference_switch = 0
    external_input_configuration_bits = (
        automatic_reference_switch << 0
        | int(bool(force_external_reference)) << 1
    )
    return struct.pack("<IB", int(output_frequency_hz), external_input_configuration_bits)


def build_sweep_settings_payload(
    sweep_configuration: SweepConfiguration,
    *,
    is_synchronization_master: bool,
    synchronization_enabled: bool,
    master_stimulus_ports: Sequence[int],
) -> bytes:
    """Build protocol-v14 SweepSettings for staged master outputs or synchronized receivers."""
    stimulus_ports = tuple(int(port) for port in master_stimulus_ports)
    if not stimulus_ports:
        raise ValueError("master_stimulus_ports must not be empty")
    if len(stimulus_ports) > 2 or set(stimulus_ports) - {1, 2}:
        raise ValueError("master_stimulus_ports may contain only ports 1 and 2")
    if len(set(stimulus_ports)) != len(stimulus_ports):
        raise ValueError("master_stimulus_ports must not contain duplicates")

    excitation_power_centidecibels_milliwatt = int(round(sweep_configuration.power_dbm * 100.0))
    dwell_time_microseconds = max(0, min(int(sweep_configuration.dwell_us), 0xFFFF))
    synchronization_mode = SYNC_MODE_HARDWARE_TRIGGER if synchronization_enabled else 0
    last_stage_index = len(stimulus_ports) - 1
    inactive_stage_index = min(last_stage_index + 1, 7)

    configuration_bits = (
        0 << 0
        | int(bool(is_synchronization_master)) << 1
        | 1 << 2
        | 1 << 3
        | 0 << 4
        | synchronization_mode << 5
    )

    if is_synchronization_master:
        stage_by_port = {port: stage for stage, port in enumerate(stimulus_ports)}
        port_1_stimulus_stage = stage_by_port.get(1, inactive_stage_index)
        port_2_stimulus_stage = stage_by_port.get(2, inactive_stage_index)
    else:
        port_1_stimulus_stage = inactive_stage_index
        port_2_stimulus_stage = inactive_stage_index

    stage_configuration_bits = (
        (last_stage_index & 0x07)
        | (port_1_stimulus_stage & 0x07) << 3
        | (port_2_stimulus_stage & 0x07) << 6
    )

    return struct.pack(
        "<QQHIhBHhH",
        int(round(sweep_configuration.start_hz)),
        int(round(sweep_configuration.stop_hz)),
        int(sweep_configuration.points),
        int(round(sweep_configuration.if_bandwidth)),
        excitation_power_centidecibels_milliwatt,
        configuration_bits,
        stage_configuration_bits,
        excitation_power_centidecibels_milliwatt,
        dwell_time_microseconds,
    )


def parse_vna_datapoint_payload(payload: bytes) -> ParsedVnaDatapoint | None:
    """Decode one VNADatapoint payload into a structured Python object."""
    fixed_header_length_bytes = 12
    per_value_storage_length_bytes = 9
    if len(payload) < fixed_header_length_bytes:
        return None

    frequency_hz, _power_level_centidecibels_milliwatt, point_index = struct.unpack_from("<QhH", payload, 0)
    value_count = (len(payload) - fixed_header_length_bytes) // per_value_storage_length_bytes
    if value_count == 0:
        return None

    real_components = struct.unpack_from(f"<{value_count}f", payload, 12)
    imaginary_components = struct.unpack_from(f"<{value_count}f", payload, 12 + 4 * value_count)
    description_masks = payload[12 + 8 * value_count : 12 + 9 * value_count]

    receiver_values_by_description_mask = {
        int(description_masks[value_index]): complex(
            real_components[value_index],
            imaginary_components[value_index],
        )
        for value_index in range(value_count)
    }
    return ParsedVnaDatapoint(
        frequency_hz=int(frequency_hz),
        point_index=int(point_index),
        receiver_values_by_description_mask=receiver_values_by_description_mask,
    )


def find_receiver_value(
    receiver_values_by_description_mask: dict[int, complex],
    *,
    stage_index: int,
    is_reference_receiver: bool,
    required_port_number: int = 0,
) -> complex | None:
    """Look up one complex receiver value by stage, receiver type, and optional port."""
    required_port_bit = (1 << (required_port_number - 1)) if required_port_number else 0
    for description_mask, complex_value in receiver_values_by_description_mask.items():
        if (description_mask >> 5) != stage_index:
            continue
        if bool(description_mask & 0x10) != is_reference_receiver:
            continue
        if required_port_bit and not (description_mask & required_port_bit):
            continue
        return complex_value
    return None
