//! Unified TRACE category gate (0.7.39+).
//!
//! Before 0.7.39 each subsystem checked its own env var:
//! `SUPERMACHINE_BAKE_TRACE`, `SUPERMACHINE_VFS_TRACE`,
//! `SUPERMACHINE_BALLOON_TRACE`, … — fourteen separate knobs.
//! Discoverability was poor and the env-var surface area was
//! sprawling.
//!
//! The new shape is one variable, comma-separated categories:
//!
//! ```text
//! SUPERMACHINE_TRACE=bake,vfs           # those two categories
//! SUPERMACHINE_TRACE=all                # everything
//! SUPERMACHINE_TRACE=*                  # synonym for "all"
//! SUPERMACHINE_TRACE=1                  # legacy: also "all"
//! ```
//!
//! Legacy `SUPERMACHINE_<CAT>_TRACE` (and the
//! `SUPERMACHINE_TIMINGS`/`SUPERMACHINE_REMAP_TIMINGS` variants)
//! still work for one release as a deprecation runway.
//!
//! Categories (lowercase):
//!   * `bake`       — bake pipeline stages
//!   * `vfs`        — drop_vfs_caches RPC + per-restore VFS hooks
//!   * `balloon`    — virtio-balloon inflate / deflate
//!   * `fuse`       — per-FUSE-op log (virtio-fs daemon)
//!   * `tls`        — router TLS handshake
//!   * `vq`         — virtio queue events
//!   * `vsock`      — virtio-vsock muxer
//!   * `router`     — router worker-selection
//!   * `remap`      — KVM memory remap timings during restore
//!   * `rosetta`    — amd64-on-arm64 Rosetta ioctl events
//!   * `timeout`    — exec timeout firing + kill latency
//!   * `timings`    — spawn_one / acquire / coord timings
//!   * `run`        — `supermachine run` CLI lifecycle
//!
//! Cost: one OnceLock-cached HashSet lookup per call. Sub-µs.
//! Safe to call from hot paths gated behind `if trace::enabled("x")`.

use std::collections::HashSet;
use std::sync::OnceLock;

/// Parse and cache the active category set. Computed once per
/// process from `SUPERMACHINE_TRACE` + legacy `SUPERMACHINE_*_TRACE`
/// (+ the two `_TIMINGS` variants that pre-date the convention).
fn active_categories() -> &'static HashSet<String> {
    static CATEGORIES: OnceLock<HashSet<String>> = OnceLock::new();
    CATEGORIES.get_or_init(|| {
        let mut set = HashSet::new();

        // Unified var.
        if let Ok(v) = std::env::var("SUPERMACHINE_TRACE") {
            let v = v.trim();
            if v == "1" || v == "all" || v == "*" || v.eq_ignore_ascii_case("true") {
                set.insert("all".to_owned());
            } else {
                for tok in v.split(',') {
                    let t = tok.trim().to_lowercase();
                    if !t.is_empty() {
                        set.insert(t);
                    }
                }
            }
        }

        // Legacy individual vars. Listed exhaustively so adding a
        // new category is a single-place edit.
        const LEGACY: &[(&str, &str)] = &[
            ("SUPERMACHINE_BAKE_TRACE", "bake"),
            ("SUPERMACHINE_VFS_TRACE", "vfs"),
            ("SUPERMACHINE_BALLOON_TRACE", "balloon"),
            ("SUPERMACHINE_FUSE_TRACE", "fuse"),
            ("SUPERMACHINE_TLS_TRACE", "tls"),
            ("SUPERMACHINE_VQ_TRACE", "vq"),
            ("SUPERMACHINE_VSOCK_TRACE", "vsock"),
            ("SUPERMACHINE_ROUTER_TRACE", "router"),
            ("SUPERMACHINE_REMAP_TIMINGS", "remap"),
            ("SUPERMACHINE_ROSETTA_TRACE", "rosetta"),
            ("SUPERMACHINE_TIMEOUT_TRACE", "timeout"),
            ("SUPERMACHINE_TIMINGS", "timings"),
            ("SUPERMACHINE_RUN_TRACE", "run"),
        ];
        for (env, cat) in LEGACY {
            if std::env::var_os(env).is_some() {
                set.insert((*cat).to_owned());
            }
        }

        set
    })
}

