// SPDX-License-Identifier: GPL-3.0-or-later
//! View models for the GUI — pure logic, fully testable.
//!
//! BDD Specs:
//!
//! P1: Display list with sliders
//!   Given 2 displays detected (A at 50%, B at 30%)
//!   When the view model is created
//!   Then display_rows returns 2 rows with correct names and brightness values
//!
//! P2: Sync toggle
//!   Given sync is OFF and display A changes from 50% to 70%
//!   When the view model applies the change
//!   Then only display A changes to 70%, B stays at 30%
//!
//!   Given sync is ON and display A changes from 50% to 70%
//!   When the view model applies the change
//!   Then display B also shifts by +20% to 50%
//!
//! P3: Display rename
//!   Given display 1 named "DDC Display 1"
//!   When I rename it to "Left Monitor"
//!   Then the view model shows "Left Monitor" and settings are persisted
//!
//! P4: Min/max caps
//!   Given display 1 with min=20%, max=80% caps
//!   When I set brightness to 100% via slider
//!   Then the view model clamps to 80%
//!
//! P5: Hardware write on slider change
//!   Given display 1 (DDC, raw_max=100) at 50%
//!   When the slider changes to 75%
//!   Then a HardwareCommand::SetDdc { index: 1, raw: 75 } is returned
//!
//! P6: Rename persists to settings
//!   Given display 1 (DDC index 0)
//!   When I rename it to "Left Monitor"
//!   Then settings file contains ddc:1.custom_name = "Left Monitor"
//!
//! P7: Cap changes persist to settings
//!   Given display 1 (DDC index 0)
//!   When I set min=10%, max=90%
//!   Then settings file contains ddc:1.min_brightness=10, max_brightness=90
//!
//! P8: Sync toggle persists to settings
//!   Given sync is OFF
//!   When I toggle sync ON
//!   Then settings.proportional_sync is true

use crate::caps::clamp_brightness;
use crate::display::provider::BacklightProvider;
use crate::display::DisplayType;
use crate::settings::{DisplaySettings, Settings};
use crate::sync::apply_sync_delta;
use anyhow::Result;

/// A command to execute on hardware, returned by view model operations.
#[derive(Debug, Clone, PartialEq)]
pub enum HardwareCommand {
    SetDdc { index: usize, raw: i32 },
    SetBacklight { name: String, raw: u32 },
}

/// View model for a single display row in the UI.
#[derive(Debug, Clone)]
pub struct DisplayRow {
    pub index: usize,
    pub name: String,
    pub hardware_name: String,
    pub brightness_pct: f64,
    pub display_type: DisplayType,
    pub raw_max: u64,
    pub min_pct: f64,
    pub max_pct: f64,
    /// For DDC: index into ddc detect result. For backlight: display name.
    pub backend_key: BackendKey,
}

/// Identifies which backend and how to address a display.
#[derive(Debug, Clone)]
pub enum BackendKey {
    Ddc(usize),
    Backlight(String),
}

/// Result of a cap change operation.
#[derive(Debug, Clone, PartialEq)]
pub struct CapsChangeResult {
    pub settings_key: String,
    pub re_clamped: Option<HardwareCommand>,
}

/// View model for the main window.
#[derive(Debug, Clone)]
pub struct MainViewModel {
    pub rows: Vec<DisplayRow>,
    pub sync_enabled: bool,
}

