"""Dataclass schema for runtime configuration used by Python pipeline tools."""

from __future__ import annotations

from dataclasses import dataclass, field
import hashlib
import json
from pathlib import Path
from typing import Any


@dataclass(slots=True)
class ComboModel:
    """One switch combination used for an acquisition sweep."""

    input: int
    output: int


@dataclass(slots=True)
class RadarSweepModel:
    """Sweep settings for LibreVNA acquisition."""

    # Keep schema defaults minimal/safe; operational values come from run_config.json.
    start_hz: float = 0.0
    stop_hz: float = 0.0
    points: int = 1
    if_bandwidth_hz: float = 1.0
    power_dbm: float = -30.0


@dataclass(slots=True)
class RadarMultiDeviceModel:
    """Multi-device LibreVNA topology settings."""

    slave_serials: list[str] = field(default_factory=list)
    force_external_reference: bool = True
    recovery_attempts: int = 3


@dataclass(slots=True)
class KamilAdcModel:
    """External Kamil ADC acquisition process settings."""

    project_dir: str = ""
    executable_path: str = ""
    tty_path: str = ""
    args: list[str] = field(default_factory=list)
    env: dict[str, str] = field(default_factory=dict)
    startup_timeout_s: float = 5.0
    sweep_timeout_s: float = 5.0
    stop_timeout_s: float = 2.0


@dataclass(slots=True)
class LaserManualModeModel:
    """Manual laser-control setpoints."""

    temp1: float = 25.0
    temp2: float = 25.0
    current1: float = 30.0
    current2: float = 30.0


@dataclass(slots=True)
class LaserVariationModeModel:
    """Laser-control variation task parameters."""

    variation_type: str = "CHANGE_CURRENT_LD1"
    static_temp1: float = 25.0
    static_temp2: float = 25.0
    static_current1: float = 30.0
    static_current2: float = 30.0
    min_value: float = 30.0
    max_value: float = 35.0
    step: float = 0.1
    time_step: int = 20
    delay_time: int = 3


@dataclass(slots=True)
class LaserControlModel:
    """Laser-control board settings applied before Kamil ADC acquisition."""

    enabled: bool = False
    port: str = ""
    mode: str = "manual"
    pi_coeff1_p: int = 2560
    pi_coeff1_i: int = 128
    pi_coeff2_p: int = 2560
    pi_coeff2_i: int = 128
    manual: LaserManualModeModel = field(default_factory=LaserManualModeModel)
    variation: LaserVariationModeModel = field(default_factory=LaserVariationModeModel)


@dataclass(slots=True)
class RadarModel:
    """Radar section of run configuration."""

    model: str = "librevna"
    serial: str = ""
    remote_host: str = "127.0.0.1"
    remote_port: int = 50209
    driver_mode: str = "mock"
    mock_signal_hz: float = 1_000_000.0
    sweep: RadarSweepModel = field(default_factory=RadarSweepModel)
    multi_device: RadarMultiDeviceModel = field(default_factory=RadarMultiDeviceModel)
    kamil_adc: KamilAdcModel = field(default_factory=KamilAdcModel)
    laser_control: LaserControlModel = field(default_factory=LaserControlModel)


@dataclass(slots=True)
class SwitchModel:
    """Generic switch section of run configuration."""

    name: str
    driver_mode: str = "mock"
    driver: str = ""
    radar_port: int = 0
    positions: int = 1
    default_position: int = 0
    gpio_chip: str = ""
    pin_a: int = -1
    pin_b: int = -1
    invert_logic: bool = False


@dataclass(slots=True)
class RingEndpointModel:
    """Shared-memory ring endpoint description."""

    name: str
    capacity: int = 1
    slot_size_bytes: int = 4096


@dataclass(slots=True)
class RingsModel:
    """Ring endpoints used by orchestration pipeline."""

    raw: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
    raw_tap: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
    preprocessed: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
    preprocessed_tap: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
    results: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))


@dataclass(slots=True)
class LocatorServerRuntimeModel:
    """Embedded locator TCP server configuration stored in run config."""

    device_id: int = 3
    protocol_version: int = 1
    host: str = "0.0.0.0"
    port: int = 8888
    max_payload_bytes: int = 64 * 1024
    client_queue_size: int = 32
    logger_name: str = "locator_runtime"


@dataclass(slots=True)
class RuntimeModel:
    """Runtime process behavior and paths."""

    settling_ms: int = 0
    idle_sleep_ms: int = 2
    continuous: bool = False
    processing_live_config_path: str = ""
    locator_server: LocatorServerRuntimeModel = field(default_factory=LocatorServerRuntimeModel)


