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

from __future__ import annotations

from typing import Any

from python_app.models.gui_profile_schema import (
    GuiBscanStateModel,
    GuiDataActionsStateModel,
    GuiGprStateModel,
    GuiLegacyGprStateModel,
    GuiPassThroughStateModel,
    GuiPreprocessDialogStateModel,
    GuiProcessingStateModel,
    GuiProfileModel,
    GuiStateModel,
    GuiSwitchStateModel,
)
from python_app.models.run_config_model import RunConfigModel


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 _optional_string(object_payload: dict[str, Any], key: str, fallback: str, context: str) -> str:
    """Return optional string field with type validation."""
    raw_value = object_payload.get(key, fallback)
    if not isinstance(raw_value, str):
        raise ValueError(f"{context}.{key} must be a JSON string")
    return raw_value


def _optional_bool(object_payload: dict[str, Any], key: str, fallback: bool, context: str) -> bool:
    """Return optional boolean field with type validation."""
    raw_value = object_payload.get(key, fallback)
    if not isinstance(raw_value, bool):
        raise ValueError(f"{context}.{key} must be a JSON bool")
    return raw_value


def _optional_int(object_payload: dict[str, Any], key: str, fallback: int, context: str) -> int:
    """Return optional integer field with type validation."""
    raw_value = object_payload.get(key, fallback)
    if isinstance(raw_value, bool) or not isinstance(raw_value, int):
        raise ValueError(f"{context}.{key} must be a JSON integer")
    return int(raw_value)


def _optional_float(object_payload: dict[str, Any], key: str, fallback: float, context: str) -> float:
    """Return optional numeric field with type validation."""
    raw_value = object_payload.get(key, fallback)
    if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float)):
        raise ValueError(f"{context}.{key} must be a JSON number")
    return float(raw_value)


def _legacy_gpr_mode_from_algorithm(value: str) -> str | None:
    """Translate the short-lived nested GPR algorithm field into legacy mode."""
    if value == "legacy_point":
        return "point"
    if value == "legacy_extended":
        return "extended"
    return None


