#include "run_config.hpp"

#include <cmath>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <utility>

#include <nlohmann/json.hpp>

namespace radar::config {
namespace {

using Json = nlohmann::json;

[[nodiscard]] auto required_field(const Json& object, const std::string& key) -> const Json& {
    const auto found = object.find(key);
    if (found == object.end()) {
        throw std::runtime_error("Missing required config field: " + key);
    }
    return *found;
}

[[nodiscard]] auto optional_field(const Json& object, const std::string& key) -> const Json* {
    const auto found = object.find(key);
    if (found == object.end()) {
        return nullptr;
    }
    return &(*found);
}

[[nodiscard]] auto as_object(const Json& value, const std::string& context) -> const Json* {
    if (!value.is_object()) {
        throw std::runtime_error("Expected JSON object at " + context);
    }
    return &value;
}

[[nodiscard]] auto as_array(const Json& value, const std::string& context) -> const Json* {
    if (!value.is_array()) {
        throw std::runtime_error("Expected JSON array at " + context);
    }
    return &value;
}

[[nodiscard]] auto as_string(const Json& value, const std::string& context) -> const std::string& {
    if (!value.is_string()) {
        throw std::runtime_error("Expected JSON string at " + context);
    }
    return value.get_ref<const std::string&>();
}

[[nodiscard]] auto as_bool(const Json& value, const std::string& context) -> bool {
    if (!value.is_boolean()) {
        throw std::runtime_error("Expected JSON bool at " + context);
    }
    return value.get<bool>();
}

[[nodiscard]] auto as_number(const Json& value, const std::string& context) -> double {
    if (!value.is_number()) {
        throw std::runtime_error("Expected JSON number at " + context);
    }
    return value.get<double>();
}

[[nodiscard]] auto number_to_u32(double value, const std::string& context) -> std::uint32_t {
    if (value < 0.0 || value > static_cast<double>(std::numeric_limits<std::uint32_t>::max())) {
        throw std::runtime_error("Value out of uint32 range at " + context);
    }

    const auto rounded = std::round(value);
    if (std::fabs(value - rounded) > 0.000001) {
        throw std::runtime_error("Expected integer value at " + context);
    }
    return static_cast<std::uint32_t>(rounded);
}

[[nodiscard]] auto number_to_i32(double value, const std::string& context) -> std::int32_t {
    const auto rounded = std::round(value);
    if (std::fabs(value - rounded) > 0.000001) {
        throw std::runtime_error("Expected integer value at " + context);
    }

    if (rounded < static_cast<double>(std::numeric_limits<std::int32_t>::min()) ||
        rounded > static_cast<double>(std::numeric_limits<std::int32_t>::max())) {
        throw std::runtime_error("Value out of int32 range at " + context);
    }
    return static_cast<std::int32_t>(rounded);
}

[[nodiscard]] auto optional_string(
    const Json& object,
    const std::string& key,
    const std::string& fallback
) -> std::string {
    if (const auto* value = optional_field(object, key); value != nullptr) {
        return as_string(*value, key);
    }
    return fallback;
}

[[nodiscard]] auto optional_bool(const Json& object, const std::string& key, bool fallback) -> bool {
    if (const auto* value = optional_field(object, key); value != nullptr) {
        return as_bool(*value, key);
    }
    return fallback;
}

[[nodiscard]] auto optional_u32(
    const Json& object,
    const std::string& key,
    std::uint32_t fallback
) -> std::uint32_t {
    if (const auto* value = optional_field(object, key); value != nullptr) {
        return number_to_u32(as_number(*value, key), key);
    }
    return fallback;
}

[[nodiscard]] auto optional_i32(
    const Json& object,
    const std::string& key,
    std::int32_t fallback
) -> std::int32_t {
    if (const auto* value = optional_field(object, key); value != nullptr) {
        return number_to_i32(as_number(*value, key), key);
    }
    return fallback;
}

[[nodiscard]] auto optional_f32(const Json& object, const std::string& key, float fallback) -> float {
    if (const auto* value = optional_field(object, key); value != nullptr) {
        return static_cast<float>(as_number(*value, key));
    }
    return fallback;
}

[[nodiscard]] auto parse_preprocess_asset(
    const Json& object,
    const std::string& key,
    const std::string& context
) -> PreprocessAssetConfig {
    const auto asset_context = context + "." + key;
    const auto* asset_obj = as_object(required_field(object, key), asset_context);

    PreprocessAssetConfig asset{};
    asset.set_name = optional_string(*asset_obj, "set_name", "");
    asset.bundle_path = optional_string(*asset_obj, "bundle_path", "");
    return asset;
}

[[nodiscard]] auto parse_preprocess_notch(
    const Json& object,
    const std::string& context
) -> PreprocessNotchConfig {
    PreprocessNotchConfig notch{};
    notch.enabled = optional_bool(object, "enabled", false);
    notch.taper_width_hz = optional_f32(object, "taper_width_hz", 40'000'000.0F);
    notch.taper_type = optional_string(object, "taper_type", "cosine");

    if (const auto* bands_value = optional_field(object, "bands_hz"); bands_value != nullptr) {
        const auto* bands_array = as_array(*bands_value, context + ".bands_hz");
        notch.bands_hz.reserve(bands_array->size());
        for (const auto& band_value : *bands_array) {
            const auto* band_array = as_array(band_value, context + ".bands_hz[]");
            if (band_array->size() != 2U) {
                throw std::runtime_error(context + ".bands_hz[] must contain exactly two numbers");
            }

            PreprocessNotchBandConfig band{};
            band.low_hz = static_cast<float>(as_number((*band_array)[0], context + ".bands_hz[][0]"));
            band.high_hz = static_cast<float>(as_number((*band_array)[1], context + ".bands_hz[][1]"));
            notch.bands_hz.push_back(band);
        }
    }

    if (notch.taper_width_hz < 0.0F) {
        throw std::runtime_error(context + ".taper_width_hz must be >= 0");
    }
    if (notch.taper_type != "cosine" && notch.taper_type != "hard") {
        throw std::runtime_error(context + ".taper_type must be 'cosine' or 'hard'");
    }
    for (const auto& band : notch.bands_hz) {
        if (band.high_hz < band.low_hz) {
            throw std::runtime_error(context + ".bands_hz[] high_hz must be >= low_hz");
        }
    }

    return notch;
}

[[nodiscard]] auto parse_driver_mode(const std::string& value) -> DriverMode {
    if (value == "mock") {
        return DriverMode::Mock;
    }
    if (value == "native") {
        return DriverMode::Native;
    }
    throw std::runtime_error("Unsupported driver_mode value: " + value);
}

[[nodiscard]] auto parse_switch_driver_kind(const std::string& value) -> SwitchDriverKind {
    if (value == "h7992") {
        return SwitchDriverKind::H7992;
    }
    if (value == "hmc349a") {
        return SwitchDriverKind::HMC349A;
    }
    throw std::runtime_error("Unsupported switch driver value: " + value);
}

void validate_radar_sweep(const RadarSweepSettings& sweep) {
    if (sweep.points == 0U) {
        throw std::runtime_error("radar.sweep.points must be > 0");
    }
    if (sweep.stop_hz < sweep.start_hz) {
        throw std::runtime_error("radar.sweep.stop_hz must be >= start_hz");
    }
}

void validate_switch_config(const SwitchConfig& config) {
    if (config.radar_port != 1U && config.radar_port != 2U) {
        throw std::runtime_error("Switch radar_port must be 1 or 2 for " + config.name);
    }

    if (config.positions == 0U) {
        throw std::runtime_error("Switch positions must be > 0 for " + config.name);
    }
    if (config.default_position >= config.positions) {
        throw std::runtime_error("Switch default_position is out of range for " + config.name);
    }

    if (config.driver_mode != DriverMode::Native) {
        return;
    }

    if (config.gpio_chip.empty()) {
        throw std::runtime_error("gpio_chip is required for native switch mode: " + config.name);
    }

    if (config.driver_kind == SwitchDriverKind::H7992) {
        if (config.positions > 4U) {
            throw std::runtime_error("Native H7992 switch driver supports positions <= 4 for " + config.name);
        }
        if (config.pin_a < 0 || config.pin_b < 0 || config.pin_a == config.pin_b) {
            throw std::runtime_error("pin_a/pin_b are invalid for native H7992 mode: " + config.name);
        }
        return;
    }

    if (config.positions > 2U) {
        throw std::runtime_error("Native HMC349A switch driver supports positions <= 2 for " + config.name);
    }
    if (config.pin_a < 0) {
        throw std::runtime_error("pin_a (control) is required for native HMC349A mode: " + config.name);
    }
}

void validate_combo_key(const ipc::ComboKey& key, const RunConfig& config) {
    if (key.input_pos >= config.input_switch.positions) {
        throw std::runtime_error("run.combos input index out of range");
    }
    if (key.output_pos >= config.output_switch.positions) {
        throw std::runtime_error("run.combos output index out of range");
    }
}

void validate_gpr_config(const GprConfig& config, const RunConfig& run_config) {
    if (!(config.relative_permittivity > 0.0F)) {
        throw std::runtime_error("gpr.relative_permittivity must be > 0");
    }

    std::unordered_set<std::uint32_t> seen_tx_positions{};
    for (const auto& entry : config.tx_geometry) {
        if (entry.output_pos >= run_config.output_switch.positions) {
            throw std::runtime_error("gpr.tx_geometry output_pos is out of range");
        }
        if (!seen_tx_positions.insert(entry.output_pos).second) {
            throw std::runtime_error("gpr.tx_geometry contains duplicate output_pos");
        }
    }

    std::unordered_set<std::uint32_t> seen_rx_positions{};
    for (const auto& entry : config.rx_geometry) {
        if (entry.input_pos >= run_config.input_switch.positions) {
            throw std::runtime_error("gpr.rx_geometry input_pos is out of range");
        }
        if (!seen_rx_positions.insert(entry.input_pos).second) {
            throw std::runtime_error("gpr.rx_geometry contains duplicate input_pos");
        }
    }
}

[[nodiscard]] auto parse_gpr_config(const Json& object, const RunConfig& run_config) -> GprConfig {
    const auto* gpr_obj = as_object(object, "gpr");
    GprConfig config{};
    config.relative_permittivity = optional_f32(*gpr_obj, "relative_permittivity", 1.0F);

    if (const auto* tx_value = optional_field(*gpr_obj, "tx_geometry"); tx_value != nullptr) {
        const auto* tx_array = as_array(*tx_value, "gpr.tx_geometry");
        config.tx_geometry.reserve(tx_array->size());
        for (const auto& entry_value : *tx_array) {
            const auto* entry_obj = as_object(entry_value, "gpr.tx_geometry[]");
            GprTxGeometry entry{};
            entry.output_pos = optional_u32(*entry_obj, "output_pos", 0U);
            entry.x_m = optional_f32(*entry_obj, "x_m", 0.0F);
            entry.y_m = optional_f32(*entry_obj, "y_m", 0.0F);
            entry.z_m = optional_f32(*entry_obj, "z_m", 0.0F);
            config.tx_geometry.push_back(std::move(entry));
        }
    }

    if (const auto* rx_value = optional_field(*gpr_obj, "rx_geometry"); rx_value != nullptr) {
        const auto* rx_array = as_array(*rx_value, "gpr.rx_geometry");
        config.rx_geometry.reserve(rx_array->size());
        for (const auto& entry_value : *rx_array) {
            const auto* entry_obj = as_object(entry_value, "gpr.rx_geometry[]");
            GprRxGeometry entry{};
            entry.input_pos = optional_u32(*entry_obj, "input_pos", 0U);
            entry.x_m = optional_f32(*entry_obj, "x_m", 0.0F);
            entry.y_m = optional_f32(*entry_obj, "y_m", 0.0F);
            entry.z_m = optional_f32(*entry_obj, "z_m", 0.0F);
            config.rx_geometry.push_back(std::move(entry));
        }
    }

    validate_gpr_config(config, run_config);
    return config;
}

[[nodiscard]] auto parse_switch_config(
    const Json& object,
    const std::string& default_name,
    std::uint32_t default_radar_port
) -> SwitchConfig {
    const auto* switch_obj = as_object(object, default_name);

    SwitchConfig config{};
    config.name = optional_string(*switch_obj, "name", default_name);
    config.driver_mode = parse_driver_mode(optional_string(*switch_obj, "driver_mode", "mock"));
    config.driver_kind = parse_switch_driver_kind(optional_string(*switch_obj, "driver", "h7992"));
    config.radar_port = optional_u32(*switch_obj, "radar_port", default_radar_port);
    config.positions = optional_u32(*switch_obj, "positions", 4);
    config.default_position = optional_u32(*switch_obj, "default_position", 0);
    config.gpio_chip = optional_string(*switch_obj, "gpio_chip", "/dev/gpiochip0");
    config.pin_a = optional_i32(*switch_obj, "pin_a", 17);
    config.pin_b = optional_i32(*switch_obj, "pin_b", 27);
    config.invert_logic = optional_bool(*switch_obj, "invert_logic", false);

    validate_switch_config(config);

    return config;
}

[[nodiscard]] auto parse_ring_endpoint(
    const Json& object,
    const std::string& key,
    const RingEndpointConfig& fallback
) -> RingEndpointConfig {
    RingEndpointConfig endpoint = fallback;
    const auto* value = optional_field(object, key);
    if (value == nullptr) {
        return endpoint;
    }

    const auto* endpoint_obj = as_object(*value, key);
    endpoint.name = optional_string(*endpoint_obj, "name", endpoint.name);
    endpoint.capacity = optional_u32(*endpoint_obj, "capacity", endpoint.capacity);
    endpoint.slot_size_bytes = optional_u32(*endpoint_obj, "slot_size_bytes", endpoint.slot_size_bytes);

    if (endpoint.capacity == 0U) {
        throw std::runtime_error("Ring capacity must be > 0 for " + key);
    }
    if (endpoint.slot_size_bytes == 0U) {
        throw std::runtime_error("Ring slot_size_bytes must be > 0 for " + key);
    }
    if (endpoint.name.empty() || endpoint.name.front() != '/') {
        throw std::runtime_error("Ring name must start with '/' for " + key);
    }

    return endpoint;
}

}  // namespace

auto load_run_config(const std::string& path) -> RunConfig {
    std::ifstream stream(path);
    if (!stream.is_open()) {
        throw std::runtime_error("Failed to open config file: " + path);
    }

    const std::string json_text((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
    if (json_text.empty()) {
        throw std::runtime_error("Config file is empty: " + path);
    }

    Json root{};
    try {
        root = Json::parse(json_text);
    } catch (const Json::parse_error& error) {
        throw std::runtime_error("Failed to parse config file: " + path + ": " + std::string(error.what()));
    }

    const auto* root_obj = as_object(root, "root");
    RunConfig config{};

    {
        const auto* radar_obj = as_object(required_field(*root_obj, "radar"), "radar");
        config.radar.model = optional_string(*radar_obj, "model", "librevna");
        config.radar.serial = optional_string(*radar_obj, "serial", "");
        config.radar.remote_host = optional_string(*radar_obj, "remote_host", "127.0.0.1");
        config.radar.remote_port = optional_u32(*radar_obj, "remote_port", 50209);
        if (config.radar.remote_host.empty()) {
            throw std::runtime_error("radar.remote_host must not be empty");
        }
        if (config.radar.remote_port == 0U || config.radar.remote_port > 65535U) {
            throw std::runtime_error("radar.remote_port must be in 1..65535");
        }
        config.radar.driver_mode = parse_driver_mode(optional_string(*radar_obj, "driver_mode", "mock"));
        config.radar.mock_signal_hz = optional_f32(*radar_obj, "mock_signal_hz", 5'000'000.0F);

        const auto* sweep_obj = as_object(required_field(*radar_obj, "sweep"), "radar.sweep");
        config.radar.sweep.start_hz =
            static_cast<float>(as_number(required_field(*sweep_obj, "start_hz"), "radar.sweep.start_hz"));
        config.radar.sweep.stop_hz =
            static_cast<float>(as_number(required_field(*sweep_obj, "stop_hz"), "radar.sweep.stop_hz"));
        if (const auto* points_value = optional_field(*sweep_obj, "points"); points_value != nullptr) {
            config.radar.sweep.points =
                number_to_u32(as_number(*points_value, "radar.sweep.points"), "radar.sweep.points");
        } else if (config.radar.model != "kamil_adc") {
            throw std::runtime_error("Missing required config field: points");
        }
        config.radar.sweep.if_bandwidth_hz = static_cast<float>(
            as_number(required_field(*sweep_obj, "if_bandwidth_hz"), "radar.sweep.if_bandwidth_hz")
        );
        config.radar.sweep.power_dbm = optional_f32(*sweep_obj, "stimulus_power_dbm", -10.0F);
        validate_radar_sweep(config.radar.sweep);
    }

    {
        const auto* switches_obj = as_object(required_field(*root_obj, "switches"), "switches");

        if (const auto* port1_value = optional_field(*switches_obj, "port1"); port1_value != nullptr) {
            const auto* port1_obj = as_object(*port1_value, "switches.port1");
            const auto* port2_obj = as_object(required_field(*switches_obj, "port2"), "switches.port2");
            config.output_switch = parse_switch_config(*port1_obj, "port1", 1);
            config.input_switch = parse_switch_config(*port2_obj, "port2", 2);
        } else {
            config.input_switch = parse_switch_config(
                *as_object(required_field(*switches_obj, "input"), "switches.input"),
                "input",
                2
            );
            config.output_switch = parse_switch_config(
                *as_object(required_field(*switches_obj, "output"), "switches.output"),
                "output",
                1
            );
        }

        if (config.input_switch.radar_port == config.output_switch.radar_port) {
            throw std::runtime_error("Switch radar_port mapping must be unique for two switches");
        }
    }

    {
        const auto* run_obj = as_object(required_field(*root_obj, "run"), "run");
        config.runtime.settling_ms = optional_u32(*run_obj, "settling_ms", 0);
        config.runtime.idle_sleep_ms = optional_u32(*run_obj, "idle_sleep_ms", 2);
        config.runtime.continuous = optional_bool(*run_obj, "continuous", true);
        config.runtime.processing_live_config_path = optional_string(
            *run_obj,
            "processing_live_config_path",
            "python_app/runtime/processing_live.json"
        );

        const auto* combos = as_array(required_field(*run_obj, "combos"), "run.combos");
        if (combos->empty()) {
            throw std::runtime_error("run.combos must not be empty");
        }

        config.run_combos.reserve(combos->size());
        for (const auto& combo_value : *combos) {
            const auto* combo_obj = as_object(combo_value, "run.combos[]");

            ipc::ComboKey key{};
            key.input_pos = number_to_u32(
                as_number(required_field(*combo_obj, "input"), "run.combos.input"),
                "run.combos.input"
            );
            key.output_pos = number_to_u32(
                as_number(required_field(*combo_obj, "output"), "run.combos.output"),
                "run.combos.output"
            );
            validate_combo_key(key, config);
            config.run_combos.push_back(key);
        }
    }

    {
        const auto* preprocess_obj = as_object(required_field(*root_obj, "preprocess"), "preprocess");
        const auto* s21_obj = as_object(required_field(*preprocess_obj, "s21"), "preprocess.s21");
        config.preprocess.s21.calibration = parse_preprocess_asset(*s21_obj, "calibration", "preprocess.s21");
        config.preprocess.s21.reference = parse_preprocess_asset(*s21_obj, "reference", "preprocess.s21");

        const auto* s11_obj = as_object(required_field(*preprocess_obj, "s11"), "preprocess.s11");
        const auto* s11_calibration_obj =
            as_object(required_field(*s11_obj, "calibration"), "preprocess.s11.calibration");
        config.preprocess.s11.calibration.open =
            parse_preprocess_asset(*s11_calibration_obj, "open", "preprocess.s11.calibration");
        config.preprocess.s11.calibration.short_standard =
            parse_preprocess_asset(*s11_calibration_obj, "short", "preprocess.s11.calibration");
        config.preprocess.s11.calibration.load =
            parse_preprocess_asset(*s11_calibration_obj, "load", "preprocess.s11.calibration");
        config.preprocess.s11.reference = parse_preprocess_asset(*s11_obj, "reference", "preprocess.s11");

        if (const auto* notch_value = optional_field(*preprocess_obj, "notch"); notch_value != nullptr) {
            config.preprocess.notch = parse_preprocess_notch(*as_object(*notch_value, "preprocess.notch"), "preprocess.notch");
        }
    }

    if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) {
        config.gpr = parse_gpr_config(*gpr_value, config);
    } else {
        validate_gpr_config(config.gpr, config);
    }

    {
        const auto* rings_obj = as_object(required_field(*root_obj, "rings"), "rings");
        config.rings.raw = parse_ring_endpoint(*rings_obj, "raw", config.rings.raw);
        config.rings.raw_tap = parse_ring_endpoint(*rings_obj, "raw_tap", config.rings.raw_tap);
        config.rings.preprocessed = parse_ring_endpoint(*rings_obj, "preprocessed", config.rings.preprocessed);
        config.rings.preprocessed_tap = parse_ring_endpoint(
            *rings_obj,
            "preprocessed_tap",
            config.rings.preprocessed_tap
        );
        config.rings.results = parse_ring_endpoint(*rings_obj, "results", config.rings.results);
    }

    return config;
}

}  // namespace radar::config
