#pragma once

#include "fgc/Geometry.h"

#include <map>
#include <string>
#include <utility>
#include <vector>

namespace fgc {

// Typed application configuration. Replaces the ad-hoc std::map<string,string>
// lookups that were scattered through main.cpp.

struct GeneralConfig {
    std::string tower_name = "Unnamed";
    int         image_interval = 5;   // seconds between captures
    bool        debug = false;        // print motor telemetry each loop tick
};

struct NetworkConfig {
    std::string broker_ip = "127.0.0.1";  // MQTT broker / ZKMS server
    std::string mqtt_user;                 // see secrets note below
    std::string mqtt_pw;
};

struct SerialConfig {
    std::string  device = "/dev/ttyACM0";
    unsigned int baud   = 115200;
};

struct CameraConfig {
    std::vector<std::string> ids;                          // GigE IP or USB DEV_ id, in order
    std::vector<std::string> labels = {"RGB", "ACR", "NIR"};  // index -> output subfolder

    // On-camera imaging (Alvium), applied in-session by VimbaCameraSource at start().
    // Defaults keep frames small enough to transfer reliably on the LattePanda USB3 host
    // (large ~60 MB frames stall; see docs/known-issues.md "Camera acquisition").
    int         binning         = 2;       // 1 = full res; 2 = 2x2 (~5 MP, ~15 MB) default
    int         offset_x        = 0;       // ROI origin; 0/0 + 0 size => sensor maximum (full frame)
    int         offset_y        = 0;
    int         width           = 0;       // ROI width  in pixels; 0 => sensor maximum
    int         height          = 0;       // ROI height in pixels; 0 => sensor maximum
    std::string pixel_format    = "RGB8";  // on-camera de-Bayer + white balance -> colour out
    int         throughput_mbytes = 250;   // DeviceLinkThroughputLimit (MByte/s); do NOT max it
    double      stream_fps      = 1.0;     // paced acquisition rate; keeps auto converged. Keep low:
                                           // ~2 fps of 15 MB frames stalls this USB3 host; 1 fps sustains
    bool        exposure_auto   = true;    // ExposureAuto = Continuous (adapt to changing light)
    double      exposure_max_us = 0.0;     // ExposureAutoMax cap (us); 0 => leave camera default
    bool        gain_auto       = true;    // GainAuto = Continuous
    double      gain_max_db     = 0.0;     // GainAutoMax cap (dB); 0 => leave camera default
    bool        white_balance_auto = true; // BalanceWhiteAuto = Continuous

    // JPEG XL encoding (consumed by ImagePipeline). Near-lossless default.
    double      jxl_distance    = 0.8;     // 0 = lossless; higher = lossier
    int         jxl_effort      = 4;       // libjxl effort 1..9
};

struct PathsConfig {
    // Where captured .jxl images are written. Supports leading ~ and $ENV
    // expansion. Empty => resolved to a sensible default at load time.
    std::string output_dir;
};

struct FeaturesConfig {
    bool enable_mqtt   = true;
    bool enable_camera = true;
    bool enable_serial = true;
    bool enable_imu    = false;  // Xsens MTi orientation/IMU (off by default)
    bool mock_camera   = false;  // use a simulated camera instead of Vimba X
    bool mock_serial   = false;  // use a simulated motor controller
    bool mock_imu      = false;  // use a simulated IMU instead of the MTi
};

// [IMU]: Xsens MTi connected over the LattePanda's RS-232 UART (a hardware
// /dev/ttyS* node, stable across reboots — not a USB device).
struct ImuConfig {
    std::string  device = "";       // e.g. /dev/ttyS4; empty => required when enabled
    unsigned int baud   = 115200;
};

struct LoggingConfig {
    std::string level;  // trace|debug|info|warn|error|off; empty => default (CLI overrides)
    std::string trace;  // verbatim wire-trace categories: serial,mqtt,camera,control,all,none
};

struct UiConfig {
    bool enable_tui = false;  // false => headless line console (default); CLI --tui/--no-tui override
};

// [Scan]: source of the capture scan grid (the (yaw,pitch) waypoints the
// auto-sweep steps through). If grid_file is set, the CSV is loaded verbatim;
// otherwise a grid is generated from the parameters below (see ScanGrid).
struct ScanConfig {
    std::string grid_file;                  // path to an editable yaw_deg,pitch_deg CSV; empty => generate
    int         yaw_intervals = 56;         // generated: yaw positions across [yaw_min_deg, yaw_max_deg]
    double      yaw_min_deg   = -90.0;
    double      yaw_max_deg   = 90.0;
    std::string pitch_levels  = "0";        // generated: comma list of pitch elevations (deg)
};

// [Test] / [TestProfile.<name>]: the hardware self-test command. A profile is a
// named bundle of repetition counts, sampling windows and pass thresholds; the
// modules read the values they care about by key (open map so new tests can add
// knobs without touching the loader).
struct TestProfile {
    std::string                        name;
    std::map<std::string, double>      params;  // numeric knobs: reps, *_window_ms, *_max_*
    std::map<std::string, std::string> text;    // raw values incl. comma lists (e.g. step sizes)