def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
    """Decode JSON-like payload into :class:`GuiProfileModel`."""
    profile = GuiProfileModel(run_config=RunConfigModel.from_dict(payload), gui=None)
    gui_payload = payload.get("gui")
    if gui_payload is None:
        return profile

    gui_object = _as_dict(gui_payload, "gui")
    gui = GuiStateModel()
    gui.version = _optional_int(gui_object, "version", gui.version, "gui")
    if gui.version != 1:
        raise ValueError(f"Unsupported gui.version: {gui.version}")

    switches_object = _as_dict(gui_object.get("switches"), "gui.switches")
    gui.switches = GuiSwitchStateModel(
        combo_mode=_optional_string(switches_object, "combo_mode", gui.switches.combo_mode, "gui.switches"),
        combos_text=_optional_string(switches_object, "combos_text", gui.switches.combos_text, "gui.switches"),
        single_input=_optional_string(switches_object, "single_input", gui.switches.single_input, "gui.switches"),
        single_output=_optional_string(
            switches_object,
            "single_output",
            gui.switches.single_output,
            "gui.switches",
        ),
    )
    if gui.switches.combo_mode not in {"text", "single"}:
        raise ValueError("gui.switches.combo_mode must be either 'text' or 'single'")

    processing_object = _as_dict(gui_object.get("processing"), "gui.processing")
    pass_through_object = _as_dict(processing_object.get("pass_through"), "gui.processing.pass_through")
    bscan_object = _as_dict(processing_object.get("bscan"), "gui.processing.bscan")
    gpr_object = _as_dict(processing_object.get("gpr"), "gui.processing.gpr")
    legacy_gpr_object = _as_dict(processing_object.get("legacy_gpr"), "gui.processing.legacy_gpr")
    root_gpr_object = payload.get("gpr")

    selected_mode = _optional_string(
        processing_object,
        "selected_mode",
        gui.processing.selected_mode,
        "gui.processing",
    )
    legacy_mode_default = gui.processing.legacy_gpr.mode
    if isinstance(root_gpr_object, dict):
        if root_gpr_object.get("mode") == "point":
            legacy_mode_default = "point"
        elif root_gpr_object.get("mode") == "extended":
            legacy_mode_default = "extended"
    legacy_algorithm_mode = _legacy_gpr_mode_from_algorithm(str(gpr_object.get("algorithm", "")))
    if legacy_algorithm_mode is not None:
        legacy_mode_default = legacy_algorithm_mode
    has_legacy_root_gpr_mode = (
        isinstance(root_gpr_object, dict)
        and root_gpr_object.get("mode") in {"point", "extended"}
    )
    if selected_mode == "gpr" and (legacy_algorithm_mode is not None or has_legacy_root_gpr_mode):
        selected_mode = "legacy_gpr"

    gpr_context = "gui.processing.gpr"
    legacy_gpr_context = "gui.processing.legacy_gpr"

    def legacy_string(key: str, fallback: str) -> str:
        """Read a legacy field, falling back to the old nested gpr object."""
        return _optional_string(
            legacy_gpr_object,
            key,
            _optional_string(gpr_object, key, fallback, gpr_context),
            legacy_gpr_context,
        )

    def legacy_bool(key: str, fallback: bool) -> bool:
        """Read a legacy bool, falling back to the old nested gpr object."""
        return _optional_bool(
            legacy_gpr_object,
            key,
            _optional_bool(gpr_object, key, fallback, gpr_context),
            legacy_gpr_context,
        )

    def legacy_int(key: str, fallback: int) -> int:
        """Read a legacy integer, falling back to the old nested gpr object."""
        return _optional_int(
            legacy_gpr_object,
            key,
            _optional_int(gpr_object, key, fallback, gpr_context),
            legacy_gpr_context,
        )

    def legacy_float(key: str, fallback: float) -> float:
        """Read a legacy float, falling back to the old nested gpr object."""
        return _optional_float(
            legacy_gpr_object,
            key,
            _optional_float(gpr_object, key, fallback, gpr_context),
            legacy_gpr_context,
        )

    gui.processing = GuiProcessingStateModel(
        selected_mode=selected_mode,
        pass_through=GuiPassThroughStateModel(
            show_magnitude=_optional_bool(
                pass_through_object,
                "show_magnitude",
                gui.processing.pass_through.show_magnitude,
                "gui.processing.pass_through",
            ),
            show_phase=_optional_bool(
                pass_through_object,
                "show_phase",
                gui.processing.pass_through.show_phase,
                "gui.processing.pass_through",
            ),
            combo_filter=_optional_string(
                pass_through_object,
                "combo_filter",
                gui.processing.pass_through.combo_filter,
                "gui.processing.pass_through",
            ),
            fixed_y_enabled=_optional_bool(
                pass_through_object,
                "fixed_y_enabled",
                gui.processing.pass_through.fixed_y_enabled,
                "gui.processing.pass_through",
            ),
            y_min_db=_optional_float(
                pass_through_object,
                "y_min_db",
                gui.processing.pass_through.y_min_db,
                "gui.processing.pass_through",
            ),
            y_max_db=_optional_float(
                pass_through_object,
                "y_max_db",
                gui.processing.pass_through.y_max_db,
                "gui.processing.pass_through",
            ),
        ),
        bscan=GuiBscanStateModel(
            axis=_optional_string(bscan_object, "axis", gui.processing.bscan.axis, "gui.processing.bscan"),
            cut_m=_optional_float(bscan_object, "cut_m", gui.processing.bscan.cut_m, "gui.processing.bscan"),
            max_depth_m=_optional_float(
                bscan_object,
                "max_depth_m",
                gui.processing.bscan.max_depth_m,
                "gui.processing.bscan",
            ),
            gain=_optional_float(bscan_object, "gain", gui.processing.bscan.gain, "gui.processing.bscan"),
            start_freq_mhz=_optional_float(
                bscan_object,
                "start_freq_mhz",
                gui.processing.bscan.start_freq_mhz,
                "gui.processing.bscan",
            ),
            stop_freq_mhz=_optional_float(
                bscan_object,
                "stop_freq_mhz",
                gui.processing.bscan.stop_freq_mhz,
                "gui.processing.bscan",
            ),
            subtract_mean_ascan=_optional_bool(
                bscan_object,
                "subtract_mean_ascan",
                gui.processing.bscan.subtract_mean_ascan,
                "gui.processing.bscan",
            ),
        ),
        gpr=GuiGprStateModel(
            input_positions=_optional_string(
                gpr_object,
                "input_positions",
                gui.processing.gpr.input_positions,
                "gui.processing.gpr",
            ),
            output_positions=_optional_string(
                gpr_object,
                "output_positions",
                gui.processing.gpr.output_positions,
                "gui.processing.gpr",
            ),
            min_depth_m=_optional_float(
                gpr_object,
                "min_depth_m",
                gui.processing.gpr.min_depth_m,
                "gui.processing.gpr",
            ),
            max_depth_m=_optional_float(
                gpr_object,
                "max_depth_m",
                gui.processing.gpr.max_depth_m,
                "gui.processing.gpr",
            ),
            range_comp_power=_optional_float(
                gpr_object,
                "range_comp_power",
                gui.processing.gpr.range_comp_power,
                "gui.processing.gpr",
            ),
            angle_comp_power=_optional_float(
                gpr_object,
                "angle_comp_power",
                gui.processing.gpr.angle_comp_power,
                "gui.processing.gpr",
            ),
            score_mode=_optional_string(
                gpr_object,
                "score_mode",
                gui.processing.gpr.score_mode,
                "gui.processing.gpr",
            ),
            max_detected_objects_to_draw=_optional_int(
                gpr_object,
                "max_detected_objects_to_draw",
                gui.processing.gpr.max_detected_objects_to_draw,
                "gui.processing.gpr",
            ),
            draw_top_m_objects=_optional_int(
                gpr_object,
                "draw_top_m_objects",
                gui.processing.gpr.draw_top_m_objects,
                "gui.processing.gpr",
            ),
            start_freq_mhz=_optional_float(
                gpr_object,
                "start_freq_mhz",
                gui.processing.gpr.start_freq_mhz,
                "gui.processing.gpr",
            ),
            stop_freq_mhz=_optional_float(
                gpr_object,
                "stop_freq_mhz",
                gui.processing.gpr.stop_freq_mhz,
                "gui.processing.gpr",
            ),
            background_subtract_enabled=_optional_bool(
                gpr_object,
                "background_subtract_enabled",
                gui.processing.gpr.background_subtract_enabled,
                "gui.processing.gpr",
            ),
            background_mean_count=_optional_int(
                gpr_object,
                "background_mean_count",
                gui.processing.gpr.background_mean_count,
                "gui.processing.gpr",
            ),
            remove_sidelobe_objects_enabled=_optional_bool(
                gpr_object,
                "remove_sidelobe_objects_enabled",
                gui.processing.gpr.remove_sidelobe_objects_enabled,
                "gui.processing.gpr",
            ),
            imaging_plane_y_m=_optional_float(
                gpr_object,
                "imaging_plane_y_m",
                gui.processing.gpr.imaging_plane_y_m,
                "gui.processing.gpr",
            ),
            render_mode=_optional_string(
                gpr_object,
                "render_mode",
                gui.processing.gpr.render_mode,
                "gui.processing.gpr",
            ),
            min_visible_score=_optional_float(
                gpr_object,
                "min_visible_score",
                gui.processing.gpr.min_visible_score,
                "gui.processing.gpr",
            ),
            visible_x_min_m=_optional_float(
                gpr_object,
                "visible_x_min_m",
                gui.processing.gpr.visible_x_min_m,
                "gui.processing.gpr",
            ),
            visible_x_max_m=_optional_float(
                gpr_object,
                "visible_x_max_m",
                gui.processing.gpr.visible_x_max_m,
                "gui.processing.gpr",
            ),
            visible_z_min_m=_optional_float(
                gpr_object,
                "visible_z_min_m",
                gui.processing.gpr.visible_z_min_m,
                "gui.processing.gpr",
            ),
            visible_z_max_m=_optional_float(
                gpr_object,
                "visible_z_max_m",
                gui.processing.gpr.visible_z_max_m,
                "gui.processing.gpr",
            ),
        ),
        legacy_gpr=GuiLegacyGprStateModel(
            mode=legacy_string("mode", legacy_mode_default),
            input_positions=legacy_string("input_positions", gui.processing.legacy_gpr.input_positions),
            output_positions=legacy_string("output_positions", gui.processing.legacy_gpr.output_positions),
            min_depth_m=legacy_float("min_depth_m", gui.processing.legacy_gpr.min_depth_m),
            max_depth_m=legacy_float("max_depth_m", gui.processing.legacy_gpr.max_depth_m),
            comp_power=legacy_float("comp_power", gui.processing.legacy_gpr.comp_power),
            start_freq_mhz=legacy_float("start_freq_mhz", gui.processing.legacy_gpr.start_freq_mhz),
            stop_freq_mhz=legacy_float("stop_freq_mhz", gui.processing.legacy_gpr.stop_freq_mhz),
            speed_m_s=legacy_float("speed_m_s", gui.processing.legacy_gpr.speed_m_s),
            ignore_socket_speed_enabled=legacy_bool(
                "ignore_socket_speed_enabled",
                gui.processing.legacy_gpr.ignore_socket_speed_enabled,
            ),
            look_angle_deg=legacy_float("look_angle_deg", gui.processing.legacy_gpr.look_angle_deg),
            snr_thresh=legacy_float("snr_thresh", gui.processing.legacy_gpr.snr_thresh),
            snr_comp_max=legacy_float("snr_comp_max", gui.processing.legacy_gpr.snr_comp_max),
            background_subtract_enabled=legacy_bool(
                "background_subtract_enabled",
                gui.processing.legacy_gpr.background_subtract_enabled,
            ),
            background_mean_count=legacy_int(
                "background_mean_count",
                gui.processing.legacy_gpr.background_mean_count,
            ),
            render_mode=legacy_string("render_mode", gui.processing.legacy_gpr.render_mode),
            min_visible_pair_count=legacy_int(
                "min_visible_pair_count",
                gui.processing.legacy_gpr.min_visible_pair_count,
            ),
            visible_x_min_m=legacy_float("visible_x_min_m", gui.processing.legacy_gpr.visible_x_min_m),
            visible_x_max_m=legacy_float("visible_x_max_m", gui.processing.legacy_gpr.visible_x_max_m),
            visible_z_min_m=legacy_float("visible_z_min_m", gui.processing.legacy_gpr.visible_z_min_m),
            visible_z_max_m=legacy_float("visible_z_max_m", gui.processing.legacy_gpr.visible_z_max_m),
        ),
    )
    if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr", "legacy_gpr"}:
        raise ValueError("gui.processing.selected_mode must be one of: pass_through, bscan, gpr, legacy_gpr")
    if gui.processing.bscan.axis not in {"abs", "real", "phase"}:
        raise ValueError("gui.processing.bscan.axis must be one of: abs, real, phase")
    if gui.processing.gpr.render_mode not in {"heatmap", "objects_only"}:
        raise ValueError("gui.processing.gpr.render_mode must be one of: heatmap, objects_only")
    if gui.processing.gpr.score_mode not in {"peak", "combined"}:
        raise ValueError("gui.processing.gpr.score_mode must be one of: peak, combined")
    if gui.processing.legacy_gpr.mode not in {"point", "extended"}:
        raise ValueError("gui.processing.legacy_gpr.mode must be one of: point, extended")
    if gui.processing.legacy_gpr.render_mode not in {"heatmap", "objects_only"}:
        raise ValueError("gui.processing.legacy_gpr.render_mode must be one of: heatmap, objects_only")
    if gui.processing.gpr.range_comp_power < 0.0:
        raise ValueError("gui.processing.gpr.range_comp_power must be >= 0")
    if gui.processing.gpr.angle_comp_power < 0.0:
        raise ValueError("gui.processing.gpr.angle_comp_power must be >= 0")
    if gui.processing.gpr.min_visible_score < 0.0:
        raise ValueError("gui.processing.gpr.min_visible_score must be >= 0")
    if gui.processing.gpr.max_detected_objects_to_draw < 0:
        raise ValueError("gui.processing.gpr.max_detected_objects_to_draw must be >= 0")
    if gui.processing.gpr.draw_top_m_objects < 0:
        raise ValueError("gui.processing.gpr.draw_top_m_objects must be >= 0")
    if gui.processing.legacy_gpr.comp_power < 0.0:
        raise ValueError("gui.processing.legacy_gpr.comp_power must be >= 0")
    if gui.processing.legacy_gpr.snr_thresh < 0.0:
        raise ValueError("gui.processing.legacy_gpr.snr_thresh must be >= 0")
    if gui.processing.legacy_gpr.snr_comp_max < 0.0:
        raise ValueError("gui.processing.legacy_gpr.snr_comp_max must be >= 0")
    if gui.processing.legacy_gpr.min_visible_pair_count < 1:
        raise ValueError("gui.processing.legacy_gpr.min_visible_pair_count must be >= 1")

    data_actions_object = _as_dict(gui_object.get("data_actions"), "gui.data_actions")
    gui.data_actions = GuiDataActionsStateModel(
        save_count=_optional_int(
            data_actions_object,
            "save_count",
            gui.data_actions.save_count,
            "gui.data_actions",
        ),
        save_path=_optional_string(
            data_actions_object,
            "save_path",
            gui.data_actions.save_path,
            "gui.data_actions",
        ),
        save_name=_optional_string(
            data_actions_object,
            "save_name",
            gui.data_actions.save_name,
            "gui.data_actions",
        ),
    )

    preprocess_dialog_object = _as_dict(gui_object.get("preprocess_dialog"), "gui.preprocess_dialog")
    gui.preprocess_dialog = GuiPreprocessDialogStateModel(
        set_name=_optional_string(
            preprocess_dialog_object,
            "set_name",
            gui.preprocess_dialog.set_name,
            "gui.preprocess_dialog",
        ),
        radar_config_dir=_optional_string(
            preprocess_dialog_object,
            "radar_config_dir",
            gui.preprocess_dialog.radar_config_dir,
            "gui.preprocess_dialog",
        ),
        use_all_radar_configs=_optional_bool(
            preprocess_dialog_object,
            "use_all_radar_configs",
            gui.preprocess_dialog.use_all_radar_configs,
            "gui.preprocess_dialog",
        ),
    )

    profile.gui = gui
    return profile


