//! TRACES: UR-023, UR-027, UR-031, UR-032, UR-033 | DR-030, DR-034, DR-035, DR-036, DR-048, IR-020

use serde::{Deserialize, Serialize};

/// Volume normalization levels matching Spotify's presets
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum VolumeLevel {
    /// Louder output (-11 LUFS)
    Loud,
    /// Default level (-14 LUFS)
    #[default]
    Normal,
    /// Quieter output (-23 LUFS)
    Quiet,
}

impl VolumeLevel {
    /// Get the target LUFS value for this volume level
    pub fn target_lufs(&self) -> f32 {
        match self {
            VolumeLevel::Loud => -11.0,
            VolumeLevel::Normal => -14.0,
            VolumeLevel::Quiet => -23.0,
        }
    }
}

/// Centre frequencies (Hz) of the fixed 10-band ISO equalizer. The band count
/// and layout are a property of the audio engine, not the UI — presets and the
/// MPV filter are defined against these bands. See docs/specs/audio-equalizer.md.
///
/// TRACES: UR-027 | DR-030, IR-020
pub const EQ_BANDS: [f32; 10] = [
    31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0,
];
/// Minimum per-band gain in dB.
pub const EQ_GAIN_MIN: f32 = -12.0;
/// Maximum per-band gain in dB.
pub const EQ_GAIN_MAX: f32 = 12.0;

/// Built-in equalizer presets. A preset *is* a gain curve defined by the band
/// layout above (a domain concept), not a mere label — the curve numbers live
/// in Rust so the frontend never encodes the taxonomy.
///
/// TRACES: UR-027 | DR-030
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EqPreset {
    Flat,
    Rock,
    Pop,
    Jazz,
    Classical,
    BassBoost,
    TrebleBoost,
    Vocal,
}

impl EqPreset {
    /// All presets, for enumerating the curve table across the IPC boundary.
    pub const ALL: [EqPreset; 8] = [
        EqPreset::Flat,
        EqPreset::Rock,
        EqPreset::Pop,
        EqPreset::Jazz,
        EqPreset::Classical,
        EqPreset::BassBoost,
        EqPreset::TrebleBoost,
        EqPreset::Vocal,
    ];

    /// The 10-band gain curve (dB) for this preset, one entry per [`EQ_BANDS`].
    /// Curves are conservative (within ±8 dB) so presets stack safely with the
    /// player volume. Bands: 31 62 125 250 500 1k 2k 4k 8k 16k.
    pub fn gains(&self) -> [f32; 10] {
        match self {
            EqPreset::Flat => [0.0; 10],
            EqPreset::Rock => [5.0, 4.0, 3.0, 1.0, -1.0, -1.0, 1.0, 3.0, 4.0, 5.0],
            EqPreset::Pop => [-1.0, 0.0, 2.0, 4.0, 5.0, 4.0, 2.0, 0.0, -1.0, -1.0],
            EqPreset::Jazz => [3.0, 2.0, 1.0, 2.0, -1.0, -1.0, 0.0, 1.0, 2.0, 3.0],
            EqPreset::Classical => [4.0, 3.0, 2.0, 1.0, -1.0, -1.0, 0.0, 2.0, 3.0, 4.0],
            EqPreset::BassBoost => [7.0, 6.0, 5.0, 3.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            EqPreset::TrebleBoost => [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 3.0, 5.0, 6.0, 7.0],
            EqPreset::Vocal => [-2.0, -1.0, 0.0, 2.0, 4.0, 5.0, 4.0, 2.0, 0.0, -1.0],
        }
    }
}

/// Audio playback settings
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioSettings {
    /// Crossfade duration in seconds (0 = disabled, max 12)
    pub crossfade_duration: f32,
    /// Enable gapless playback between tracks
    pub gapless_playback: bool,
    /// Enable volume normalization
    pub normalize_volume: bool,
    /// Target volume level for normalization
    pub volume_level: VolumeLevel,
    /// Enable the graphic equalizer. When false, no EQ filter is applied.
    #[serde(default)]
    pub equalizer_enabled: bool,
    /// Per-band gains in dB, one per [`EQ_BANDS`]. Normalised to 10 entries and
    /// clamped to [`EQ_GAIN_MIN`, `EQ_GAIN_MAX`] via [`Self::with_equalizer_normalised`].
    #[serde(default = "default_eq_bands")]
    pub equalizer_bands: Vec<f32>,
}

/// Flat 10-band curve — the default equalizer state.
fn default_eq_bands() -> Vec<f32> {
    vec![0.0; EQ_BANDS.len()]
}

impl Default for AudioSettings {
    fn default() -> Self {
        Self {
            crossfade_duration: 0.0,
            gapless_playback: true,
            normalize_volume: false,
            volume_level: VolumeLevel::Normal,
            equalizer_enabled: false,
            equalizer_bands: default_eq_bands(),
        }
    }
}

impl AudioSettings {
    /// Clamp crossfade duration to valid range (0-12 seconds)
    pub fn with_crossfade_clamped(mut self) -> Self {
        self.crossfade_duration = self.crossfade_duration.clamp(0.0, 12.0);
        self
    }

