#include "processing_live_config.hpp"

#include <chrono>
#include <cmath>
#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_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("gain_db"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.gain_db must be number");
        }
        config.gain_db = static_cast<float>(found->get<double>());
    }
    if (const auto found = root.find("phase_deg"); found != root.end()) {
        if (!found->is_number()) {
            throw std::runtime_error("processing.phase_deg must be number");
        }
        config.phase_deg = static_cast<float>(found->get<double>());
    }
    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_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("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_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_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("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 time_error{};
    const auto file_time = std::filesystem::last_write_time(path_, time_error);
    if (time_error) {
        return current_;
    }

    if (has_last_write_time_ && file_time == last_write_time_) {
        return current_;
    }

    try {
        current_ = read_from_file();
        last_write_time_ = file_time;
        has_last_write_time_ = 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_from_file() const -> ProcessingLiveConfig {
    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 parse_live_config(buffer.str(), path_);
}

}  // namespace radar::processing
