//! Lightweight meeting-process detection.
//!
//! Scope (per Jake's ideology note): single signal only — poll the process
//! list and match user-editable patterns. No mic-activity heuristic, no
//! calendar integration. If the user opts in, we surface a non-modal toast
//! so they can decide to start recording. We never start recording
//! ourselves from this signal.

use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};

/// Reusable wrapper around a `sysinfo::System` whose process table is
/// refreshed in place on every poll, instead of allocating a fresh one.
///
/// On a busy host (~300 processes), `System::new_with_specifics` followed by
/// `refresh_processes` walks `/proc` cold and costs ~50–100 ms; reusing the
/// same instance reuses sysinfo's per-process bookkeeping so subsequent
/// refreshes are dominated by diffing rather than allocation. The Tauri
/// host holds one of these behind a `Mutex` for the meeting-detection
/// command to call every 15 s.
pub struct ProcessLister {
    system: System,
}

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

impl ProcessLister {
    pub fn new() -> Self {
        Self {
            system: System::new_with_specifics(
                RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing()),
            ),
        }
    }

    /// Refresh the process table in place and return the current
    /// lowercased executable names.
    pub fn snapshot(&mut self) -> Vec<String> {
        self.system.refresh_processes(ProcessesToUpdate::All, true);
        self.system
            .processes()
            .values()
            .map(|process| process.name().to_string_lossy().to_lowercase())
            .collect()
    }
}

/// Snapshot the current process list's executable/command names. Lowercased
/// for case-insensitive pattern matching.
///
/// Convenience wrapper that allocates a fresh `ProcessLister` per call.
/// Hot paths (the meeting-detection poller) should hold a long-lived
/// `ProcessLister` and call `snapshot()` directly to avoid the per-call
/// allocation of `System`'s internal bookkeeping.
pub fn list_running_process_names() -> Vec<String> {
    ProcessLister::new().snapshot()
}

/// Match a snapshot of process names against case-insensitive substring
/// `patterns`. Returns the set of patterns that matched at least once, in
/// input order, deduped. Empty / whitespace-only patterns are skipped so
/// a stray blank entry in the user's list never matches everything.
pub fn match_meeting_patterns(process_names: &[String], patterns: &[String]) -> Vec<String> {
    let mut matches: Vec<String> = Vec::new();
    for raw_pattern in patterns {
        let needle = raw_pattern.trim().to_lowercase();
        if needle.is_empty() {
            continue;
        }
        if process_names.iter().any(|name| name.contains(&needle))
            && !matches.iter().any(|existing| existing == &needle)
        {
            matches.push(needle);
        }
    }
    matches
}

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

    #[test]
    fn matches_are_case_insensitive_substrings() {
        let processes = vec![
            "Zoom Meeting".to_lowercase(),
            "firefox".to_lowercase(),
            "Microsoft Teams".to_lowercase(),
        ];
        let patterns = vec!["ZOOM".into(), "teams".into(), "discord".into()];

        let got = match_meeting_patterns(&processes, &patterns);

        assert_eq!(got, vec!["zoom", "teams"]);
    }

    #[test]
    fn empty_and_whitespace_patterns_are_ignored() {
        let processes = vec!["anything".to_lowercase()];
        let patterns = vec!["".into(), "   ".into()];

        assert!(match_meeting_patterns(&processes, &patterns).is_empty());
    }

    #[test]
    fn matches_are_deduped() {
        let processes = vec!["zoomclient".into(), "zoomhelper".into()];
        let patterns = vec!["zoom".into(), "zoom".into()];

        assert_eq!(match_meeting_patterns(&processes, &patterns), vec!["zoom"]);
    }

    #[test]
    fn list_running_returns_something_on_this_host() {
        // Smoke check — this is the test host and always has running procs.
        let names = list_running_process_names();
        assert!(!names.is_empty(), "expected at least one running process");
    }
}