/// True iff the category (or "all") is currently enabled. Use at
/// every site that previously checked `std::env::var_os(...)`.
///
/// Categories are lowercase; the call site picks one from the
/// table in the module doc. Unknown categories return false
/// (typo'd category name → silent; consistent with the previous
/// per-var behavior).
pub fn enabled(category: &str) -> bool {
    let set = active_categories();
    if set.contains("all") {
        return true;
    }
    set.contains(category)
}

/// FUSE trace's existing shape is `SUPERMACHINE_FUSE_TRACE=PATH`
/// where the value is a target file path (not just a boolean).
/// This helper preserves that — returns the path when set OR
/// returns the unified-var equivalent when `fuse` is in the
/// category set (falling back to a sentinel path the caller
/// resolves).
///
/// Used only by `crates/supermachine/src/fuse/*` — most code
/// should use the plain `enabled()`.
pub fn fuse_target() -> Option<std::ffi::OsString> {
    // Legacy form takes priority when explicitly set with a path.
    if let Some(v) = std::env::var_os("SUPERMACHINE_FUSE_TRACE") {
        return Some(v);
    }
    if enabled("fuse") {
        // Default target when only the unified var was set.
        // Embedders who care about the path should still use the
        // legacy var.
        Some(std::ffi::OsString::from("/tmp/supermachine-fuse-trace.log"))
    } else {
        None
    }
}

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

    // The OnceLock means tests must serialize their env mutations.
    static LOCK: Mutex<()> = Mutex::new(());

    /// Reset the OnceLock by recomputing via a fresh process —
    /// not actually possible inside #[test]; the tests below
    /// instead exercise the parse logic via a separate function
    /// that takes the env values as input.
    fn parse(unified: Option<&str>, legacy: &[(&str, &str)]) -> HashSet<String> {
        let mut set = HashSet::new();
        if let Some(v) = unified {
            let v = v.trim();
            if v == "1" || v == "all" || v == "*" || v.eq_ignore_ascii_case("true") {
                set.insert("all".to_owned());
            } else {
                for tok in v.split(',') {
                    let t = tok.trim().to_lowercase();
                    if !t.is_empty() {
                        set.insert(t);
                    }
                }
            }
        }
        for (env_set, cat) in legacy {
            // `env_set` is non-empty iff the legacy var is set in
            // the test scenario.
            if !env_set.is_empty() {
                set.insert((*cat).to_owned());
            }
        }
        set
    }

    #[test]
    fn unified_all_matches_every_category() {
        let _g = LOCK.lock().unwrap();
        let s = parse(Some("all"), &[]);
        assert!(s.contains("all"));
    }

    #[test]
    fn unified_star_is_synonym_for_all() {
        let _g = LOCK.lock().unwrap();
        let s = parse(Some("*"), &[]);
        assert!(s.contains("all"));
    }

    #[test]
    fn unified_one_truthy_int_is_synonym_for_all() {
        let _g = LOCK.lock().unwrap();
        let s = parse(Some("1"), &[]);
        assert!(s.contains("all"));
    }

    #[test]
    fn unified_categories_csv_parses() {
        let _g = LOCK.lock().unwrap();
        let s = parse(Some("bake,vfs, balloon"), &[]);
        assert!(s.contains("bake"));
        assert!(s.contains("vfs"));
        assert!(s.contains("balloon"));
        assert!(!s.contains("vsock"));
    }

    #[test]
    fn unified_uppercase_is_normalized() {
        let _g = LOCK.lock().unwrap();
        let s = parse(Some("BAKE,Vfs"), &[]);
        assert!(s.contains("bake"));
        assert!(s.contains("vfs"));
    }

    #[test]
    fn legacy_vars_set_their_category() {
        let _g = LOCK.lock().unwrap();
        let s = parse(None, &[("1", "bake"), ("1", "timings")]);
        assert!(s.contains("bake"));
        assert!(s.contains("timings"));
        assert!(!s.contains("vfs"));
    }

    #[test]
    fn unified_and_legacy_compose() {
        let _g = LOCK.lock().unwrap();
        let s = parse(Some("vfs"), &[("1", "bake")]);
        assert!(s.contains("vfs"));
        assert!(s.contains("bake"));
    }

    #[test]
    fn empty_inputs_yield_empty_set() {
        let _g = LOCK.lock().unwrap();
        let s = parse(None, &[]);
        assert!(s.is_empty());
    }

    #[test]
    fn whitespace_only_csv_is_ignored() {
        let _g = LOCK.lock().unwrap();
        let s = parse(Some("  ,  ,"), &[]);
        assert!(s.is_empty());
    }
}
