#include "processing_live_config.hpp"

#include <chrono>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>

#include <nlohmann/json.hpp>

namespace radar::processing {
namespace {

using Json = nlohmann::json;

[[nodiscard]] auto parse_history_command(const std::string& value) -> HistoryCommand {
    if (value == "none") {
        return HistoryCommand::None;
    }
    if (value == "remove_last") {
        return HistoryCommand::RemoveLast;
    }
    if (value == "clear_all") {
        return HistoryCommand::ClearAll;
    }
    throw std::runtime_error("processing.history_command must be one of: none, remove_last, clear_all");
}

[[nodiscard]] auto parse_legacy_gpr_mode(const std::string& value, const std::string& field_name) -> LegacyGprMode {
    if (value == "point") {
        return LegacyGprMode::Point;
    }
    if (value == "extended") {
        return LegacyGprMode::Extended;
    }
    throw std::runtime_error(field_name + " must be one of: point, extended");
}

[[nodiscard]] auto parse_gpr_score_mode(const std::string& value, const std::string& field_name) -> std::string {
    if (value == "peak" || value == "combined") {
        return value;
    }
    throw std::runtime_error(field_name + " must be one of: peak, combined");
}

void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::string& value) {
    if (value == "backprojection") {
        return;
    }
    if (value == "legacy_point") {
        config.legacy_gpr_mode = LegacyGprMode::Point;
    } else if (value == "legacy_extended") {
        config.legacy_gpr_mode = LegacyGprMode::Extended;
    } else {
        throw std::runtime_error(
            "processing.gpr_algorithm must be one of: backprojection, legacy_point, legacy_extended"
        );
    }
    if (config.processor_mode.empty() || config.processor_mode == "gpr") {
        config.processor_mode = "legacy_gpr";
    }
}

[[nodiscard]] auto parse_s_parameter_channel(const std::string& value, const std::string& field_name) -> std::string {
    if (value == "s21" || value == "s11") {
        return value;
    }
    throw std::runtime_error(field_name + " must be one of: s21, s11");
}

[[nodiscard]] auto parse_u64_number(const Json& value, const std::string& field_name) -> std::uint64_t {
    if (!value.is_number()) {
        throw std::runtime_error(field_name + " must be number");
    }
    const double numeric = value.get<double>();
    if (numeric < 0.0 || numeric > static_cast<double>(std::numeric_limits<std::uint64_t>::max())) {
        throw std::runtime_error(field_name + " is out of uint64 range");
    }
    const double rounded = std::round(numeric);
    if (std::fabs(numeric - rounded) > 1e-6) {
        throw std::runtime_error(field_name + " must be integer");
    }
    return static_cast<std::uint64_t>(rounded);
}

[[nodiscard]] auto parse_u32_number(const Json& value, const std::string& field_name) -> std::uint32_t {
    const auto parsed = parse_u64_number(value, field_name);
    if (parsed > static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())) {
        throw std::runtime_error(field_name + " is out of uint32 range");
    }
    return static_cast<std::uint32_t>(parsed);
}

[[nodiscard]] auto parse_u32_array(const Json& value, const std::string& field_name) -> std::vector<std::uint32_t> {
    if (!value.is_array()) {
        throw std::runtime_error(field_name + " must be array");
    }

    std::vector<std::uint32_t> result{};
    result.reserve(value.size());
    for (std::size_t index = 0; index < value.size(); ++index) {
        result.push_back(parse_u32_number(value[index], field_name + "[" + std::to_string(index) + "]"));
    }
    return result;
}

