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

#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#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.enable_imu    = getBool(kv, "Features.enable_imu", cfg.features.enable_imu);
    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.features.mock_imu      = getBool(kv, "Features.mock_imu", cfg.features.mock_imu);

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

    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 {
std::string trimmed(const std::string& s) {
    const char* ws = " \t\r\n";
    size_t a = s.find_first_not_of(ws);
    if (a == std::string::npos) return "";
    size_t b = s.find_last_not_of(ws);
    return s.substr(a, b - a + 1);
}
}  // namespace

std::string updateIniSectionKeys(const std::string& contents, const std::string& section,
                                 const std::vector<std::pair<std::string, std::string>>& kv) {
    std::vector<bool> written(kv.size(), false);
    std::istringstream in(contents);
    std::ostringstream out;
    std::string line, cur;
    while (std::getline(in, line)) {
        const std::string t = trimmed(line);
        if (t.size() >= 2 && t.front() == '[' && t.back() == ']') {
            cur = trimmed(t.substr(1, t.size() - 2));
        } else if (cur == section && !t.empty() && t[0] != ';' && t[0] != '#') {
            const size_t eq = line.find('=');
            if (eq != std::string::npos) {
                const std::string key = trimmed(line.substr(0, eq));
                for (size_t i = 0; i < kv.size(); ++i) {
                    if (!written[i] && key == kv[i].first) {
                        line = kv[i].first + " = " + kv[i].second;
                        written[i] = true;
                        break;
                    }
                }
            }
        }
        out << line << "\n";
    }
    // Any keys not present in the section get appended in a fresh [section] block
    // (inih merges duplicate sections on read).
    bool missing = false;
    for (bool w : written) missing = missing || !w;
    if (missing) {
        out << "\n[" << section << "]\n";
        for (size_t i = 0; i < kv.size(); ++i)
            if (!written[i]) out << kv[i].first << " = " << kv[i].second << "\n";
    }
    return out.str();
}

bool saveMotorCalibration(const std::string& path, const Geometry& geo) {
    if (path.empty()) return false;
    std::ifstream f(path);
    if (!f) return false;
    std::stringstream buf;
    buf << f.rdbuf();
    f.close();

    auto num = [](double v) {
        char b[32];
        std::snprintf(b, sizeof(b), "%.10g", v);
        return std::string(b);
    };
    const std::vector<std::pair<std::string, std::string>> kv = {
        {"yaw_counts_per_deg", num(geo.yaw.counts_per_deg)},
        {"yaw_zero_count", std::to_string(geo.yaw.zero_count)},
        {"pitch_counts_per_deg", num(geo.pitch.counts_per_deg)},
        {"pitch_zero_count", std::to_string(geo.pitch.zero_count)},
    };
    const std::string updated = updateIniSectionKeys(buf.str(), "Motor", kv);

    std::ofstream out(path, std::ios::trunc);
    if (!out) return false;
    out << updated;
    return static_cast<bool>(out);
}

}  // namespace fgc