impl MainViewModel {
    /// Create from pre-detected DDC displays (for async detection with progress).
    /// Detects backlight displays synchronously (fast, local filesystem).
    pub fn from_detected(
        ddc_list: &[crate::display::ddc_ffi::DdcDisplayInfo],
        backlight: &dyn BacklightProvider,
        settings: &Settings,
    ) -> Result<Self> {
        let mut rows = Vec::new();
        let mut index = 0;

        let bl_list = backlight.detect()?;
        for bl in &bl_list {
            index += 1;
            let pct = if bl.max > 0 {
                bl.current as f64 / bl.max as f64 * 100.0
            } else {
                0.0
            };
            let key = format!("backlight:{}", bl.name);
            let ds = settings.get_display(&key);
            let custom_name = settings.get_custom_name(&key);
            rows.push(DisplayRow {
                index,
                name: custom_name.unwrap_or_else(|| {
                    if bl_list.len() == 1 {
                        "Built-in Display".to_string()
                    } else {
                        format!("Backlight ({})", bl.name)
                    }
                }),
                hardware_name: if bl_list.len() == 1 {
                    "Built-in Display".to_string()
                } else {
                    format!("Backlight ({})", bl.name)
                },
                brightness_pct: pct,
                display_type: DisplayType::Backlight,
                raw_max: bl.max as u64,
                min_pct: ds.min_brightness,
                max_pct: ds.max_brightness,
                backend_key: BackendKey::Backlight(bl.name.clone()),
            });
        }

        for ddc_disp in ddc_list.iter() {
            index += 1;
            let pct = if ddc_disp.max_brightness > 0 {
                ddc_disp.current_brightness as f64 / ddc_disp.max_brightness as f64 * 100.0
            } else {
                0.0
            };
            let key = format!("ddc:{}", ddc_disp.display_num);
            let ds = settings.get_display(&key);
            let custom_name = settings.get_custom_name(&key);
            rows.push(DisplayRow {
                index,
                name: custom_name.unwrap_or(ddc_disp.model_name.clone()),
                hardware_name: ddc_disp.model_name.clone(),
                brightness_pct: pct,
                display_type: DisplayType::Ddc,
                raw_max: ddc_disp.max_brightness as u64,
                min_pct: ds.min_brightness,
                max_pct: ds.max_brightness,
                backend_key: BackendKey::Ddc(ddc_disp.display_num as usize),
            });
        }

        Ok(Self {
            rows,
            sync_enabled: settings.proportional_sync,
        })
    }

    /// Handle a brightness change from a slider.
    /// Returns (changed display indices, hardware commands to execute).
    pub fn on_brightness_change(
        &mut self,
        source_index: usize,
        new_pct: f64,
    ) -> (Vec<usize>, Vec<HardwareCommand>) {
        let source_row = match self.rows.iter().find(|r| r.index == source_index) {
            Some(r) => r.clone(),
            None => return (Vec::new(), Vec::new()),
        };

        let clamped = clamp_brightness(new_pct, source_row.min_pct, source_row.max_pct);

        if self.sync_enabled {
            let old_pct = source_row.brightness_pct;
            let delta = clamped - old_pct;
            let mut changed = Vec::new();
            let mut commands = Vec::new();

            for row in &mut self.rows {
                if row.index == source_index {
                    row.brightness_pct = clamped;
                    changed.push(row.index);
                    commands.push(make_command(row));
                } else {
                    let new_val = apply_sync_delta(row.brightness_pct, delta);
                    let capped = clamp_brightness(new_val, row.min_pct, row.max_pct);
                    if (row.brightness_pct - capped).abs() > f64::EPSILON {
                        row.brightness_pct = capped;
                        changed.push(row.index);
                        commands.push(make_command(row));
                    }
                }
            }
            (changed, commands)
        } else {
            for row in &mut self.rows {
                if row.index == source_index {
                    row.brightness_pct = clamped;
                    return (vec![source_index], vec![make_command(row)]);
                }
            }
            (Vec::new(), Vec::new())
        }
    }

    /// Toggle sync mode.
    pub fn toggle_sync(&mut self) {
        self.sync_enabled = !self.sync_enabled;
    }

    /// Rename a display and return the settings key to persist.
    pub fn rename(&mut self, index: usize, new_name: String) -> Option<String> {
        if let Some(row) = self.rows.iter_mut().find(|r| r.index == index) {
            let key = row_settings_key(row);
            row.name = new_name;
            Some(key)
        } else {
            None
        }
    }

