// SPDX-License-Identifier: GPL-3.0-or-later
//! UI performance logging for diagnosing slider lag.
//!
//! Opt-in, low-overhead NDJSON logger that tracks slider changes through
//! the entire pipeline: GTK signal → worker coalescing → DDC I²C write →
//! hardware readback verification.
//!
//! Usage: `temperlux-gui --perf-log=/tmp/perf.ndjson --perf-log-readback`

use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;

/// Global perf recorder. `None` ⇒ logging disabled (zero cost).
static RECORDER: OnceLock<Option<PerfRecorder>> = OnceLock::new();

/// Monotonic event sequence counter across all threads.
static EVENT_SEQ: AtomicU64 = AtomicU64::new(0);

/// Whether readback verification is enabled.
static READBACK: AtomicBool = AtomicBool::new(false);

/// Process-wide reference instant for relative timestamps.
static START: OnceLock<Instant> = OnceLock::new();

/// Initialize perf logging. Call once at startup.
/// If `path` is `None`, logging is disabled.
pub fn init(path: Option<PathBuf>, readback: bool) -> anyhow::Result<()> {
    READBACK.store(readback, Ordering::Relaxed);

    let Some(path) = path else {
        let _ = RECORDER.set(None);
        return Ok(());
    };

    let start = Instant::now();
    let _ = START.set(start);

    let file = std::fs::File::create(&path)
        .map_err(|e| anyhow::anyhow!("Failed to create perf log at {:?}: {}", path, e))?;
    let writer = BufWriter::new(file);

    let (tx, rx) = std::sync::mpsc::channel::<Event>();
    let writer = Arc::new(Mutex::new(writer));

    // Dedicated writer thread — never blocks hot paths
    let writer_clone = writer.clone();
    std::thread::Builder::new()
        .name("temperlux-perf".into())
        .spawn(move || {
            while let Ok(ev) = rx.recv() {
                let mut w = writer_clone.lock().unwrap();
                if let Ok(json) = serde_json::to_string(&ev) {
                    let _ = writeln!(w, "{}", json);
                    let _ = w.flush();
                }
            }
        })?;

    let recorder = PerfRecorder { tx };
    let _ = RECORDER.set(Some(recorder));

    // Write header
    if let Some(Some(rec)) = RECORDER.get() {
        let header = Event::Header {
            version: 1,
            pid: std::process::id(),
            start_unix_ms: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_millis() as u64,
        };
        let _ = rec.push(header);
    }

    Ok(())
}

/// Check if perf logging is enabled.
#[inline]
pub fn is_enabled() -> bool {
    RECORDER.get().map(|r| r.is_some()).unwrap_or(false)
}

/// Check if readback verification is enabled.
#[inline]
pub fn readback_enabled() -> bool {
    READBACK.load(Ordering::Relaxed)
}

/// Get current time in ms since perf init. Returns 0 if not initialized.
#[inline]
pub fn now_ms() -> u64 {
    START.get()
        .map(|s| s.elapsed().as_millis() as u64)
        .unwrap_or(0)
}

/// Allocate the next monotonic sequence number.
#[inline]
pub fn next_seq() -> u64 {
    EVENT_SEQ.fetch_add(1, Ordering::Relaxed)
}

/// Log a perf event. No-op when disabled.
#[inline]
pub fn log(ev: Event) {
    if let Some(Some(rec)) = RECORDER.get() {
        let _ = rec.push(ev);
    }
}

/// Get default perf log path under XDG cache.
pub fn default_path() -> PathBuf {
    let cache = dirs::cache_dir()
        .unwrap_or_else(|| PathBuf::from("/tmp"));
    let pid = std::process::id();
    let ts = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    cache.join("temperlux").join(format!("perf-{}-{}.ndjson", pid, ts))
}

struct PerfRecorder {
    tx: std::sync::mpsc::Sender<Event>,
}

impl PerfRecorder {
    fn push(&self, ev: Event) -> Result<(), ()> {
        self.tx.send(ev).map_err(|_| ())
    }
}

// ─── Event model ──────────────────────────────────────────────────────────────

#[derive(serde::Serialize, Clone, Debug)]
#[serde(tag = "kind")]
pub enum Event {
    /// Header record — always the first line.
    Header {
        version: u32,
        pid: u32,
        start_unix_ms: u64,
    },

    /// GTK Scale::value-changed signal fired (main thread).
    SliderChanged {
        seq: u64,
        t_ms: u64,
        display_key: String,
        gtk_value_pct: f64,
        raw_value: i32,
    },

    /// DisplayWorker drained the channel and is about to execute.
    WorkerDequeued {
        seq: u64,
        t_ms: u64,
        display_key: String,
        raw_value: i32,
        coalesced_count: u32,
    },

    /// DDC command dispatched to DdcService (from worker thread).
    DdcDispatched {
        seq: u64,
        t_ms: u64,
        parent_seq: u64,
        display_num: i32,
        raw_value: u16,
    },

    /// DdcService thread received the command.
    DdcServiceReceived {
        seq: u64,
        t_ms: u64,
        parent_seq: u64,
        display_num: i32,
        raw_value: u16,
        coalesced_count: u32,
    },

    /// I²C write started (before ddca_set_non_table_vcp_value).
    DdcWriteStart {
        seq: u64,
        t_ms: u64,
        parent_seq: u64,
        display_num: i32,
        raw: u16,
    },

    /// I²C write completed.
    DdcWriteEnd {
        seq: u64,
        t_ms: u64,
        parent_seq: u64,
        display_num: i32,
        raw: u16,
        duration_us: u64,
        success: bool,
    },

    /// Post-write readback verification.
    DdcReadback {
        seq: u64,
        t_ms: u64,
        parent_seq: u64,
        display_num: i32,
        expected_raw: u16,
        actual_raw: u16,
        duration_us: u64,
        matched: bool,
    },

    /// Backlight (sysfs) write boundaries.
    BacklightWriteStart {
        seq: u64,
        t_ms: u64,
        parent_seq: u64,
        name: String,
        raw: u32,
    },

    BacklightWriteEnd {
        seq: u64,
        t_ms: u64,
        parent_seq: u64,
        name: String,
        raw: u32,
        duration_us: u64,
        success: bool,
    },
}

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

    #[test]
    fn disabled_by_default() {
        assert!(!is_enabled());
    }

    #[test]
    fn seq_is_monotonic() {
        let a = next_seq();
        let b = next_seq();
        assert!(b > a, "seq should be monotonic: {} vs {}", a, b);
    }
}