@dataclass(slots=True)
class PreprocessAssetModel:
    """One preprocessing asset selected for live acquisition."""

    set_name: str = ""
    bundle_path: str = ""


@dataclass(slots=True)
class S21PreprocessModel:
    """Two-port S21 preprocessing assets."""

    calibration: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
    reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)


@dataclass(slots=True)
class S11CalibrationModel:
    """One-port S11 OSL calibration assets."""

    open: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
    short: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)
    load: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)


@dataclass(slots=True)
class S11PreprocessModel:
    """One-port S11 preprocessing assets."""

    calibration: S11CalibrationModel = field(default_factory=S11CalibrationModel)
    reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel)


@dataclass(slots=True)
class PreprocessNotchModel:
    """Optional frequency-domain notch filter applied after calibration and reference subtraction."""

    enabled: bool = False
    bands_hz: list[tuple[float, float]] = field(default_factory=list)
    taper_width_hz: float = 40_000_000.0
    taper_type: str = "cosine"


@dataclass(slots=True)
class PreprocessModel:
    """Selected preprocessing artifacts for live acquisition."""

    s21: S21PreprocessModel = field(default_factory=S21PreprocessModel)
    s11: S11PreprocessModel = field(default_factory=S11PreprocessModel)
    notch: PreprocessNotchModel = field(default_factory=PreprocessNotchModel)


@dataclass(slots=True)
class GprTxGeometryModel:
    """One transmitter geometry record keyed by output switch position.

    y_m / z_m default to 0 so 1D antenna layouts keep their pre-3D semantics.
    """

    output_pos: int = 0
    x_m: float = 0.0
    y_m: float = 0.0
    z_m: float = 0.0


@dataclass(slots=True)
class GprRxGeometryModel:
    """One receiver geometry record keyed by input switch position."""

    input_pos: int = 0
    x_m: float = 0.0
    y_m: float = 0.0
    z_m: float = 0.0


@dataclass(slots=True)
class GprModel:
    """Stable GPR configuration saved in run_config.json."""

    relative_permittivity: float = 1.0
    tx_geometry: list[GprTxGeometryModel] = field(default_factory=list)
    rx_geometry: list[GprRxGeometryModel] = field(default_factory=list)