    /// Normalise the equalizer band vector to exactly [`EQ_BANDS`]`.len()`
    /// entries (pad with 0 dB / truncate) and clamp each gain to the valid
    /// range. Guards against malformed persisted or IPC input.
    ///
    /// TRACES: UR-027 | DR-030
    pub fn with_equalizer_normalised(mut self) -> Self {
        let n = EQ_BANDS.len();
        self.equalizer_bands.resize(n, 0.0);
        for g in &mut self.equalizer_bands {
            *g = g.clamp(EQ_GAIN_MIN, EQ_GAIN_MAX);
        }
        self
    }
}

/// Video playback settings
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoSettings {
    /// Enable auto-play of next episode (with countdown)
    pub auto_play_next_episode: bool,
    /// Countdown duration in seconds before auto-play (5-30 seconds)
    pub auto_play_countdown_seconds: u32,
    /// Maximum number of episodes to auto-play consecutively (0 = unlimited)
    #[serde(default)]
    pub auto_play_max_episodes: u32,
}

impl Default for VideoSettings {
    fn default() -> Self {
        Self {
            auto_play_next_episode: true,
            auto_play_countdown_seconds: 10,
            auto_play_max_episodes: 0,
        }
    }
}

impl VideoSettings {
    /// Clamp countdown duration to valid range (5-30 seconds)
    pub fn with_countdown_clamped(mut self) -> Self {
        self.auto_play_countdown_seconds = self.auto_play_countdown_seconds.clamp(5, 30);
        self
    }
}

/// Serialise `AudioSettings` into the JSON payload handed to the Android player
/// over JNI.
///
/// Sanitises first (crossfade clamped, band vector normalised) so a malformed
/// vector can never reach the Kotlin parser. JSON is used rather than a wide JNI
/// signature so that adding a field does not change the method signature — the
/// same approach `load()` already uses for subtitles.
///
/// The emitted keys are camelCase (serde) and `volumeLevel` is lowercase; the
/// Kotlin side matches on those literals. Both are pinned by tests.
///
/// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036
pub fn audio_settings_jni_payload(settings: &AudioSettings) -> Result<String, serde_json::Error> {
    let sanitised = settings
        .clone()
        .with_crossfade_clamped()
        .with_equalizer_normalised();
    serde_json::to_string(&sanitised)
}

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

    /// The JNI payload must sanitise before serialising: an over-long crossfade
    /// is clamped and a wrong-length band vector is normalised to EQ_BANDS.len().
    /// Sending raw values would let a malformed vector reach the Kotlin parser.
    ///
    /// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 | UT-AUDIO-JNI-1
    #[test]
    fn test_audio_settings_jni_payload_is_sanitised() {
        let settings = AudioSettings {
            crossfade_duration: 30.0,
            equalizer_bands: vec![20.0, -30.0],
            ..AudioSettings::default()
        };

        let json = audio_settings_jni_payload(&settings).expect("serialises");
        let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");

        assert_eq!(v["crossfadeDuration"], 12.0, "crossfade clamped to 12s");

        let bands = v["equalizerBands"].as_array().expect("bands array");
        assert_eq!(bands.len(), EQ_BANDS.len(), "band vector normalised to 10");
        assert_eq!(bands[0], EQ_GAIN_MAX as f64, "gain clamped to +12dB");
        assert_eq!(bands[1], EQ_GAIN_MIN as f64, "gain clamped to -12dB");
    }

