"""Encoding and decoding logic for :mod:`python_app.models.run_config_schema`."""

from __future__ import annotations

from typing import Any

from python_app.models.run_config_schema import (
    ComboModel,
    GprRxGeometryModel,
    GprTxGeometryModel,
    PreprocessAssetModel,
    PreprocessNotchModel,
    RunConfigModel,
)
from python_app.models.run_config_validation import load_ring_payload, load_switch_payload, validate_gpr_model


def _as_dict(value: Any, context: str) -> dict[str, Any]:
    """Validate payload node is object-like, treating missing values as empty object."""
    if value is None:
        return {}
    if not isinstance(value, dict):
        raise ValueError(f"{context} must be a JSON object")
    return value


def _load_preprocess_asset(payload: dict[str, Any], target: PreprocessAssetModel) -> None:
    """Load preprocess asset fields into target model."""
    target.set_name = str(payload.get("set_name", target.set_name))
    target.bundle_path = str(payload.get("bundle_path", target.bundle_path))


def _load_string_list(payload: dict[str, Any], key: str, context: str) -> list[str]:
    """Load an optional list of strings with strict shape validation."""
    raw_value = payload.get(key, [])
    if raw_value is None:
        return []
    if not isinstance(raw_value, list):
        raise ValueError(f"{context}.{key} must be a JSON array")
    values: list[str] = []
    for index, item in enumerate(raw_value):
        if not isinstance(item, str):
            raise ValueError(f"{context}.{key}[{index}] must be a JSON string")
        values.append(item)
    return values


def _load_string_dict(payload: dict[str, Any], key: str, context: str) -> dict[str, str]:
    """Load an optional string-to-string dictionary with strict shape validation."""
    raw_value = payload.get(key, {})
    if raw_value is None:
        return {}
    if not isinstance(raw_value, dict):
        raise ValueError(f"{context}.{key} must be a JSON object")
    values: dict[str, str] = {}
    for item_key, item_value in raw_value.items():
        if not isinstance(item_key, str) or not isinstance(item_value, str):
            raise ValueError(f"{context}.{key} must contain only string keys and values")
        values[item_key] = item_value
    return values


