// SPDX-License-Identifier: GPL-3.0-or-later
// Display brightness worker — coalesces rapid slider changes into hardware commands.
//
// BDD Specs:
//
// S1: Rapid slider changes coalesce to latest value
//   Given a worker processing brightness commands
//   When values 10, 20, 30, 40, 50 arrive before the first executes
//   Then only value 50 is sent to hardware
//
// S2: Hardware commands execute in background
//   Given a worker receiving values
//   When a value is submitted
//   Then the GUI thread is never blocked
//
// S3: Per-display workers are independent
//   Given two displays with separate workers
//   When display 1 gets value 50 and display 2 gets value 70
//   Then both values execute concurrently
//
// S4: Label updates immediately (no queue)
//   Given a slider at 50%
//   When the value changes to 75%
//   Then the percentage label shows "75%" immediately

use crate::gui::viewmodel::HardwareCommand;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

/// Tracks pending commands per display (for testing).
#[derive(Debug, Clone)]
pub struct DebouncedState {
    pending: Arc<Mutex<HashMap<String, HardwareCommand>>>,
    #[cfg(test)]
    flushed: Arc<Mutex<Vec<HardwareCommand>>>,
}

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

impl DebouncedState {
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {
            pending: Arc::new(Mutex::new(HashMap::new())),
            #[cfg(test)]
            flushed: Arc::new(Mutex::new(Vec::new())),
        }
    }

    pub fn record(&self, key: &str, cmd: HardwareCommand) {
        let mut pending = self.pending.lock().unwrap();
        pending.insert(key.to_string(), cmd);
    }

    pub fn flush(&self, key: &str) -> Option<HardwareCommand> {
        let mut pending = self.pending.lock().unwrap();
        let cmd = pending.remove(key);
        #[cfg(test)]
        if let Some(ref c) = cmd {
            self.flushed.lock().unwrap().push(c.clone());
        }
        cmd
    }

    pub fn flush_all(&self) -> Vec<HardwareCommand> {
        let mut pending = self.pending.lock().unwrap();
        let cmds: Vec<HardwareCommand> = pending.drain().map(|(_, v)| v).collect();
        #[cfg(test)]
        {
            self.flushed.lock().unwrap().extend(cmds.iter().cloned());
        }
        cmds
    }

    #[cfg(test)]
    pub fn pending_count(&self) -> usize {
        self.pending.lock().unwrap().len()
    }
}

/// A background worker that coalesces slider changes into hardware commands.
/// Each display gets its own worker thread that always executes the latest value.
#[cfg(feature = "gui")]
pub struct DisplayWorker {
    tx: std::sync::mpsc::Sender<(i32, u64)>,
    /// Last hardware execution latency in milliseconds (read from GUI thread).
    pub last_latency_ms: Arc<Mutex<u64>>,
    /// Display key for perf logging.
    display_key: String,
}

#[cfg(feature = "gui")]
impl Clone for DisplayWorker {
    fn clone(&self) -> Self {
        Self {
            tx: self.tx.clone(),
            last_latency_ms: self.last_latency_ms.clone(),
            display_key: self.display_key.clone(),
        }
    }
}

