"""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,
    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 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")

    model.radar.model = str(radar_payload.get("model", model.radar.model))
    model.radar.serial = str(radar_payload.get("serial", model.radar.serial))
    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))

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

    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.preprocess.calibration_set = str(preprocess_payload.get("calibration_set", model.preprocess.calibration_set))
    model.preprocess.reference_set = str(preprocess_payload.get("reference_set", model.preprocess.reference_set))
    model.preprocess.calibration_bundle_path = str(
        preprocess_payload.get("calibration_bundle_path", model.preprocess.calibration_bundle_path)
    )
    model.preprocess.reference_bundle_path = str(
        preprocess_payload.get("reference_bundle_path", model.preprocess.reference_bundle_path)
    )

    model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode))
    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)),
                )
            )
    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)),
                )
            )
    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()
    return {
        "radar": {
            "model": model.radar.model,
            "serial": model.radar.serial,
            "driver_mode": model.radar.driver_mode,
            "mock_signal_hz": model.radar.mock_signal_hz,
            "sweep": {
                "start_hz": model.radar.sweep.start_hz,
                "stop_hz": model.radar.sweep.stop_hz,
                "points": model.radar.sweep.points,
                "if_bandwidth_hz": model.radar.sweep.if_bandwidth_hz,
                "stimulus_power_dbm": model.radar.sweep.power_dbm,
            },
        },
        "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,
            "combos": [{"input": combo.input, "output": combo.output} for combo in model.combos],
        },
        "preprocess": {
            "calibration_set": model.preprocess.calibration_set,
            "reference_set": model.preprocess.reference_set,
            "calibration_bundle_path": model.preprocess.calibration_bundle_path,
            "reference_bundle_path": model.preprocess.reference_bundle_path,
        },
        "gpr": {
            "mode": model.gpr.mode,
            "relative_permittivity": model.gpr.relative_permittivity,
            "tx_geometry": [
                {
                    "output_pos": entry.output_pos,
                    "x_m": entry.x_m,
                }
                for entry in model.gpr.tx_geometry
            ],
            "rx_geometry": [
                {
                    "input_pos": entry.input_pos,
                    "x_m": entry.x_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,
            },
        },
    }