    /// The Kotlin side parses these exact keys. camelCase is what serde emits
    /// for AudioSettings; a rename here silently breaks the Android parser,
    /// which is why the contract is pinned by a test rather than by convention.
    ///
    /// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 | UT-AUDIO-JNI-2
    #[test]
    fn test_audio_settings_jni_payload_key_contract() {
        let json = audio_settings_jni_payload(&AudioSettings::default()).expect("serialises");
        let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");

        for key in [
            "crossfadeDuration",
            "gaplessPlayback",
            "normalizeVolume",
            "volumeLevel",
            "equalizerEnabled",
            "equalizerBands",
        ] {
            assert!(v.get(key).is_some(), "JNI payload must carry `{key}`");
        }

        // VolumeLevel is #[serde(rename_all = "lowercase")]; Kotlin matches on
        // these literals.
        assert_eq!(v["volumeLevel"], "normal");
    }

    #[test]
    fn test_default_settings() {
        let settings = AudioSettings::default();
        assert_eq!(settings.crossfade_duration, 0.0);
        assert!(settings.gapless_playback);
        assert!(!settings.normalize_volume);
        assert_eq!(settings.volume_level, VolumeLevel::Normal);
        // Equalizer defaults: disabled and flat.
        assert!(!settings.equalizer_enabled);
        assert_eq!(settings.equalizer_bands, vec![0.0; EQ_BANDS.len()]);
    }

    /// EQ presets each return one gain per band; Flat is all zeros.
    ///
    /// TRACES: UR-027 | DR-030 | UT-079
    #[test]
    fn test_eq_preset_curves() {
        for preset in EqPreset::ALL {
            assert_eq!(
                preset.gains().len(),
                EQ_BANDS.len(),
                "preset {:?} must have one gain per band",
                preset
            );
            // Every preset stays within the advertised gain range.
            for g in preset.gains() {
                assert!(
                    (EQ_GAIN_MIN..=EQ_GAIN_MAX).contains(&g),
                    "preset {:?} gain {} out of range",
                    preset,
                    g
                );
            }
        }
        assert_eq!(EqPreset::Flat.gains(), [0.0; 10]);
        // Bass boost lifts the low bands and leaves the top flat.
        let bass = EqPreset::BassBoost.gains();
        assert!(bass[0] > 0.0 && bass[9] == 0.0);
    }

    /// `with_equalizer_normalised` clamps out-of-range gains and forces the
    /// band vector to exactly EQ_BANDS.len() (pad short, truncate long).
    ///
    /// TRACES: UR-027 | DR-030 | UT-080
    #[test]
    fn test_eq_normalisation() {
        // Out-of-range gains are clamped.
        let s = AudioSettings {
            equalizer_bands: vec![100.0, -100.0, 3.0],
            ..Default::default()
        }
        .with_equalizer_normalised();
        assert_eq!(s.equalizer_bands.len(), EQ_BANDS.len());
        assert_eq!(s.equalizer_bands[0], EQ_GAIN_MAX);
        assert_eq!(s.equalizer_bands[1], EQ_GAIN_MIN);
        assert_eq!(s.equalizer_bands[2], 3.0);
        // Short vector padded with 0 dB.
        assert_eq!(s.equalizer_bands[9], 0.0);

        // Over-long vector truncated.
        let long = AudioSettings {
            equalizer_bands: vec![1.0; 20],
            ..Default::default()
        }
        .with_equalizer_normalised();
        assert_eq!(long.equalizer_bands.len(), EQ_BANDS.len());
    }

    /// Old persisted JSON without the EQ fields loads as disabled + flat.
    ///
    /// TRACES: UR-027 | DR-030 | UT-081
    #[test]
    fn test_audio_settings_eq_backward_compat() {
        let json = r#"{"crossfadeDuration":0.0,"gaplessPlayback":true,"normalizeVolume":false,"volumeLevel":"normal"}"#;
        let parsed: AudioSettings = serde_json::from_str(json).unwrap();
        assert!(!parsed.equalizer_enabled);
        assert_eq!(parsed.equalizer_bands, vec![0.0; EQ_BANDS.len()]);
    }