@dataclass(slots=True)
class RunConfigModel:
    """Top-level runtime config model consumed by C++ processes and GUI."""

    radar: RadarModel = field(default_factory=RadarModel)
    input_switch: SwitchModel = field(default_factory=lambda: SwitchModel(name=""))
    output_switch: SwitchModel = field(default_factory=lambda: SwitchModel(name=""))
    rings: RingsModel = field(default_factory=RingsModel)
    runtime: RuntimeModel = field(default_factory=RuntimeModel)
    preprocess: PreprocessModel = field(default_factory=PreprocessModel)
    gpr: GprModel = field(default_factory=GprModel)
    combos: list[ComboModel] = field(default_factory=list)

    LIBREVNA_MODEL = "librevna"
    LIBREVNA_MULTI_MODEL = "librevna_multi"
    COMPACT_M_K209_MODEL = "compact_m_k209"
    KAMIL_ADC_MODEL = "kamil_adc"
    MULTI_DEVICE_INPUT_POSITIONS = 4
    MULTI_DEVICE_OUTPUT_POSITIONS = 2

    @staticmethod
    def build_full_combos(input_positions: int, output_positions: int) -> list[ComboModel]:
        """Build full Cartesian product of input/output switch positions."""
        return [
            ComboModel(input=input_pos, output=output_pos)
            for output_pos in range(output_positions)
            for input_pos in range(input_positions)
        ]

    @classmethod
    def build_multi_device_virtual_combos(cls) -> list[ComboModel]:
        """Build fixed virtual combo matrix for one master and two slave devices."""
        return cls.build_full_combos(
            cls.MULTI_DEVICE_INPUT_POSITIONS,
            cls.MULTI_DEVICE_OUTPUT_POSITIONS,
        )

    @property
    def is_multi_device(self) -> bool:
        """Return whether this config targets synchronized multi-device acquisition."""
        return self.radar.model == self.LIBREVNA_MULTI_MODEL

    @property
    def is_kamil_adc(self) -> bool:
        """Return whether this config targets the external Kamil ADC acquisition path."""
        return self.radar.model == self.KAMIL_ADC_MODEL

    def radar_key_extra_parts(self) -> list[str]:
        """Return model-specific identity parts that affect captured data."""
        if self.is_multi_device:
            return list(self.radar.multi_device.slave_serials)
        if self.is_kamil_adc:
            payload = {
                "kamil_adc": {
                    "project_dir": self.radar.kamil_adc.project_dir,
                    "executable_path": self.radar.kamil_adc.executable_path,
                    "tty_path": self.radar.kamil_adc.tty_path,
                    "args": list(self.radar.kamil_adc.args),
                    "env": dict(sorted(self.radar.kamil_adc.env.items())),
                },
                "laser_control": {
                    "enabled": self.radar.laser_control.enabled,
                    "port": self.radar.laser_control.port,
                    "mode": self.radar.laser_control.mode,
                    "pi_coeff1_p": self.radar.laser_control.pi_coeff1_p,
                    "pi_coeff1_i": self.radar.laser_control.pi_coeff1_i,
                    "pi_coeff2_p": self.radar.laser_control.pi_coeff2_p,
                    "pi_coeff2_i": self.radar.laser_control.pi_coeff2_i,
                    "manual": {
                        "temp1": self.radar.laser_control.manual.temp1,
                        "temp2": self.radar.laser_control.manual.temp2,
                        "current1": self.radar.laser_control.manual.current1,
                        "current2": self.radar.laser_control.manual.current2,
                    },
                    "variation": {
                        "variation_type": self.radar.laser_control.variation.variation_type,
                        "static_temp1": self.radar.laser_control.variation.static_temp1,
                        "static_temp2": self.radar.laser_control.variation.static_temp2,
                        "static_current1": self.radar.laser_control.variation.static_current1,
                        "static_current2": self.radar.laser_control.variation.static_current2,
                        "min_value": self.radar.laser_control.variation.min_value,
                        "max_value": self.radar.laser_control.variation.max_value,
                        "step": self.radar.laser_control.variation.step,
                        "time_step": self.radar.laser_control.variation.time_step,
                        "delay_time": self.radar.laser_control.variation.delay_time,
                    },
                },
            }
            encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"))
            digest = hashlib.sha256(encoded.encode("utf-8")).hexdigest()[:16]
            return [f"kamil_{digest}"]
        return []

    def apply_device_model_constraints(self) -> None:
        """Apply only required wire-format constraints for the selected device model."""
        if not self.is_multi_device:
            return

        self.radar.model = self.LIBREVNA_MULTI_MODEL
        self.output_switch.name = self.output_switch.name or "virtual_output"
        self.output_switch.driver_mode = "mock"
        self.output_switch.driver = self.output_switch.driver or "h7992"
        self.output_switch.radar_port = 1
        self.output_switch.positions = self.MULTI_DEVICE_OUTPUT_POSITIONS
        self.output_switch.default_position = 0

        self.input_switch.name = self.input_switch.name or "virtual_input"
        self.input_switch.driver_mode = "mock"
        self.input_switch.driver = self.input_switch.driver or "h7992"
        self.input_switch.radar_port = 2
        self.input_switch.positions = self.MULTI_DEVICE_INPUT_POSITIONS
        self.input_switch.default_position = 0
        self.combos = self.build_multi_device_virtual_combos()

    def ensure_combos(self) -> None:
        """Populate combos with full matrix when no explicit run combos are set."""
        if self.is_multi_device:
            self.apply_device_model_constraints()
            return
        if self.combos:
            return
        self.combos = self.build_full_combos(self.input_switch.positions, self.output_switch.positions)

    @classmethod
    def from_dict(cls, payload: dict[str, Any]) -> RunConfigModel:
        """Build model from JSON-like payload using codec layer."""
        from python_app.models.run_config_codec import run_config_from_dict

        return run_config_from_dict(payload)

    @classmethod
    def load_from_path(cls, path: Path) -> RunConfigModel:
        """Load JSON file from disk and decode into model."""
        payload = json.loads(path.read_text(encoding="utf-8"))
        if not isinstance(payload, dict):
            raise ValueError(f"Config root must be JSON object: {path}")
        return cls.from_dict(payload)

    def clone(self) -> RunConfigModel:
        """Create deep copy through codec round-trip."""
        return RunConfigModel.from_dict(self.to_dict())

    def to_dict(self) -> dict[str, Any]:
        """Encode model into JSON-serializable dictionary."""
        from python_app.models.run_config_codec import run_config_to_dict

        return run_config_to_dict(self)