def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
    """Decode JSON-like payload into :class:`RunConfigModel`."""
    # Schema carries only minimal-safe fallbacks; operational defaults live in run_config.json.
    model = RunConfigModel()

    radar_payload = _as_dict(payload.get("radar"), "radar")
    sweep_payload = _as_dict(radar_payload.get("sweep"), "radar.sweep")
    switches_payload = _as_dict(payload.get("switches"), "switches")
    port1_payload = _as_dict(switches_payload.get("port1"), "switches.port1")
    port2_payload = _as_dict(switches_payload.get("port2"), "switches.port2")
    run_payload = _as_dict(payload.get("run"), "run")
    preprocess_payload = _as_dict(payload.get("preprocess"), "preprocess")
    gpr_payload = _as_dict(payload.get("gpr"), "gpr")
    rings_payload = _as_dict(payload.get("rings"), "rings")
    raw_ring_payload = _as_dict(rings_payload.get("raw"), "rings.raw")
    raw_tap_ring_payload = _as_dict(rings_payload.get("raw_tap"), "rings.raw_tap")
    pre_ring_payload = _as_dict(rings_payload.get("preprocessed"), "rings.preprocessed")
    pre_tap_ring_payload = _as_dict(rings_payload.get("preprocessed_tap"), "rings.preprocessed_tap")
    result_ring_payload = _as_dict(rings_payload.get("results"), "rings.results")
    locator_server_payload = _as_dict(
        run_payload.get("locator_server", run_payload.get("locator")),
        "run.locator_server",
    )
    multi_device_payload = _as_dict(radar_payload.get("multi_device"), "radar.multi_device")
    kamil_adc_payload = _as_dict(radar_payload.get("kamil_adc"), "radar.kamil_adc")
    laser_control_payload = _as_dict(radar_payload.get("laser_control"), "radar.laser_control")

    model.radar.model = str(radar_payload.get("model", model.radar.model))
    model.radar.serial = str(radar_payload.get("serial", model.radar.serial))
    model.radar.remote_host = str(radar_payload.get("remote_host", model.radar.remote_host))
    model.radar.remote_port = int(radar_payload.get("remote_port", model.radar.remote_port))
    model.radar.driver_mode = str(radar_payload.get("driver_mode", model.radar.driver_mode))
    model.radar.mock_signal_hz = float(radar_payload.get("mock_signal_hz", model.radar.mock_signal_hz))

    model.radar.sweep.start_hz = float(sweep_payload.get("start_hz", model.radar.sweep.start_hz))
    model.radar.sweep.stop_hz = float(sweep_payload.get("stop_hz", model.radar.sweep.stop_hz))
    model.radar.sweep.points = int(sweep_payload.get("points", model.radar.sweep.points))
    model.radar.sweep.if_bandwidth_hz = float(
        sweep_payload.get("if_bandwidth_hz", model.radar.sweep.if_bandwidth_hz)
    )
    model.radar.sweep.power_dbm = float(sweep_payload.get("stimulus_power_dbm", model.radar.sweep.power_dbm))
    slave_serials_payload = multi_device_payload.get(
        "slave_serials",
        multi_device_payload.get("slave_serial_numbers", model.radar.multi_device.slave_serials),
    )
    if isinstance(slave_serials_payload, list):
        model.radar.multi_device.slave_serials = [str(value).strip() for value in slave_serials_payload if str(value).strip()]
    elif isinstance(slave_serials_payload, str):
        model.radar.multi_device.slave_serials = [
            value.strip()
            for value in slave_serials_payload.split(",")
            if value.strip()
        ]
    model.radar.multi_device.force_external_reference = bool(
        multi_device_payload.get(
            "force_external_reference",
            model.radar.multi_device.force_external_reference,
        )
    )
    model.radar.multi_device.recovery_attempts = int(
        multi_device_payload.get(
            "recovery_attempts",
            model.radar.multi_device.recovery_attempts,
        )
    )
    model.radar.kamil_adc.project_dir = str(
        kamil_adc_payload.get("project_dir", model.radar.kamil_adc.project_dir)
    )
    model.radar.kamil_adc.executable_path = str(
        kamil_adc_payload.get("executable_path", model.radar.kamil_adc.executable_path)
    )
    model.radar.kamil_adc.tty_path = str(
        kamil_adc_payload.get("tty_path", model.radar.kamil_adc.tty_path)
    )
    model.radar.kamil_adc.args = _load_string_list(kamil_adc_payload, "args", "radar.kamil_adc")
    model.radar.kamil_adc.env = _load_string_dict(kamil_adc_payload, "env", "radar.kamil_adc")
    model.radar.kamil_adc.startup_timeout_s = float(
        kamil_adc_payload.get("startup_timeout_s", model.radar.kamil_adc.startup_timeout_s)
    )
    model.radar.kamil_adc.sweep_timeout_s = float(
        kamil_adc_payload.get("sweep_timeout_s", model.radar.kamil_adc.sweep_timeout_s)
    )
    model.radar.kamil_adc.stop_timeout_s = float(
        kamil_adc_payload.get("stop_timeout_s", model.radar.kamil_adc.stop_timeout_s)
    )

    model.radar.laser_control.enabled = bool(
        laser_control_payload.get("enabled", model.radar.laser_control.enabled)
    )
    model.radar.laser_control.port = str(
        laser_control_payload.get("port", model.radar.laser_control.port)
    )
    model.radar.laser_control.mode = str(
        laser_control_payload.get("mode", model.radar.laser_control.mode)
    )
    model.radar.laser_control.pi_coeff1_p = int(
        laser_control_payload.get("pi_coeff1_p", model.radar.laser_control.pi_coeff1_p)
    )
    model.radar.laser_control.pi_coeff1_i = int(
        laser_control_payload.get("pi_coeff1_i", model.radar.laser_control.pi_coeff1_i)
    )
    model.radar.laser_control.pi_coeff2_p = int(
        laser_control_payload.get("pi_coeff2_p", model.radar.laser_control.pi_coeff2_p)
    )
    model.radar.laser_control.pi_coeff2_i = int(
        laser_control_payload.get("pi_coeff2_i", model.radar.laser_control.pi_coeff2_i)
    )
    laser_manual_payload = _as_dict(laser_control_payload.get("manual"), "radar.laser_control.manual")
    model.radar.laser_control.manual.temp1 = float(
        laser_manual_payload.get("temp1", model.radar.laser_control.manual.temp1)
    )
    model.radar.laser_control.manual.temp2 = float(
        laser_manual_payload.get("temp2", model.radar.laser_control.manual.temp2)
    )
    model.radar.laser_control.manual.current1 = float(
        laser_manual_payload.get("current1", model.radar.laser_control.manual.current1)
    )
    model.radar.laser_control.manual.current2 = float(
        laser_manual_payload.get("current2", model.radar.laser_control.manual.current2)
    )
    laser_variation_payload = _as_dict(
        laser_control_payload.get("variation"),
        "radar.laser_control.variation",
    )
    model.radar.laser_control.variation.variation_type = str(
        laser_variation_payload.get(
            "variation_type",
            model.radar.laser_control.variation.variation_type,
        )
    )
    model.radar.laser_control.variation.static_temp1 = float(
        laser_variation_payload.get(
            "static_temp1",
            model.radar.laser_control.variation.static_temp1,
        )
    )
    model.radar.laser_control.variation.static_temp2 = float(
        laser_variation_payload.get(
            "static_temp2",
            model.radar.laser_control.variation.static_temp2,
        )
    )
    model.radar.laser_control.variation.static_current1 = float(
        laser_variation_payload.get(
            "static_current1",
            model.radar.laser_control.variation.static_current1,
        )
    )
    model.radar.laser_control.variation.static_current2 = float(
        laser_variation_payload.get(
            "static_current2",
            model.radar.laser_control.variation.static_current2,
        )
    )
    model.radar.laser_control.variation.min_value = float(
        laser_variation_payload.get("min_value", model.radar.laser_control.variation.min_value)
    )
    model.radar.laser_control.variation.max_value = float(
        laser_variation_payload.get("max_value", model.radar.laser_control.variation.max_value)
    )
    model.radar.laser_control.variation.step = float(
        laser_variation_payload.get("step", model.radar.laser_control.variation.step)
    )
    model.radar.laser_control.variation.time_step = int(
        laser_variation_payload.get("time_step", model.radar.laser_control.variation.time_step)
    )
    model.radar.laser_control.variation.delay_time = int(
        laser_variation_payload.get("delay_time", model.radar.laser_control.variation.delay_time)
    )

    load_switch_payload(port1_payload, model.output_switch)
    load_switch_payload(port2_payload, model.input_switch)
    model.apply_device_model_constraints()

    model.runtime.settling_ms = int(run_payload.get("settling_ms", model.runtime.settling_ms))
    model.runtime.idle_sleep_ms = int(run_payload.get("idle_sleep_ms", model.runtime.idle_sleep_ms))
    model.runtime.continuous = bool(run_payload.get("continuous", model.runtime.continuous))
    model.runtime.processing_live_config_path = str(
        run_payload.get("processing_live_config_path", model.runtime.processing_live_config_path)
    )
    model.runtime.locator_server.device_id = int(
        locator_server_payload.get("device_id", model.runtime.locator_server.device_id)
    )
    model.runtime.locator_server.protocol_version = int(
        locator_server_payload.get(
            "protocol_version",
            model.runtime.locator_server.protocol_version,
        )
    )
    model.runtime.locator_server.host = str(
        locator_server_payload.get("host", model.runtime.locator_server.host)
    )
    model.runtime.locator_server.port = int(
        locator_server_payload.get("port", model.runtime.locator_server.port)
    )
    model.runtime.locator_server.max_payload_bytes = int(
        locator_server_payload.get(
            "max_payload_bytes",
            model.runtime.locator_server.max_payload_bytes,
        )
    )
    model.runtime.locator_server.client_queue_size = int(
        locator_server_payload.get(
            "client_queue_size",
            model.runtime.locator_server.client_queue_size,
        )
    )
    model.runtime.locator_server.logger_name = str(
        locator_server_payload.get(
            "logger_name",
            model.runtime.locator_server.logger_name,
        )
    )

    s21_preprocess_payload = _as_dict(preprocess_payload.get("s21"), "preprocess.s21")
    _load_preprocess_asset(
        _as_dict(s21_preprocess_payload.get("calibration"), "preprocess.s21.calibration"),
        model.preprocess.s21.calibration,
    )
    _load_preprocess_asset(
        _as_dict(s21_preprocess_payload.get("reference"), "preprocess.s21.reference"),
        model.preprocess.s21.reference,
    )

    s11_preprocess_payload = _as_dict(preprocess_payload.get("s11"), "preprocess.s11")
    s11_calibration_payload = _as_dict(s11_preprocess_payload.get("calibration"), "preprocess.s11.calibration")
    _load_preprocess_asset(
        _as_dict(s11_calibration_payload.get("open"), "preprocess.s11.calibration.open"),
        model.preprocess.s11.calibration.open,
    )
    _load_preprocess_asset(
        _as_dict(s11_calibration_payload.get("short"), "preprocess.s11.calibration.short"),
        model.preprocess.s11.calibration.short,
    )
    _load_preprocess_asset(
        _as_dict(s11_calibration_payload.get("load"), "preprocess.s11.calibration.load"),
        model.preprocess.s11.calibration.load,
    )
    _load_preprocess_asset(
        _as_dict(s11_preprocess_payload.get("reference"), "preprocess.s11.reference"),
        model.preprocess.s11.reference,
    )
    notch_payload = _as_dict(preprocess_payload.get("notch"), "preprocess.notch")
    model.preprocess.notch = PreprocessNotchModel(
        enabled=bool(notch_payload.get("enabled", model.preprocess.notch.enabled)),
        taper_width_hz=float(notch_payload.get("taper_width_hz", model.preprocess.notch.taper_width_hz)),
        taper_type=str(notch_payload.get("taper_type", model.preprocess.notch.taper_type)),
        bands_hz=[],
    )
    bands_payload = notch_payload.get("bands_hz", [])
    if isinstance(bands_payload, list):
        for band in bands_payload:
            if isinstance(band, (list, tuple)) and len(band) == 2:
                model.preprocess.notch.bands_hz.append((float(band[0]), float(band[1])))

    model.gpr.relative_permittivity = float(
        gpr_payload.get("relative_permittivity", model.gpr.relative_permittivity)
    )
    model.gpr.tx_geometry = []
    tx_geometry_payload = gpr_payload.get("tx_geometry", [])
    if isinstance(tx_geometry_payload, list):
        for entry in tx_geometry_payload:
            entry_payload = _as_dict(entry, "gpr.tx_geometry[]")
            model.gpr.tx_geometry.append(
                GprTxGeometryModel(
                    output_pos=int(entry_payload.get("output_pos", 0)),
                    x_m=float(entry_payload.get("x_m", 0.0)),
                    y_m=float(entry_payload.get("y_m", 0.0)),
                    z_m=float(entry_payload.get("z_m", 0.0)),
                )
            )
    model.gpr.rx_geometry = []
    rx_geometry_payload = gpr_payload.get("rx_geometry", [])
    if isinstance(rx_geometry_payload, list):
        for entry in rx_geometry_payload:
            entry_payload = _as_dict(entry, "gpr.rx_geometry[]")
            model.gpr.rx_geometry.append(
                GprRxGeometryModel(
                    input_pos=int(entry_payload.get("input_pos", 0)),
                    x_m=float(entry_payload.get("x_m", 0.0)),
                    y_m=float(entry_payload.get("y_m", 0.0)),
                    z_m=float(entry_payload.get("z_m", 0.0)),
                )
            )
    model.apply_device_model_constraints()
    validate_gpr_model(
        model.gpr,
        input_switch_positions=model.input_switch.positions,
        output_switch_positions=model.output_switch.positions,
    )

    load_ring_payload(raw_ring_payload, model.rings.raw)
    load_ring_payload(raw_tap_ring_payload, model.rings.raw_tap)
    load_ring_payload(pre_ring_payload, model.rings.preprocessed)
    load_ring_payload(pre_tap_ring_payload, model.rings.preprocessed_tap)
    load_ring_payload(result_ring_payload, model.rings.results)

    combos_payload = run_payload.get("combos", [])
    model.combos = []
    if isinstance(combos_payload, list):
        for combo in combos_payload:
            combo_payload = _as_dict(combo, "run.combos[]")
            model.combos.append(
                ComboModel(
                    input=int(combo_payload.get("input", 0)),
                    output=int(combo_payload.get("output", 0)),
                )
            )

    model.ensure_combos()
    return model