    /// EQ fields serialize as camelCase and round-trip.
    ///
    /// TRACES: UR-027 | DR-030 | UT-082
    #[test]
    fn test_audio_settings_eq_serialization() {
        let settings = AudioSettings {
            equalizer_enabled: true,
            equalizer_bands: EqPreset::Rock.gains().to_vec(),
            ..Default::default()
        };
        let json = serde_json::to_string(&settings).unwrap();
        assert!(json.contains("\"equalizerEnabled\":true"));
        assert!(json.contains("\"equalizerBands\":"));

        let parsed: AudioSettings = serde_json::from_str(&json).unwrap();
        assert!(parsed.equalizer_enabled);
        assert_eq!(parsed.equalizer_bands, EqPreset::Rock.gains().to_vec());
    }

    #[test]
    fn test_volume_level_lufs() {
        assert_eq!(VolumeLevel::Loud.target_lufs(), -11.0);
        assert_eq!(VolumeLevel::Normal.target_lufs(), -14.0);
        assert_eq!(VolumeLevel::Quiet.target_lufs(), -23.0);
    }

    #[test]
    fn test_crossfade_clamping() {
        let settings = AudioSettings {
            crossfade_duration: 20.0,
            ..Default::default()
        }
        .with_crossfade_clamped();
        assert_eq!(settings.crossfade_duration, 12.0);

        let settings = AudioSettings {
            crossfade_duration: -5.0,
            ..Default::default()
        }
        .with_crossfade_clamped();
        assert_eq!(settings.crossfade_duration, 0.0);
    }

    #[test]
    fn test_settings_serialization() {
        let settings = AudioSettings {
            crossfade_duration: 5.0,
            gapless_playback: true,
            normalize_volume: true,
            volume_level: VolumeLevel::Loud,
            ..Default::default()
        };

        let json = serde_json::to_string(&settings).unwrap();
        assert!(json.contains("\"crossfadeDuration\":5.0"));
        assert!(json.contains("\"gaplessPlayback\":true"));
        assert!(json.contains("\"normalizeVolume\":true"));
        assert!(json.contains("\"volumeLevel\":\"loud\""));

        let parsed: AudioSettings = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.crossfade_duration, 5.0);
        assert_eq!(parsed.volume_level, VolumeLevel::Loud);
    }

    #[test]
    fn test_video_default_settings() {
        let settings = VideoSettings::default();
        assert!(settings.auto_play_next_episode);
        assert_eq!(settings.auto_play_countdown_seconds, 10);
        assert_eq!(settings.auto_play_max_episodes, 0);
    }

    #[test]
    fn test_video_countdown_clamping() {
        let settings = VideoSettings {
            auto_play_countdown_seconds: 60,
            ..Default::default()
        }
        .with_countdown_clamped();
        assert_eq!(settings.auto_play_countdown_seconds, 30);

        let settings = VideoSettings {
            auto_play_countdown_seconds: 2,
            ..Default::default()
        }
        .with_countdown_clamped();
        assert_eq!(settings.auto_play_countdown_seconds, 5);
    }

    #[test]
    fn test_video_settings_serialization() {
        let settings = VideoSettings {
            auto_play_next_episode: false,
            auto_play_countdown_seconds: 15,
            auto_play_max_episodes: 5,
        };

        let json = serde_json::to_string(&settings).unwrap();
        assert!(json.contains("\"autoPlayNextEpisode\":false"));
        assert!(json.contains("\"autoPlayCountdownSeconds\":15"));
        assert!(json.contains("\"autoPlayMaxEpisodes\":5"));

        let parsed: VideoSettings = serde_json::from_str(&json).unwrap();
        assert!(!parsed.auto_play_next_episode);
        assert_eq!(parsed.auto_play_countdown_seconds, 15);
        assert_eq!(parsed.auto_play_max_episodes, 5);
    }

    #[test]
    fn test_video_settings_backward_compat() {
        // Old JSON without auto_play_max_episodes field
        let json = r#"{"autoPlayNextEpisode":true,"autoPlayCountdownSeconds":10}"#;
        let parsed: VideoSettings = serde_json::from_str(json).unwrap();
        assert!(parsed.auto_play_next_episode);
        assert_eq!(parsed.auto_play_countdown_seconds, 10);
        assert_eq!(parsed.auto_play_max_episodes, 0);
    }
}
