//! config.rs — Dual-CSS HF Mode
//! Configuration structs + JSON persistence.
//!
//! Faithful port of config.py. Field names and the on-disk JSON layout are kept
//! identical so the Rust app is config-compatible with the Python version.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

// ── Audio ───────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AudioConfig {
    pub input_device: i32, // -1 = system default
    pub output_device: i32,
    pub sample_rate: u32,
    pub buffer_size: u32,
    pub input_gain_db: f32,
    pub output_level: f32, // 0..1
}

impl Default for AudioConfig {
    fn default() -> Self {
        Self {
            input_device: -1,
            output_device: -1,
            sample_rate: 48000,
            buffer_size: 2048,
            input_gain_db: 0.0,
            output_level: 0.8,
        }
    }
}

// ── Modem ───────────────────────────────────────────────────────────────────

/// Audio bandpass the modem must stay inside (SSB passband).
pub const BAND_MIN_HZ: u32 = 300;
pub const BAND_MAX_HZ: u32 = 3000;

/// Bandwidth presets shared by the quick settings and the auto-detect
/// scanner (the scanner only hypothesises these, so a station transmitting on
/// a preset is always discoverable). The narrow presets trade rate for
/// sensitivity: halving the bandwidth halves the in-band noise power (+3 dB)
/// at half the data rate.
pub const BW_PRESETS: [u32; 8] = [100, 250, 500, 1000, 1500, 2000, 2500, 2700];

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ModemConfig {
    pub spreading_factor: u32, // SF 7–12
    pub freq_low: u32,         // Hz, lower audio bound
    pub freq_high: u32,        // Hz, upper audio bound
    pub l1_preamble_symbols: u32,
    pub l2_preamble_symbols: u32,
    pub sync_symbols: u32,
    pub interleaver_depth: u32,
    /// Auto-detect incoming SF/bandwidth from the preamble (rate comes from
    /// the frame header). Fixed-parameter RX when false.
    pub auto_rx: bool,
    /// Turbo mode (TX side): the down chirp carries an independent symbol
    /// instead of mirroring the up symbol, doubling the data rate at the cost
    /// of the joint-constraint redundancy. RX needs no setting — the mode is
    /// carried in each frame's header.
    pub turbo: bool,
}

impl Default for ModemConfig {
    fn default() -> Self {
        Self {
            spreading_factor: 10,
            freq_low: 300,
            freq_high: 3000,
            l1_preamble_symbols: 8,
            l2_preamble_symbols: 6,
            sync_symbols: 2,
            interleaver_depth: 8,
            auto_rx: true,
            turbo: false,
        }
    }
}

impl ModemConfig {
    /// Audio bandwidth in Hz (freq_high − freq_low, floored at 50 Hz so the
    /// DSP never divides by zero even if the fields are momentarily inverted).
    pub fn bandwidth(&self) -> u32 {
        self.freq_high.saturating_sub(self.freq_low).max(50)
    }

    /// Number of chips per symbol = 2^spreading_factor.
    pub fn n_chips(&self) -> u32 {
        1u32 << self.spreading_factor
    }

    /// Symbol duration in milliseconds.
    pub fn symbol_duration_ms(&self) -> f64 {
        self.n_chips() as f64 / self.bandwidth() as f64 * 1000.0
    }

    /// Centre frequency of the modem passband.
    pub fn center_freq(&self) -> u32 {
        (self.freq_low + self.freq_high) / 2
    }

    /// Place the passband by centre frequency + bandwidth, clamped so the
    /// whole signal stays inside the 300–3000 Hz bandpass.
    pub fn set_band(&mut self, center: u32, bw: u32) {
        let bw = bw.clamp(50, BAND_MAX_HZ - BAND_MIN_HZ);
        let half = bw / 2;
        let center = center.clamp(BAND_MIN_HZ + half, BAND_MAX_HZ - (bw - half));
        self.freq_low = center - half;
        self.freq_high = self.freq_low + bw;
    }
}

// ── FEC ─────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FecConfig {
    pub fec_type: String,          // RS | BCH | None
    pub rs_code_rate: String,      // 1/2 | 1/3 | 2/3
    pub header_protection: String, // RS(15,3) | RS(7,3) | BCH(31,6) | Repeat
    pub crc_enable: bool,
    pub crc_type: String, // CRC-16 | CRC-32
}

impl Default for FecConfig {
    fn default() -> Self {
        Self {
            fec_type: "RS".to_string(),
            rs_code_rate: "1/2".to_string(),
            header_protection: "RS(15,3)".to_string(),
            crc_enable: true,
            crc_type: "CRC-16".to_string(),
        }
    }
}

