#include "fgc/Config.h"
#include "fgc/Paths.h"

#include <cstdlib>
#include <stdexcept>

extern "C" {
#include "ini.h"
}

namespace fgc {

namespace {

std::string get(const std::map<std::string, std::string>& kv, const std::string& key,
                const std::string& fallback = "") {
    auto it = kv.find(key);
    return (it != kv.end() && !it->second.empty()) ? it->second : fallback;
}

int getInt(const std::map<std::string, std::string>& kv, const std::string& key, int fallback) {
    auto it = kv.find(key);
    if (it == kv.end() || it->second.empty()) return fallback;
    try {
        return std::stoi(it->second);
    } catch (const std::exception&) {
        throw std::runtime_error("Config key '" + key + "' must be an integer, got '" +
                                 it->second + "'");
    }
}

double getDouble(const std::map<std::string, std::string>& kv, const std::string& key,
                 double fallback) {
    auto it = kv.find(key);
    if (it == kv.end() || it->second.empty()) return fallback;
    try {
        return std::stod(it->second);
    } catch (const std::exception&) {
        throw std::runtime_error("Config key '" + key + "' must be a number, got '" +
                                 it->second + "'");
    }
}

long getLong(const std::map<std::string, std::string>& kv, const std::string& key, long fallback) {
    auto it = kv.find(key);
    if (it == kv.end() || it->second.empty()) return fallback;
    try {
        return std::stol(it->second);
    } catch (const std::exception&) {
        throw std::runtime_error("Config key '" + key + "' must be an integer, got '" +
                                 it->second + "'");
    }
}

bool getBool(const std::map<std::string, std::string>& kv, const std::string& key, bool fallback) {
    auto it = kv.find(key);
    if (it == kv.end() || it->second.empty()) return fallback;
    const std::string& v = it->second;
    if (v == "1" || v == "true" || v == "yes" || v == "on") return true;
    if (v == "0" || v == "false" || v == "no" || v == "off") return false;
    throw std::runtime_error("Config key '" + key + "' must be a boolean, got '" + v + "'");
}

std::string envOr(const char* name, const std::string& fallback) {
    const char* v = std::getenv(name);
    return (v && *v) ? std::string(v) : fallback;
}

// inih callback: flatten "Section.name" => value into the map.
int iniHandler(void* user, const char* section, const char* name, const char* value) {
    auto* kv = static_cast<std::map<std::string, std::string>*>(user);
    (*kv)[std::string(section) + "." + std::string(name)] = value ? value : "";
    return 1;
}

}  // namespace

double AppConfig::image_rate() const {
    return general.image_interval > 0 ? 1.0 / general.image_interval : 0.0;
}

AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
    AppConfig cfg;

    cfg.general.tower_name     = get(kv, "General.tower_name", cfg.general.tower_name);
    cfg.general.image_interval = getInt(kv, "General.image_interval", cfg.general.image_interval);
    cfg.general.debug          = getBool(kv, "General.debug", cfg.general.debug);

    cfg.network.broker_ip = get(kv, "Network.zkms_server_ip", cfg.network.broker_ip);
    // Secrets: environment variables win over the file.
    cfg.network.mqtt_user = envOr("FGC_MQTT_USER", get(kv, "Network.mqtt_user"));
    cfg.network.mqtt_pw   = envOr("FGC_MQTT_PW", get(kv, "Network.mqtt_pw"));

    cfg.serial.device = get(kv, "Serial.device", cfg.serial.device);
    cfg.serial.baud   = static_cast<unsigned>(getInt(kv, "Serial.baud", cfg.serial.baud));

    cfg.camera.ids.clear();
    for (const char* key : {"Camera.id_Cam1", "Camera.id_Cam2", "Camera.id_Cam3", "Camera.id_Cam4"}) {
        std::string id = get(kv, key);
        if (!id.empty()) cfg.camera.ids.push_back(id);
    }

    std::string out = get(kv, "Paths.output_dir");
    cfg.paths.output_dir = out.empty() ? paths::defaultOutputDir() : paths::expandUser(out);

    cfg.features.enable_mqtt   = getBool(kv, "Features.enable_mqtt", cfg.features.enable_mqtt);
    cfg.features.enable_camera = getBool(kv, "Features.enable_camera", cfg.features.enable_camera);
    cfg.features.enable_serial = getBool(kv, "Features.enable_serial", cfg.features.enable_serial);
    cfg.features.mock_camera   = getBool(kv, "Features.mock_camera", cfg.features.mock_camera);
    cfg.features.mock_serial   = getBool(kv, "Features.mock_serial", cfg.features.mock_serial);

    cfg.logging.level = get(kv, "Logging.level", cfg.logging.level);
    cfg.logging.trace = get(kv, "Logging.trace", cfg.logging.trace);

    cfg.ui.enable_tui = getBool(kv, "UI.enable_tui", cfg.ui.enable_tui);

    // [Motor]: operator-calibrated degrees<->counts maps (see Geometry).
    cfg.geometry.yaw.counts_per_deg   = getDouble(kv, "Motor.yaw_counts_per_deg", cfg.geometry.yaw.counts_per_deg);
    cfg.geometry.yaw.zero_count       = getLong(kv, "Motor.yaw_zero_count", cfg.geometry.yaw.zero_count);
    cfg.geometry.yaw.min_deg          = getDouble(kv, "Motor.yaw_min_deg", cfg.geometry.yaw.min_deg);
    cfg.geometry.yaw.max_deg          = getDouble(kv, "Motor.yaw_max_deg", cfg.geometry.yaw.max_deg);
    cfg.geometry.pitch.counts_per_deg = getDouble(kv, "Motor.pitch_counts_per_deg", cfg.geometry.pitch.counts_per_deg);
    cfg.geometry.pitch.zero_count     = getLong(kv, "Motor.pitch_zero_count", cfg.geometry.pitch.zero_count);
    cfg.geometry.pitch.min_deg        = getDouble(kv, "Motor.pitch_min_deg", cfg.geometry.pitch.min_deg);
    cfg.geometry.pitch.max_deg        = getDouble(kv, "Motor.pitch_max_deg", cfg.geometry.pitch.max_deg);

    // [Scan]: scan-grid source (explicit CSV file, else generated).
    cfg.scan.grid_file     = get(kv, "Scan.grid_file", cfg.scan.grid_file);
    cfg.scan.yaw_intervals = getInt(kv, "Scan.yaw_intervals", cfg.scan.yaw_intervals);
    cfg.scan.yaw_min_deg   = getDouble(kv, "Scan.yaw_min_deg", cfg.scan.yaw_min_deg);
    cfg.scan.yaw_max_deg   = getDouble(kv, "Scan.yaw_max_deg", cfg.scan.yaw_max_deg);
    cfg.scan.pitch_levels  = get(kv, "Scan.pitch_levels", cfg.scan.pitch_levels);

    // ---- validation ----
    if (cfg.general.image_interval <= 0)
        throw std::runtime_error("General.image_interval must be > 0 (got " +
                                 std::to_string(cfg.general.image_interval) + ")");
    if (cfg.serial.baud == 0)
        throw std::runtime_error("Serial.baud must be > 0");

    return cfg;
}

AppConfig ConfigLoader::loadFromFile(const std::string& path) {
    std::map<std::string, std::string> kv;
    int rc = ini_parse(path.c_str(), iniHandler, &kv);
    if (rc < 0) throw std::runtime_error("Cannot read config file: " + path);
    if (rc > 0)
        throw std::runtime_error("Parse error in config file " + path + " at line " +
                                 std::to_string(rc));
    return fromMap(kv);
}

}  // namespace fgc