def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
    """Encode :class:`GuiProfileModel` into JSON-serializable dictionary."""
    payload = model.run_config.to_dict()
    gui = model.gui if model.gui is not None else GuiStateModel()
    payload["gui"] = {
        "version": gui.version,
        "switches": {
            "combo_mode": gui.switches.combo_mode,
            "combos_text": gui.switches.combos_text,
            "single_input": gui.switches.single_input,
            "single_output": gui.switches.single_output,
        },
        "processing": {
            "selected_mode": gui.processing.selected_mode,
            "pass_through": {
                "show_magnitude": gui.processing.pass_through.show_magnitude,
                "show_phase": gui.processing.pass_through.show_phase,
                "combo_filter": gui.processing.pass_through.combo_filter,
                "fixed_y_enabled": gui.processing.pass_through.fixed_y_enabled,
                "y_min_db": gui.processing.pass_through.y_min_db,
                "y_max_db": gui.processing.pass_through.y_max_db,
            },
            "bscan": {
                "axis": gui.processing.bscan.axis,
                "cut_m": gui.processing.bscan.cut_m,
                "max_depth_m": gui.processing.bscan.max_depth_m,
                "gain": gui.processing.bscan.gain,
                "start_freq_mhz": gui.processing.bscan.start_freq_mhz,
                "stop_freq_mhz": gui.processing.bscan.stop_freq_mhz,
                "subtract_mean_ascan": gui.processing.bscan.subtract_mean_ascan,
            },
            "gpr": {
                "input_positions": gui.processing.gpr.input_positions,
                "output_positions": gui.processing.gpr.output_positions,
                "min_depth_m": gui.processing.gpr.min_depth_m,
                "max_depth_m": gui.processing.gpr.max_depth_m,
                "range_comp_power": gui.processing.gpr.range_comp_power,
                "angle_comp_power": gui.processing.gpr.angle_comp_power,
                "score_mode": gui.processing.gpr.score_mode,
                "max_detected_objects_to_draw": gui.processing.gpr.max_detected_objects_to_draw,
                "draw_top_m_objects": gui.processing.gpr.draw_top_m_objects,
                "start_freq_mhz": gui.processing.gpr.start_freq_mhz,
                "stop_freq_mhz": gui.processing.gpr.stop_freq_mhz,
                "background_subtract_enabled": gui.processing.gpr.background_subtract_enabled,
                "background_mean_count": gui.processing.gpr.background_mean_count,
                "remove_sidelobe_objects_enabled": gui.processing.gpr.remove_sidelobe_objects_enabled,
                "imaging_plane_y_m": gui.processing.gpr.imaging_plane_y_m,
                "render_mode": gui.processing.gpr.render_mode,
                "min_visible_score": gui.processing.gpr.min_visible_score,
                "visible_x_min_m": gui.processing.gpr.visible_x_min_m,
                "visible_x_max_m": gui.processing.gpr.visible_x_max_m,
                "visible_z_min_m": gui.processing.gpr.visible_z_min_m,
                "visible_z_max_m": gui.processing.gpr.visible_z_max_m,
            },
            "legacy_gpr": {
                "mode": gui.processing.legacy_gpr.mode,
                "input_positions": gui.processing.legacy_gpr.input_positions,
                "output_positions": gui.processing.legacy_gpr.output_positions,
                "min_depth_m": gui.processing.legacy_gpr.min_depth_m,
                "max_depth_m": gui.processing.legacy_gpr.max_depth_m,
                "comp_power": gui.processing.legacy_gpr.comp_power,
                "start_freq_mhz": gui.processing.legacy_gpr.start_freq_mhz,
                "stop_freq_mhz": gui.processing.legacy_gpr.stop_freq_mhz,
                "speed_m_s": gui.processing.legacy_gpr.speed_m_s,
                "ignore_socket_speed_enabled": gui.processing.legacy_gpr.ignore_socket_speed_enabled,
                "look_angle_deg": gui.processing.legacy_gpr.look_angle_deg,
                "snr_thresh": gui.processing.legacy_gpr.snr_thresh,
                "snr_comp_max": gui.processing.legacy_gpr.snr_comp_max,
                "background_subtract_enabled": gui.processing.legacy_gpr.background_subtract_enabled,
                "background_mean_count": gui.processing.legacy_gpr.background_mean_count,
                "render_mode": gui.processing.legacy_gpr.render_mode,
                "min_visible_pair_count": gui.processing.legacy_gpr.min_visible_pair_count,
                "visible_x_min_m": gui.processing.legacy_gpr.visible_x_min_m,
                "visible_x_max_m": gui.processing.legacy_gpr.visible_x_max_m,
                "visible_z_min_m": gui.processing.legacy_gpr.visible_z_min_m,
                "visible_z_max_m": gui.processing.legacy_gpr.visible_z_max_m,
            },
        },
        "data_actions": {
            "save_count": gui.data_actions.save_count,
            "save_path": gui.data_actions.save_path,
            "save_name": gui.data_actions.save_name,
        },
        "preprocess_dialog": {
            "set_name": gui.preprocess_dialog.set_name,
            "radar_config_dir": gui.preprocess_dialog.radar_config_dir,
            "use_all_radar_configs": gui.preprocess_dialog.use_all_radar_configs,
        },
    }
    return payload