// ── PTT ─────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PttConfig {
    pub method: String, // RTS | DTR | VOX | CAT | flrig | None
    pub serial_port: String,
    pub ptt_delay_ms: u32, // ms to wait before TX audio
    pub hang_time_ms: u32, // ms silence before dropping PTT
    pub vox_level: f32,    // 0..1 trigger threshold
    pub cat_baud: u32,
    pub flrig_host: String, // flrig XML-RPC host
    pub flrig_port: u16,    // flrig XML-RPC port (default 12345)
}

impl Default for PttConfig {
    fn default() -> Self {
        Self {
            method: "RTS".to_string(),
            serial_port: "COM1".to_string(),
            ptt_delay_ms: 50,
            hang_time_ms: 500,
            vox_level: 0.7,
            cat_baud: 9600,
            flrig_host: "127.0.0.1".to_string(),
            flrig_port: 12345,
        }
    }
}

// ── Station ───────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct StationConfig {
    pub callsign: String,
    pub grid_square: String,
    pub name: String,
    pub log_file: String,
}

impl Default for StationConfig {
    fn default() -> Self {
        Self {
            callsign: String::new(),
            grid_square: String::new(),
            name: String::new(),
            log_file: "dual_css_hf.log".to_string(),
        }
    }
}

// ── Display ───────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DisplayConfig {
    pub waterfall_gain: f32,
    pub waterfall_floor: f32,    // dB reference minimum
    pub color_scheme: String,    // default | hot | gray
    pub scroll_speed: u32,       // rows advanced per DSP update
    pub fft_size: usize,         // FFT length: 2048..=32768 (power of two)
    pub waterfall_freq_min: u32, // Hz, low edge of the displayed band (WSJT-X style)
    pub waterfall_freq_max: u32, // Hz, high edge of the displayed band
    pub gpu_fft: bool,           // use the wgpu GPU-compute FFT (CPU rustfft is the fallback)
}

impl Default for DisplayConfig {
    fn default() -> Self {
        Self {
            waterfall_gain: 1.0,
            waterfall_floor: -100.0,
            color_scheme: "default".to_string(),
            scroll_speed: 1,
            fft_size: 4096,
            waterfall_freq_min: 0,
            waterfall_freq_max: 3500,
            gpu_fft: false,
        }
    }
}

impl DisplayConfig {
    /// Clamp the FFT size to a supported power of two in [2048, 32768].
    pub fn clamped_fft_size(&self) -> usize {
        const ALLOWED: [usize; 5] = [2048, 4096, 8192, 16384, 32768];
        if ALLOWED.contains(&self.fft_size) {
            self.fft_size
        } else {
            // Snap to the nearest allowed value.
            *ALLOWED
                .iter()
                .min_by_key(|&&v| (v as i64 - self.fft_size as i64).abs())
                .unwrap()
        }
    }
}

// ── Root config ─────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AppConfig {
    pub audio: AudioConfig,
    pub modem: ModemConfig,
    pub fec: FecConfig,
    pub ptt: PttConfig,
    pub station: StationConfig,
    pub display: DisplayConfig,
}

impl AppConfig {
    /// Clamp loaded values into ranges the DSP can actually handle. The GUI
    /// keeps these invariants on its own, but a hand-edited JSON could
    /// otherwise panic the modem (`1 << sf` with sf ≥ 32, `freq_high <
    /// freq_low` underflowing `bandwidth()`, a zero sample rate, …).
    pub fn sanitize(&mut self) {
        let m = &mut self.modem;
        m.spreading_factor = m.spreading_factor.clamp(7, 12);
        // Re-derive the band via the centre/bw helper, which keeps the whole
        // signal inside the 300–3000 Hz bandpass.
        let bw = m.freq_high.saturating_sub(m.freq_low).max(50);
        let center = (m.freq_low + m.freq_high) / 2;
        m.set_band(center, bw);
        m.l1_preamble_symbols = m.l1_preamble_symbols.clamp(4, 64);
        m.l2_preamble_symbols = m.l2_preamble_symbols.clamp(1, 64);
        m.sync_symbols = m.sync_symbols.clamp(1, 16);

        let a = &mut self.audio;
        a.sample_rate = a.sample_rate.clamp(8000, 192_000);
        a.output_level = a.output_level.clamp(0.0, 1.0);

        let d = &mut self.display;
        d.scroll_speed = d.scroll_speed.clamp(1, 10);
        d.fft_size = d.clamped_fft_size();
    }
}

/// Path to the JSON config file, kept next to the executable's working dir.
/// Matches the Python CONFIG_FILE name for cross-compatibility.
fn config_file() -> PathBuf {
    PathBuf::from("dual_css_hf_config.json")
}