#[cfg(feature = "gui")]
impl DisplayWorker {
    /// Spawn a background worker for a display.
    pub fn spawn<F>(display_key: String, executor: F) -> Self
    where
        F: Fn(i32) + Send + 'static,
    {
        let (tx, rx) = std::sync::mpsc::channel::<(i32, u64)>();
        let latency = Arc::new(Mutex::new(0u64));
        let latency_clone = latency.clone();
        let key_for_perf = display_key.clone();

        std::thread::spawn(move || {
            while let Ok((mut latest, _parent_seq)) = rx.recv() {
                // Debounce: wait 150ms for slider to settle, then drain all pending
                std::thread::sleep(std::time::Duration::from_millis(150));
                let mut coalesced_count: u32 = 0;
                while let Ok((val, _seq)) = rx.try_recv() {
                    latest = val;
                    coalesced_count += 1;
                    // Keep draining — slider may still be moving
                    while let Ok((v, _s)) = rx.try_recv() {
                        latest = v;
                        coalesced_count += 1;
                    }
                    // One more short wait to catch trailing events
                    std::thread::sleep(std::time::Duration::from_millis(30));
                }

                // Emit perf event for dequeue
                if crate::perf::is_enabled() {
                    crate::perf::log(crate::perf::Event::WorkerDequeued {
                        seq: crate::perf::next_seq(),
                        t_ms: crate::perf::now_ms(),
                        display_key: key_for_perf.clone(),
                        raw_value: latest,
                        coalesced_count,
                    });
                }

                // Measure execution time
                let start = std::time::Instant::now();
                executor(latest);
                let elapsed_ms = start.elapsed().as_millis() as u64;
                *latency_clone.lock().unwrap() = elapsed_ms;
            }
        });

        Self { tx, last_latency_ms: latency, display_key }
    }

    /// Submit a new brightness value with causal seq. Returns immediately (S2 non-blocking).
    pub fn set(&self, value: i32, parent_seq: u64) {
        let _ = self.tx.send((value, parent_seq));
    }

    /// Get the last hardware execution latency in ms.
    pub fn latency_ms(&self) -> u64 {
        *self.last_latency_ms.lock().unwrap()
    }
}

/// Manages per-display workers. Each display gets its own background thread (S3).
#[cfg(feature = "gui")]
pub struct DisplayWorkerPool {
    workers: HashMap<String, DisplayWorker>,
}

#[cfg(feature = "gui")]
impl Default for DisplayWorkerPool {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(feature = "gui")]
impl DisplayWorkerPool {
    pub fn new() -> Self {
        Self {
            workers: HashMap::new(),
        }
    }

    /// Get or create a worker for a display key.
    pub fn get_or_create<F>(&mut self, key: &str, executor: F) -> &DisplayWorker
    where
        F: Fn(i32) + Send + 'static,
    {
        if !self.workers.contains_key(key) {
            self.workers.insert(key.to_string(), DisplayWorker::spawn(key.to_string(), executor));
        }
        self.workers.get(key).unwrap()
    }
}

/// Convert a BackendKey to a string for deduplication.
pub fn backend_key_string(key: &crate::gui::viewmodel::BackendKey) -> String {
    match key {
        crate::gui::viewmodel::BackendKey::Ddc(idx) => format!("ddc:{}", idx),
        crate::gui::viewmodel::BackendKey::Backlight(name) => format!("bl:{}", name),
    }
}

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

    fn ddc_cmd(index: usize, raw: i32) -> HardwareCommand {
        HardwareCommand::SetDdc { index, raw }
    }

    #[test]
    fn rapid_changes_single_command() {
        let state = DebouncedState::new();
        state.record("ddc:0", ddc_cmd(0, 10));
        state.record("ddc:0", ddc_cmd(0, 50));
        assert_eq!(state.pending_count(), 1);
        assert_eq!(state.flush("ddc:0"), Some(ddc_cmd(0, 50)));
    }

    #[test]
    fn per_display_batching() {
        let state = DebouncedState::new();
        state.record("ddc:0", ddc_cmd(0, 50));
        state.record("ddc:1", ddc_cmd(1, 70));
        assert_eq!(state.pending_count(), 2);
    }

    #[test]
    fn flush_all_returns_everything() {
        let state = DebouncedState::new();
        state.record("ddc:0", ddc_cmd(0, 50));
        state.record("ddc:1", ddc_cmd(1, 70));
        assert_eq!(state.flush_all().len(), 2);
        assert_eq!(state.pending_count(), 0);
    }

    #[test]
    fn backend_key_string_conversion() {
        use crate::gui::viewmodel::BackendKey;
        assert_eq!(backend_key_string(&BackendKey::Ddc(0)), "ddc:0");
        assert_eq!(backend_key_string(&BackendKey::Backlight("intel".to_string())), "bl:intel");
    }
}