[[nodiscard]] auto parse_live_config(const std::string& json_text, const std::string& path) -> ProcessingLiveConfig {
    if (json_text.empty()) {
        throw std::runtime_error("Processing live config is empty: " + path);
    }

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

    if (!root.is_object()) {
        throw std::runtime_error("Processing live config root must be JSON object");
    }

    ProcessingLiveConfig config{};
    if (const auto found = root.find("processor_mode"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.processor_mode must be string");
        }
        config.processor_mode = found->get<std::string>();
    }
    if (const auto found = root.find("pass_through_channel"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.pass_through_channel must be string");
        }
        config.pass_through_channel =
            parse_s_parameter_channel(found->get<std::string>(), "processing.pass_through_channel");
    }
    if (const auto found = root.find("pass_through_fixed_y_enabled"); found != root.end()) {
        if (!found->is_boolean()) {
            throw std::runtime_error("processing.pass_through_fixed_y_enabled must be bool");
        }
        config.pass_through_fixed_y_enabled = found->get<bool>();
    }
    if (const auto found = root.find("pass_through_y_min_db"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.pass_through_y_min_db must be number");
        }
        config.pass_through_y_min_db = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("pass_through_y_max_db"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.pass_through_y_max_db must be number");
        }
        config.pass_through_y_max_db = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("bscan_axis"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.bscan_axis must be string");
        }
        config.bscan_axis = found->get<std::string>();
    }
    if (const auto found = root.find("bscan_channel"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.bscan_channel must be string");
        }
        config.bscan_channel = parse_s_parameter_channel(found->get<std::string>(), "processing.bscan_channel");
    }
    if (const auto found = root.find("bscan_cut_m"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.bscan_cut_m must be number");
        }
        config.bscan_cut_m = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("bscan_max_depth_m"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.bscan_max_depth_m must be number");
        }
        config.bscan_max_depth_m = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("bscan_gain"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.bscan_gain must be number");
        }
        config.bscan_gain = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("bscan_start_freq_mhz"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.bscan_start_freq_mhz must be number");
        }
        config.bscan_start_freq_mhz = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("bscan_stop_freq_mhz"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.bscan_stop_freq_mhz must be number");
        }
        config.bscan_stop_freq_mhz = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("legacy_gpr_mode"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.legacy_gpr_mode must be string");
        }
        config.legacy_gpr_mode =
            parse_legacy_gpr_mode(found->get<std::string>(), "processing.legacy_gpr_mode");
    }
    if (const auto found = root.find("gpr_algorithm"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.gpr_algorithm must be string");
        }
        apply_legacy_gpr_algorithm_alias(config, found->get<std::string>());
    }
    if (const auto found = root.find("gpr_input_positions"); found != root.end()) {
        config.gpr_input_positions = parse_u32_array(*found, "processing.gpr_input_positions");
    }
    if (const auto found = root.find("gpr_output_positions"); found != root.end()) {
        config.gpr_output_positions = parse_u32_array(*found, "processing.gpr_output_positions");
    }
    if (const auto found = root.find("gpr_min_depth_m"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_min_depth_m must be number");
        }
        config.gpr_min_depth_m = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_max_depth_m"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_max_depth_m must be number");
        }
        config.gpr_max_depth_m = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_range_comp_power"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_range_comp_power must be number");
        }
        config.gpr_range_comp_power = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_angle_comp_power"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_angle_comp_power must be number");
        }
        config.gpr_angle_comp_power = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_comp_power"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_comp_power must be number");
        }
        config.gpr_comp_power = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_score_mode"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.gpr_score_mode must be string");
        }
        config.gpr_score_mode = parse_gpr_score_mode(found->get<std::string>(), "processing.gpr_score_mode");
    }
    if (const auto found = root.find("gpr_speed_m_s"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_speed_m_s must be number");
        }
        config.gpr_speed_m_s = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_look_angle_deg"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_look_angle_deg must be number");
        }
        config.gpr_look_angle_deg = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_snr_thresh"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_snr_thresh must be number");
        }
        config.gpr_snr_thresh = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_snr_comp_max"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_snr_comp_max must be number");
        }
        config.gpr_snr_comp_max = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_start_freq_mhz"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_start_freq_mhz must be number");
        }
        config.gpr_start_freq_mhz = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_stop_freq_mhz"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_stop_freq_mhz must be number");
        }
        config.gpr_stop_freq_mhz = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("gpr_background_subtract_enabled"); found != root.end()) {
        if (!found->is_boolean()) {
            throw std::runtime_error("processing.gpr_background_subtract_enabled must be bool");
        }
        config.gpr_background_subtract_enabled = found->get<bool>();
    }
    if (const auto found = root.find("gpr_background_mean_count"); found != root.end()) {
        config.gpr_background_mean_count = parse_u32_number(*found, "processing.gpr_background_mean_count");
    }
    if (const auto found = root.find("gpr_remove_sidelobe_objects_enabled"); found != root.end()) {
        if (!found->is_boolean()) {
            throw std::runtime_error("processing.gpr_remove_sidelobe_objects_enabled must be bool");
        }
        config.gpr_remove_sidelobe_objects_enabled = found->get<bool>();
    }
    if (const auto found = root.find("gpr_imaging_plane_y_m"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gpr_imaging_plane_y_m must be number");
        }
        config.gpr_imaging_plane_y_m = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("reprocess_current_result"); found != root.end()) {
        if (!found->is_boolean()) {
            throw std::runtime_error("processing.reprocess_current_result must be bool");
        }
        config.reprocess_current_result = found->get<bool>();
    }
    if (const auto found = root.find("history_command_seq"); found != root.end()) {
        config.history_command_seq = parse_u64_number(*found, "processing.history_command_seq");
    }
    if (const auto found = root.find("history_command"); found != root.end()) {
        if (!found->is_string()) {
            throw std::runtime_error("processing.history_command must be string");
        }
        config.history_command = parse_history_command(found->get<std::string>());
    }
    return config;
}

}  // namespace

ProcessingLiveConfigLoader::ProcessingLiveConfigLoader(std::string path)
    : path_(std::move(path)),
      next_check_at_(std::chrono::steady_clock::now()) {
    if (path_.empty()) {
        throw std::runtime_error("Processing live config path must not be empty");
    }
}

auto ProcessingLiveConfigLoader::current() const -> ProcessingLiveConfig {
    return current_;
}

auto ProcessingLiveConfigLoader::revision() const -> std::uint64_t {
    return revision_;
}

auto ProcessingLiveConfigLoader::refresh_if_needed() -> ProcessingLiveConfig {
    const auto now = std::chrono::steady_clock::now();
    if (now < next_check_at_) {
        return current_;
    }
    next_check_at_ = now + std::chrono::milliseconds(100);

    std::error_code file_error{};
    if (!std::filesystem::exists(path_, file_error) || file_error) {
        return current_;
    }

    try {
        const auto json_text = read_file_text();
        if (has_last_json_text_ && json_text == last_json_text_) {
            return current_;
        }

        current_ = parse_live_config(json_text, path_);
        last_json_text_ = json_text;
        has_last_json_text_ = true;
        ++revision_;
    } catch (const std::exception& error) {
        std::cerr << "data_processor warning: failed to refresh live processing config: " << error.what() << '\n';
    }

    return current_;
}

auto ProcessingLiveConfigLoader::read_file_text() const -> std::string {
    std::ifstream stream(path_);
    if (!stream.is_open()) {
        throw std::runtime_error("Failed to open live processing config: " + path_);
    }

    std::stringstream buffer{};
    buffer << stream.rdbuf();
    return buffer.str();
}

}  // namespace radar::processing