    /// Set min/max caps for a display. Returns CapsChangeResult with settings key
    /// and optional hardware command if brightness was re-clamped.
    pub fn set_caps(
        &mut self,
        index: usize,
        min_pct: f64,
        max_pct: f64,
    ) -> Option<CapsChangeResult> {
        if let Some(row) = self.rows.iter_mut().find(|r| r.index == index) {
            row.min_pct = min_pct;
            row.max_pct = max_pct;
            let key = row_settings_key(row);

            // Re-clamp brightness if it's outside the new range
            let clamped = clamp_brightness(row.brightness_pct, min_pct, max_pct);
            let re_clamped = if (row.brightness_pct - clamped).abs() > f64::EPSILON {
                row.brightness_pct = clamped;
                Some(make_command(row))
            } else {
                None
            };

            Some(CapsChangeResult {
                settings_key: key,
                re_clamped,
            })
        } else {
            None
        }
    }

    /// Apply rename to settings (caller must save).
    pub fn apply_rename_to_settings(&self, key: &str, name: &str, settings: &mut Settings) {
        settings.set_custom_name(key, name);
    }

    /// Apply caps to settings (caller must save).
    pub fn apply_caps_to_settings(
        &self,
        key: &str,
        min_pct: f64,
        max_pct: f64,
        settings: &mut Settings,
    ) {
        let existing = settings.get_display(key);
        settings.set_display(
            key,
            DisplaySettings {
                min_brightness: min_pct,
                max_brightness: max_pct,
                custom_name: existing.custom_name,
            },
        );
    }

    /// Get the raw value for a given display row.
    pub fn raw_value_for(&self, display_index: usize) -> i32 {
        let row = self.rows.iter().find(|r| r.index == display_index);
        let pct = row.map(|r| r.brightness_pct).unwrap_or(0.0);
        let max = row.map(|r| r.raw_max).unwrap_or(100);
        (pct / 100.0 * max as f64).round() as i32
    }
}

/// Build a hardware command from a display row.
fn make_command(row: &DisplayRow) -> HardwareCommand {
    let raw = (row.brightness_pct / 100.0 * row.raw_max as f64).round() as i32;
    match &row.backend_key {
        BackendKey::Ddc(idx) => HardwareCommand::SetDdc {
            index: *idx,
            raw,
        },
        BackendKey::Backlight(name) => HardwareCommand::SetBacklight {
            name: name.clone(),
            raw: raw as u32,
        },
    }
}