def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
    """Encode :class:`RunConfigModel` to C++ pipeline-compatible JSON structure."""
    model.ensure_combos()
    sweep_payload = {
        "start_hz": model.radar.sweep.start_hz,
        "stop_hz": model.radar.sweep.stop_hz,
        "if_bandwidth_hz": model.radar.sweep.if_bandwidth_hz,
        "stimulus_power_dbm": model.radar.sweep.power_dbm,
    }
    if not model.is_kamil_adc:
        sweep_payload["points"] = model.radar.sweep.points

    return {
        "radar": {
            "model": model.radar.model,
            "serial": model.radar.serial,
            "remote_host": model.radar.remote_host,
            "remote_port": model.radar.remote_port,
            "driver_mode": model.radar.driver_mode,
            "mock_signal_hz": model.radar.mock_signal_hz,
            "multi_device": {
                "slave_serials": list(model.radar.multi_device.slave_serials),
                "force_external_reference": model.radar.multi_device.force_external_reference,
                "recovery_attempts": model.radar.multi_device.recovery_attempts,
            },
            "kamil_adc": {
                "project_dir": model.radar.kamil_adc.project_dir,
                "executable_path": model.radar.kamil_adc.executable_path,
                "tty_path": model.radar.kamil_adc.tty_path,
                "args": list(model.radar.kamil_adc.args),
                "env": dict(model.radar.kamil_adc.env),
                "startup_timeout_s": model.radar.kamil_adc.startup_timeout_s,
                "sweep_timeout_s": model.radar.kamil_adc.sweep_timeout_s,
                "stop_timeout_s": model.radar.kamil_adc.stop_timeout_s,
            },
            "laser_control": {
                "enabled": model.radar.laser_control.enabled,
                "port": model.radar.laser_control.port,
                "mode": model.radar.laser_control.mode,
                "pi_coeff1_p": model.radar.laser_control.pi_coeff1_p,
                "pi_coeff1_i": model.radar.laser_control.pi_coeff1_i,
                "pi_coeff2_p": model.radar.laser_control.pi_coeff2_p,
                "pi_coeff2_i": model.radar.laser_control.pi_coeff2_i,
                "manual": {
                    "temp1": model.radar.laser_control.manual.temp1,
                    "temp2": model.radar.laser_control.manual.temp2,
                    "current1": model.radar.laser_control.manual.current1,
                    "current2": model.radar.laser_control.manual.current2,
                },
                "variation": {
                    "variation_type": model.radar.laser_control.variation.variation_type,
                    "static_temp1": model.radar.laser_control.variation.static_temp1,
                    "static_temp2": model.radar.laser_control.variation.static_temp2,
                    "static_current1": model.radar.laser_control.variation.static_current1,
                    "static_current2": model.radar.laser_control.variation.static_current2,
                    "min_value": model.radar.laser_control.variation.min_value,
                    "max_value": model.radar.laser_control.variation.max_value,
                    "step": model.radar.laser_control.variation.step,
                    "time_step": model.radar.laser_control.variation.time_step,
                    "delay_time": model.radar.laser_control.variation.delay_time,
                },
            },
            "sweep": sweep_payload,
        },
        "switches": {
            "port1": {
                "name": model.output_switch.name,
                "driver_mode": model.output_switch.driver_mode,
                "driver": model.output_switch.driver,
                "radar_port": model.output_switch.radar_port,
                "positions": model.output_switch.positions,
                "default_position": model.output_switch.default_position,
                "gpio_chip": model.output_switch.gpio_chip,
                "pin_a": model.output_switch.pin_a,
                "pin_b": model.output_switch.pin_b,
                "invert_logic": model.output_switch.invert_logic,
            },
            "port2": {
                "name": model.input_switch.name,
                "driver_mode": model.input_switch.driver_mode,
                "driver": model.input_switch.driver,
                "radar_port": model.input_switch.radar_port,
                "positions": model.input_switch.positions,
                "default_position": model.input_switch.default_position,
                "gpio_chip": model.input_switch.gpio_chip,
                "pin_a": model.input_switch.pin_a,
                "pin_b": model.input_switch.pin_b,
                "invert_logic": model.input_switch.invert_logic,
            },
        },
        "run": {
            "settling_ms": model.runtime.settling_ms,
            "idle_sleep_ms": model.runtime.idle_sleep_ms,
            "continuous": model.runtime.continuous,
            "processing_live_config_path": model.runtime.processing_live_config_path,
            "locator_server": {
                "device_id": model.runtime.locator_server.device_id,
                "protocol_version": model.runtime.locator_server.protocol_version,
                "host": model.runtime.locator_server.host,
                "port": model.runtime.locator_server.port,
                "max_payload_bytes": model.runtime.locator_server.max_payload_bytes,
                "client_queue_size": model.runtime.locator_server.client_queue_size,
                "logger_name": model.runtime.locator_server.logger_name,
            },
            "combos": [{"input": combo.input, "output": combo.output} for combo in model.combos],
        },
        "preprocess": {
            "s21": {
                "calibration": {
                    "set_name": model.preprocess.s21.calibration.set_name,
                    "bundle_path": model.preprocess.s21.calibration.bundle_path,
                },
                "reference": {
                    "set_name": model.preprocess.s21.reference.set_name,
                    "bundle_path": model.preprocess.s21.reference.bundle_path,
                },
            },
            "s11": {
                "calibration": {
                    "open": {
                        "set_name": model.preprocess.s11.calibration.open.set_name,
                        "bundle_path": model.preprocess.s11.calibration.open.bundle_path,
                    },
                    "short": {
                        "set_name": model.preprocess.s11.calibration.short.set_name,
                        "bundle_path": model.preprocess.s11.calibration.short.bundle_path,
                    },
                    "load": {
                        "set_name": model.preprocess.s11.calibration.load.set_name,
                        "bundle_path": model.preprocess.s11.calibration.load.bundle_path,
                    },
                },
                "reference": {
                    "set_name": model.preprocess.s11.reference.set_name,
                    "bundle_path": model.preprocess.s11.reference.bundle_path,
                },
            },
            "notch": {
                "enabled": model.preprocess.notch.enabled,
                "bands_hz": [[low_hz, high_hz] for low_hz, high_hz in model.preprocess.notch.bands_hz],
                "taper_width_hz": model.preprocess.notch.taper_width_hz,
                "taper_type": model.preprocess.notch.taper_type,
            },
        },
        "gpr": {
            "relative_permittivity": model.gpr.relative_permittivity,
            "tx_geometry": [
                {
                    "output_pos": entry.output_pos,
                    "x_m": entry.x_m,
                    "y_m": entry.y_m,
                    "z_m": entry.z_m,
                }
                for entry in model.gpr.tx_geometry
            ],
            "rx_geometry": [
                {
                    "input_pos": entry.input_pos,
                    "x_m": entry.x_m,
                    "y_m": entry.y_m,
                    "z_m": entry.z_m,
                }
                for entry in model.gpr.rx_geometry
            ],
        },
        "rings": {
            "raw": {
                "name": model.rings.raw.name,
                "capacity": model.rings.raw.capacity,
                "slot_size_bytes": model.rings.raw.slot_size_bytes,
            },
            "raw_tap": {
                "name": model.rings.raw_tap.name,
                "capacity": model.rings.raw_tap.capacity,
                "slot_size_bytes": model.rings.raw_tap.slot_size_bytes,
            },
            "preprocessed": {
                "name": model.rings.preprocessed.name,
                "capacity": model.rings.preprocessed.capacity,
                "slot_size_bytes": model.rings.preprocessed.slot_size_bytes,
            },
            "preprocessed_tap": {
                "name": model.rings.preprocessed_tap.name,
                "capacity": model.rings.preprocessed_tap.capacity,
                "slot_size_bytes": model.rings.preprocessed_tap.slot_size_bytes,
            },
            "results": {
                "name": model.rings.results.name,
                "capacity": model.rings.results.capacity,
                "slot_size_bytes": model.rings.results.slot_size_bytes,
            },
        },
    }