    double                   get(const std::string& key, double fallback) const;
    int                      geti(const std::string& key, int fallback) const;
    bool                     getBool(const std::string& key, bool fallback) const;
    std::string              gets(const std::string& key, const std::string& fallback = "") const;
    std::vector<long>        getLongList(const std::string& key) const;  // parse "200,500,1000"
};

struct TestConfig {
    std::string                        default_profile = "standard";
    std::string                        baseline_file;   // empty => <logdir>/test_baseline.txt
    std::map<std::string, TestProfile> profiles;

    const TestProfile* find(const std::string& name) const;  // nullptr if absent
};

// Which homed endstop becomes yaw 0 deg, with degrees rising toward the other
// limit (which then lands near +360, a bit less for the soft-limit/mechanical
// gap). Off = use the configured/calibrated yaw zero_count as-is.
enum class YawHomeZero { Off, Low, High };

struct AppConfig {
    GeneralConfig  general;
    NetworkConfig  network;
    SerialConfig   serial;
    CameraConfig   camera;
    PathsConfig    paths;
    FeaturesConfig features;
    LoggingConfig  logging;
    UiConfig       ui;        // [UI] terminal dashboard toggle
    Geometry       geometry;  // [Motor] degrees<->counts maps (yaw + pitch)
    YawHomeZero    yaw_home_zero = YawHomeZero::Off;  // [Motor] re-anchor yaw zero on homing
    long           enc_error_warn_counts = 400;       // [Motor] live encoder-error WARN (0=off)
    ScanConfig     scan;      // [Scan] grid source
    ImuConfig      imu;       // [IMU] Xsens MTi serial device
    TestConfig     test;      // [Test] hardware self-test profiles

    // Capture rate in images/second (derived from general.image_interval).
    double image_rate() const;
};

// Loads and validates configuration.
//
// Secrets: mqtt_user / mqtt_pw are taken from the environment variables
// FGC_MQTT_USER / FGC_MQTT_PW when present; the INI values are a fallback.
class ConfigLoader {
public:
    // Reads the INI file at `path` (via the bundled inih parser), maps it into
    // a typed AppConfig, applies environment overrides, and validates it.
    // Throws std::runtime_error with a clear message on failure.
    static AppConfig loadFromFile(const std::string& path);

    // Same mapping/validation logic, but from an already-parsed key->value map
    // ("Section.name" => value). Exposed for unit testing without file IO.
    static AppConfig fromMap(const std::map<std::string, std::string>& kv);
};

// Return `contents` with the given `key = value` pairs set under `[section]`:
// existing keys in that section are replaced in place (comments/order preserved),
// and any missing ones are appended in a `[section]` block at the end. Pure (no
// I/O) so it is unit-testable.
std::string updateIniSectionKeys(const std::string& contents, const std::string& section,
                                 const std::vector<std::pair<std::string, std::string>>& kv);

// Persist the live `[Motor]` calibration (counts_per_deg / zero_count for both
// axes) back into the INI file at `path`. Returns true on success.
bool saveMotorCalibration(const std::string& path, const Geometry& geo);

}  // namespace fgc