/// Get the settings key for a display row.
fn row_settings_key(row: &DisplayRow) -> String {
    match &row.backend_key {
        BackendKey::Ddc(idx) => format!("ddc:{}", idx),
        BackendKey::Backlight(name) => format!("backlight:{}", name),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::display::ddc_ffi::DdcDisplayInfo as FfiDdcDisplayInfo;
    use crate::display::provider::BacklightDisplayInfo;

    struct MockBl { displays: Vec<BacklightDisplayInfo> }
    impl BacklightProvider for MockBl {
        fn detect(&self) -> Result<Vec<BacklightDisplayInfo>> { Ok(self.displays.clone()) }
        fn set_brightness(&self, _: &str, _: u32) -> Result<()> { Ok(()) }
    }

    fn ddc(name: &str, current: u16, max: u16) -> FfiDdcDisplayInfo {
        FfiDdcDisplayInfo { display_num: 1, model_name: name.to_string(), current_brightness: current, max_brightness: max }
    }

    fn ddc_n(num: i32, name: &str, current: u16, max: u16) -> FfiDdcDisplayInfo {
        FfiDdcDisplayInfo { display_num: num, model_name: name.to_string(), current_brightness: current, max_brightness: max }
    }

    fn bl(name: &str, current: u32, max: u32) -> BacklightDisplayInfo {
        BacklightDisplayInfo { name: name.to_string(), current, max }
    }

    // P1: Display list
    #[test]
    fn view_model_has_correct_rows() {
        let ddc_list = vec![ddc("A", 50, 100), ddc("B", 30, 100)];
        let bl = MockBl { displays: vec![] };
        let vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        assert_eq!(vm.rows.len(), 2);
        assert!((vm.rows[0].brightness_pct - 50.0).abs() < f64::EPSILON);
        assert!((vm.rows[1].brightness_pct - 30.0).abs() < f64::EPSILON);
    }

    // P2: Sync OFF
    #[test]
    fn sync_off_only_source_changes() {
        let ddc_list = vec![ddc("A", 50, 100), ddc("B", 30, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        vm.sync_enabled = false;
        let (changed, _) = vm.on_brightness_change(1, 70.0);
        assert_eq!(changed, vec![1]);
        assert!((vm.rows[0].brightness_pct - 70.0).abs() < f64::EPSILON);
        assert!((vm.rows[1].brightness_pct - 30.0).abs() < f64::EPSILON);
    }

    // P2: Sync ON
    #[test]
    fn sync_on_all_displays_shift() {
        let ddc_list = vec![ddc("A", 50, 100), ddc("B", 30, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        vm.sync_enabled = true;
        let (changed, _) = vm.on_brightness_change(1, 70.0);
        assert!(changed.contains(&1));
        assert!(changed.contains(&2));
        assert!((vm.rows[1].brightness_pct - 50.0).abs() < f64::EPSILON);
    }

    // P2: Toggle
    #[test]
    fn toggle_sync_flips() {
        let ddc_list: Vec<FfiDdcDisplayInfo> = vec![];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        assert!(!vm.sync_enabled);
        vm.toggle_sync();
        assert!(vm.sync_enabled);
    }

    // P3: Rename returns key
    #[test]
    fn rename_returns_settings_key() {
        let ddc_list = vec![ddc("Monitor", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let key = vm.rename(1, "Left Monitor".to_string()).unwrap();
        assert_eq!(key, "ddc:1");
        assert_eq!(vm.rows[0].name, "Left Monitor");
    }

    // P4: Caps clamp
    #[test]
    fn caps_clamp_to_max() {
        let ddc_list = vec![ddc("M", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut s = Settings::default();
        s.set_display("ddc:1", DisplaySettings { min_brightness: 0.0, max_brightness: 80.0, custom_name: None });
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &s).unwrap();
        let (_, cmds) = vm.on_brightness_change(1, 100.0);
        assert!((vm.rows[0].brightness_pct - 80.0).abs() < f64::EPSILON);
        // Hardware command should use clamped raw value (80% of 100 = 80)
        assert_eq!(cmds[0], HardwareCommand::SetDdc { index: 1, raw: 80 });
    }

    #[test]
    fn caps_clamp_to_min() {
        let ddc_list = vec![ddc("M", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut s = Settings::default();
        s.set_display("ddc:1", DisplaySettings { min_brightness: 20.0, max_brightness: 100.0, custom_name: None });
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &s).unwrap();
        vm.on_brightness_change(1, 10.0);
        assert!((vm.rows[0].brightness_pct - 20.0).abs() < f64::EPSILON);
    }

    // P5: Hardware command for DDC
    #[test]
    fn ddc_brightness_change_produces_hardware_command() {
        let ddc_list = vec![ddc("Monitor", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let (_, cmds) = vm.on_brightness_change(1, 75.0);
        assert_eq!(cmds.len(), 1);
        assert_eq!(cmds[0], HardwareCommand::SetDdc { index: 1, raw: 75 });
    }

    // P5: Hardware command for backlight
    #[test]
    fn backlight_brightness_change_produces_hardware_command() {
        let ddc_list: Vec<FfiDdcDisplayInfo> = vec![];
        let bl = MockBl { displays: vec![bl("amdgpu_bl0", 50, 255)] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let (_, cmds) = vm.on_brightness_change(1, 80.0);
        assert_eq!(cmds.len(), 1);
        assert_eq!(cmds[0], HardwareCommand::SetBacklight { name: "amdgpu_bl0".to_string(), raw: 204 });
    }

    // P5: Sync produces multiple hardware commands
    #[test]
    fn sync_produces_commands_for_all_displays() {
        let ddc_list = vec![ddc_n(1, "A", 50, 100), ddc_n(2, "B", 30, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        vm.sync_enabled = true;
        let (_, cmds) = vm.on_brightness_change(1, 70.0);
        assert_eq!(cmds.len(), 2);
        assert_eq!(cmds[0], HardwareCommand::SetDdc { index: 1, raw: 70 });
        assert_eq!(cmds[1], HardwareCommand::SetDdc { index: 2, raw: 50 });
    }

    // P6: Rename persists to settings
    #[test]
    fn rename_persists_to_settings() {
        let ddc_list = vec![ddc("Monitor", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let key = vm.rename(1, "Left Monitor".to_string()).unwrap();
        let mut settings = Settings::default();
        vm.apply_rename_to_settings(&key, "Left Monitor", &mut settings);
        assert_eq!(settings.get_custom_name("ddc:1"), Some("Left Monitor".to_string()));
    }

    // P7: Cap changes persist to settings
    #[test]
    fn caps_persist_to_settings() {
        let ddc_list = vec![ddc("Monitor", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let result = vm.set_caps(1, 10.0, 90.0).unwrap();
        let mut settings = Settings::default();
        vm.apply_caps_to_settings(&result.settings_key, 10.0, 90.0, &mut settings);
        let ds = settings.get_display("ddc:1");
        assert!((ds.min_brightness - 10.0).abs() < f64::EPSILON);
        assert!((ds.max_brightness - 90.0).abs() < f64::EPSILON);
    }

    // P7: set_caps updates view model
    #[test]
    fn set_caps_updates_view_model() {
        let ddc_list = vec![ddc("Monitor", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        vm.set_caps(1, 10.0, 90.0);
        assert!((vm.rows[0].min_pct - 10.0).abs() < f64::EPSILON);
        assert!((vm.rows[0].max_pct - 90.0).abs() < f64::EPSILON);
    }

    // P9: Setting caps re-clamps brightness and produces hardware command
    #[test]
    fn set_caps_re_clamps_brightness_above_max() {
        let ddc_list = vec![ddc("Monitor", 90, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let result = vm.set_caps(1, 0.0, 80.0).unwrap();
        // 90% clamped to 80%
        assert!((vm.rows[0].brightness_pct - 80.0).abs() < f64::EPSILON);
        assert_eq!(
            result.re_clamped,
            Some(HardwareCommand::SetDdc { index: 1, raw: 80 })
        );
    }

    #[test]
    fn set_caps_re_clamps_brightness_below_min() {
        let ddc_list = vec![ddc("Monitor", 5, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let result = vm.set_caps(1, 20.0, 100.0).unwrap();
        // 5% clamped to 20%
        assert!((vm.rows[0].brightness_pct - 20.0).abs() < f64::EPSILON);
        assert_eq!(
            result.re_clamped,
            Some(HardwareCommand::SetDdc { index: 1, raw: 20 })
        );
    }

    #[test]
    fn set_caps_no_re_clamp_when_in_range() {
        let ddc_list = vec![ddc("Monitor", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let result = vm.set_caps(1, 10.0, 90.0).unwrap();
        // 50% is within [10, 90] — no re-clamp
        assert!(result.re_clamped.is_none());
        assert!((vm.rows[0].brightness_pct - 50.0).abs() < f64::EPSILON);
    }

    // P8: Sync toggle persists
    #[test]
    fn sync_toggle_persists_via_settings() {
        let mut settings = Settings::default();
        assert!(!settings.proportional_sync);
        settings.proportional_sync = true;
        assert!(settings.proportional_sync);
    }

    // P4: Caps with sync
    #[test]
    fn caps_clamp_with_sync() {
        let ddc_list = vec![ddc("A", 50, 100), ddc("B", 90, 100)];
        let bl = MockBl { displays: vec![] };
        let mut s = Settings::default();
        s.set_display("ddc:1", DisplaySettings { min_brightness: 0.0, max_brightness: 80.0, custom_name: None });
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &s).unwrap();
        vm.sync_enabled = true;
        vm.on_brightness_change(1, 70.0);
        assert!((vm.rows[1].brightness_pct - 80.0).abs() < f64::EPSILON);
    }

    // Backend key for backlight rename
    #[test]
    fn backlight_rename_uses_correct_key() {
        let ddc_list: Vec<FfiDdcDisplayInfo> = vec![];
        let bl = MockBl { displays: vec![bl("amdgpu_bl0", 50, 255)] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let key = vm.rename(1, "Laptop Screen".to_string()).unwrap();
        assert_eq!(key, "backlight:amdgpu_bl0");
    }
}