/// Loads/saves AppConfig from JSON on disk.
pub struct ConfigManager {
    pub config: AppConfig,
    path: PathBuf,
}

impl ConfigManager {
    pub fn new() -> Self {
        let mut mgr = Self {
            config: AppConfig::default(),
            path: config_file(),
        };
        mgr.load();
        mgr
    }

    pub fn load(&mut self) {
        if !self.path.exists() {
            return;
        }
        match std::fs::read_to_string(&self.path) {
            Ok(text) => {
                // `#[serde(default)]` on every struct means unknown-but-missing
                // fields fall back to defaults, replicating the Python loader's
                // "only apply known fields so old configs survive" behaviour.
                match serde_json::from_str::<AppConfig>(&text) {
                    Ok(mut cfg) => {
                        cfg.sanitize();
                        self.config = cfg;
                    }
                    Err(exc) => eprintln!("[Config] Load error: {exc}"),
                }
            }
            Err(exc) => eprintln!("[Config] Load error: {exc}"),
        }
    }

    pub fn save(&self) {
        match serde_json::to_string_pretty(&self.config) {
            Ok(text) => {
                if let Err(exc) = std::fs::write(&self.path, text) {
                    eprintln!("[Config] Save error: {exc}");
                }
            }
            Err(exc) => eprintln!("[Config] Save error: {exc}"),
        }
    }
}

impl Default for ConfigManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn modem_derived_values() {
        let m = ModemConfig::default();
        assert_eq!(m.bandwidth(), 2700);
        assert_eq!(m.n_chips(), 1024);
        // 1024 / 2700 * 1000 ≈ 379.26 ms
        assert!((m.symbol_duration_ms() - 379.259).abs() < 0.01);
    }

    #[test]
    fn defaults_roundtrip_json() {
        let cfg = AppConfig::default();
        let json = serde_json::to_string(&cfg).unwrap();
        let back: AppConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(back.audio.sample_rate, 48000);
        assert_eq!(back.modem.spreading_factor, 10);
        assert_eq!(back.fec.rs_code_rate, "1/2");
    }

    #[test]
    fn sanitize_repairs_hostile_values() {
        // A hand-edited config must not be able to panic the DSP:
        // sf=40 would overflow `1u32 << sf`; inverted band edges would
        // underflow bandwidth(); zero sample rate divides by zero.
        let mut cfg = AppConfig::default();
        cfg.modem.spreading_factor = 40;
        cfg.modem.freq_low = 3000;
        cfg.modem.freq_high = 300;
        cfg.audio.sample_rate = 0;
        cfg.display.fft_size = 7;
        cfg.sanitize();
        assert_eq!(cfg.modem.spreading_factor, 12);
        assert!(cfg.modem.freq_high > cfg.modem.freq_low);
        assert!(cfg.modem.bandwidth() >= 50);
        assert!(cfg.audio.sample_rate >= 8000);
        assert_eq!(cfg.display.fft_size, 2048);
    }

    #[test]
    fn bandwidth_never_underflows() {
        let m = ModemConfig {
            freq_low: 3000,
            freq_high: 300, // inverted
            ..Default::default()
        };
        assert_eq!(m.bandwidth(), 50); // floored, not wrapped to ~4 billion
    }

    #[test]
    fn set_band_stays_inside_bandpass() {
        let mut m = ModemConfig::default();
        // Centre pushed past the top edge → band slides back inside.
        m.set_band(5000, 1000);
        assert!(m.freq_low >= BAND_MIN_HZ && m.freq_high <= BAND_MAX_HZ);
        assert_eq!(m.bandwidth(), 1000);
        // Centre below the bottom edge.
        m.set_band(0, 500);
        assert!(m.freq_low >= BAND_MIN_HZ && m.freq_high <= BAND_MAX_HZ);
        assert_eq!(m.bandwidth(), 500);
        // Bandwidth wider than the whole bandpass gets clamped to fit.
        m.set_band(1650, 9999);
        assert_eq!(m.freq_low, BAND_MIN_HZ);
        assert_eq!(m.freq_high, BAND_MAX_HZ);
        // Centre is preserved when it fits.
        m.set_band(800, 500);
        assert_eq!(m.center_freq(), 800);
        assert_eq!(m.bandwidth(), 500);
    }

    #[test]
    fn partial_json_uses_defaults() {
        // Old/partial config: only one nested field present.
        let json = r#"{"modem": {"spreading_factor": 12}}"#;
        let cfg: AppConfig = serde_json::from_str(json).unwrap();
        assert_eq!(cfg.modem.spreading_factor, 12);
        // Missing fields fall back to defaults.
        assert_eq!(cfg.modem.freq_low, 300);
        assert_eq!(cfg.audio.sample_rate, 48000);
    }
}
