//! Run the interceptor at login.
//!
//! **Windows**: one canonical entry under `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
//! (via `winreg`). Enabling also purges the artifacts the *old* build left — it used the
//! `auto-launch` crate, whose default "Dynamic" mode writes to `HKLM\...\Run` (machine-wide,
//! when it can elevate) plus a `StartupApproved\Run` enable-toggle. Without that cleanup an
//! upgraded user gets the HKLM entry *and* our HKCU entry firing at login — two daemons racing
//! for the port. So we collapse everything to the single HKCU entry.
//!
//! **macOS / Linux**: the `auto-launch` crate (launchd agent / XDG `.desktop` entry).

use anyhow::{Context, Result};

/// Enable (or disable) running `llmtrim serve --port <port>` at login. Silent on
/// success — callers (the `autostart` command, `setup`, `uninstall`) own the
/// messaging so each flow keeps its own voice.
pub fn configure(enable: bool, port: u16) -> Result<()> {
    #[cfg(windows)]
    {
        configure_windows(enable, port)
    }
    #[cfg(not(windows))]
    {
        configure_auto_launch(enable, port)
    }
}

/// Hide the process's own console window. Used by the Windows autostart Run-key entry: Explorer
/// launches our console binary at login and allocates a console window that would otherwise stay
/// visible for the daemon's whole life. We hide it (`SW_HIDE`) right at startup so login starts
/// the interceptor windowless. No-op on non-Windows, where login uses a detached launchd agent /
/// `.desktop` entry that never gets a terminal in the first place.
pub fn hide_console_window() {
    #[cfg(windows)]
    {
        // Hiding the window needs `GetConsoleWindow` + `ShowWindow`, both `unsafe` FFI this crate
        // forbids (`unsafe_code = "forbid"`), so shell out to PowerShell with a one-shot P/Invoke
        // — same pattern as `setup::broadcast_env_change`. SW_HIDE = 0. `GetConsoleWindow` returns
        // 0 when no console is attached, in which case `ShowWindow(0, ..)` is a harmless no-op.
        // Best-effort: a failure just leaves the window visible, never blocks the daemon.
        const PS: &str = "\
            $sig = '[DllImport(\"kernel32.dll\")] public static extern IntPtr GetConsoleWindow();\
            [DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);';\
            $t = Add-Type -MemberDefinition $sig -Name NativeMethods -Namespace Win32 -PassThru;\
            $h = $t::GetConsoleWindow();\
            if ($h -ne [IntPtr]::Zero) { [void]$t::ShowWindow($h, 0) }";
        let _ = std::process::Command::new("powershell")
            .args(["-NoProfile", "-NonInteractive", "-Command", PS])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status();
    }
}

/// Is run-at-login currently enabled? Read-only probe for `status`/`doctor` — checks the
/// same canonical location `configure` writes (HKCU Run key / XDG desktop entry / launchd
/// agent). `false` on any read failure: a probe must never error a status report.
pub fn is_enabled() -> bool {
    #[cfg(windows)]
    {
        use winreg::RegKey;
        use winreg::enums::{HKEY_CURRENT_USER, KEY_READ};
        RegKey::predef(HKEY_CURRENT_USER)
            .open_subkey_with_flags(RUN_KEY, KEY_READ)
            .and_then(|key| key.get_value::<String, _>(VALUE_NAME))
            .is_ok()
    }
    #[cfg(target_os = "linux")]
    {
        std::env::var("HOME")
            .map(|home| is_enabled_in(std::path::Path::new(&home)))
            .unwrap_or(false)
    }
    #[cfg(all(not(windows), not(target_os = "linux")))]
    {
        let Ok(exe) = std::env::current_exe() else {
            return false;
        };
        let path = exe.to_string_lossy();
        auto_launch::AutoLaunchBuilder::new()
            .set_app_name("llmtrim")
            .set_app_path(path.as_ref())
            .build()
            .and_then(|auto| auto.is_enabled())
            .unwrap_or(false)
    }
}

/// Inner seam for [`is_enabled`] on Linux: probe under `base` as the home directory.
#[cfg(target_os = "linux")]
fn is_enabled_in(base: &std::path::Path) -> bool {
    xdg_autostart_dir(base).join("llmtrim.desktop").exists()
}

// ── Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run ─────────────────

#[cfg(windows)]
const RUN_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
#[cfg(windows)]
const STARTUP_APPROVED_KEY: &str =
    r"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run";
#[cfg(windows)]
const VALUE_NAME: &str = "llmtrim";

#[cfg(windows)]
fn configure_windows(enable: bool, port: u16) -> Result<()> {
    use winreg::RegKey;
    use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};

    if enable {
        let exe = std::env::current_exe().context("could not find the llmtrim executable")?;
        let (key, _) = RegKey::predef(HKEY_CURRENT_USER)
            .create_subkey_with_flags(RUN_KEY, KEY_READ | KEY_WRITE)
            .with_context(|| format!("failed to open HKCU\\{RUN_KEY}"))?;
        // `--hide-console`: Explorer launches this console exe at login, allocating a visible
        // console window that would otherwise stay open for the daemon's whole life. The flag
        // makes the process hide its own console at startup (see `hide_console_window`).
        let cmd = format!(
            "\"{}\" serve --port {} --supervised --hide-console",
            exe.display(),
            port
        );
        key.set_value(VALUE_NAME, &cmd)
            .context("failed to set llmtrim autostart in the registry Run key")?;
        // Collapse to this single entry: remove any legacy auto-launch leftovers (the HKLM
        // Run entry it wrote under elevation, and the StartupApproved toggles in either hive)
        // so login starts exactly one daemon, not two.
        purge_legacy_autostart();
    } else {
        // Disable: clear our entry *and* every legacy location, so uninstall leaves nothing
        // that revives the daemon at next login.
        remove_autostart_everywhere();
    }
    Ok(())
}

/// Delete the `llmtrim` value under `subkey` in `hive`, best-effort. Opening for write can fail
/// without admin (HKLM) — that's fine, we just couldn't find/clean it. A missing value is fine
/// too (idempotent). Never errors: cleanup must never block enable/disable.
#[cfg(windows)]
fn best_effort_delete(hive: winreg::HKEY, subkey: &str) {
    use winreg::RegKey;
    use winreg::enums::KEY_WRITE;
    if let Ok(key) = RegKey::predef(hive).open_subkey_with_flags(subkey, KEY_WRITE) {
        let _ = key.delete_value(VALUE_NAME);
    }
}

/// Remove every autostart artifact *except* our canonical HKCU Run entry — i.e. the old
/// `auto-launch` build's HKLM Run entry and its StartupApproved toggles in both hives.
#[cfg(windows)]
fn purge_legacy_autostart() {
    use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
    best_effort_delete(HKEY_LOCAL_MACHINE, RUN_KEY);
    best_effort_delete(HKEY_CURRENT_USER, STARTUP_APPROVED_KEY);
    best_effort_delete(HKEY_LOCAL_MACHINE, STARTUP_APPROVED_KEY);
}

/// Remove the autostart entry from every location we (or the old build) could have written:
/// the Run key and the StartupApproved toggle, in both HKCU and HKLM.
#[cfg(windows)]
fn remove_autostart_everywhere() {
    use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
    best_effort_delete(HKEY_CURRENT_USER, RUN_KEY);
    best_effort_delete(HKEY_LOCAL_MACHINE, RUN_KEY);
    best_effort_delete(HKEY_CURRENT_USER, STARTUP_APPROVED_KEY);
    best_effort_delete(HKEY_LOCAL_MACHINE, STARTUP_APPROVED_KEY);
}

// ── macOS / Linux: auto-launch crate ────────────────────────────────────────────

/// The XDG autostart directory relative to a home base (Linux only).
#[cfg(target_os = "linux")]
fn xdg_autostart_dir(base: &std::path::Path) -> std::path::PathBuf {
    base.join(".config").join("autostart")
}

/// The content of the `.desktop` entry for a given `exe` path and `port`.
#[cfg(target_os = "linux")]
fn desktop_entry(exe: &std::path::Path, port: u16) -> String {
    format!(
        "[Desktop Entry]\n\
         Type=Application\n\
         Version=1.0\n\
         Name=llmtrim\n\
         Comment=llmtrim startup script\n\
         Exec={} serve --port {} --supervised\n\
         StartupNotify=false\n\
         Terminal=false",
        exe.display(),
        port
    )
}

/// Inner seam: enable or disable the XDG autostart entry using `base` as the home directory
/// instead of the real `$HOME`. Tests pass a temp dir; production passes the actual home.
/// On Linux only — macOS goes through `configure_auto_launch` directly without a base seam.
#[cfg(target_os = "linux")]
fn configure_in(enable: bool, port: u16, base: &std::path::Path) -> Result<()> {
    let dir = xdg_autostart_dir(base);
    let file = dir.join("llmtrim.desktop");

    if enable {
        let exe = std::env::current_exe().context("could not find the llmtrim executable")?;
        std::fs::create_dir_all(&dir)
            .with_context(|| format!("failed to create {}", dir.display()))?;
        std::fs::write(&file, desktop_entry(&exe, port))
            .with_context(|| format!("failed to write {}", file.display()))?;
    } else if file.exists() {
        std::fs::remove_file(&file)
            .with_context(|| format!("failed to remove {}", file.display()))?;
        // Already absent → no-op (idempotent disable).
    }
    Ok(())
}

#[cfg(target_os = "linux")]
fn configure_auto_launch(enable: bool, port: u16) -> Result<()> {
    let home = std::env::var("HOME")
        .context("HOME is not set — cannot determine XDG autostart directory")?;
    configure_in(enable, port, std::path::Path::new(&home))
}

#[cfg(all(not(windows), not(target_os = "linux")))]
fn configure_auto_launch(enable: bool, port: u16) -> Result<()> {
    use auto_launch::AutoLaunchBuilder;

    let exe = std::env::current_exe().context("could not find the llmtrim executable")?;
    let path = exe.to_string_lossy();
    let port_arg = port.to_string();

    let auto = AutoLaunchBuilder::new()
        .set_app_name("llmtrim")
        .set_app_path(path.as_ref())
        .set_args(&["serve", "--port", port_arg.as_str(), "--supervised"])
        .build()
        .map_err(|e| anyhow::anyhow!("failed to configure autostart: {e}"))?;

    if enable {
        auto.enable()
            .map_err(|e| anyhow::anyhow!("failed to enable autostart: {e}"))?;
    } else {
        auto.disable()
            .map_err(|e| anyhow::anyhow!("failed to disable autostart: {e}"))?;
    }
    Ok(())
}

// ── Tray autostart ───────────────────────────────────────────────────────────
//
// A *second*, independent login entry that launches the desktop tray GUI
// (`llmtrim-tray`), separate from the daemon entry above so a user can run one
// without the other. Same per-OS backends (HKCU Run value / XDG `.desktop` /
// launchd agent), keyed by a distinct name so the two never collide.

/// Enable (or disable) launching the tray app at login. Errors if enabling when
/// the tray binary isn't installed (a plain `cargo install` on a headless box).
pub fn configure_tray(enable: bool) -> Result<()> {
    let exe = crate::tray::tray_binary();
    if enable && exe.is_none() {
        anyhow::bail!(
            "the llmtrim tray app isn't installed — install the desktop bundle before \
             enabling tray autostart"
        );
    }
    #[cfg(windows)]
    {
        configure_tray_windows(enable, exe.as_deref())
    }
    #[cfg(target_os = "linux")]
    {
        let home = std::env::var("HOME")
            .context("HOME is not set — cannot determine XDG autostart directory")?;
        configure_tray_in(enable, exe.as_deref(), std::path::Path::new(&home))
    }
    #[cfg(all(not(windows), not(target_os = "linux")))]
    {
        configure_tray_auto_launch(enable, exe.as_deref())
    }
}

/// Is launch-tray-at-login currently enabled? Read-only probe, mirrors
/// [`is_enabled`] but for the tray entry. `false` on any read failure.
pub fn is_tray_enabled() -> bool {
    #[cfg(windows)]
    {
        use winreg::RegKey;
        use winreg::enums::{HKEY_CURRENT_USER, KEY_READ};
        RegKey::predef(HKEY_CURRENT_USER)
            .open_subkey_with_flags(RUN_KEY, KEY_READ)
            .and_then(|key| key.get_value::<String, _>(VALUE_NAME_TRAY))
            .is_ok()
    }
    #[cfg(target_os = "linux")]
    {
        std::env::var("HOME")
            .map(|home| is_tray_enabled_in(std::path::Path::new(&home)))
            .unwrap_or(false)
    }
    #[cfg(all(not(windows), not(target_os = "linux")))]
    {
        let Some(exe) = crate::tray::tray_binary() else {
            return false;
        };
        let path = exe.to_string_lossy();
        auto_launch::AutoLaunchBuilder::new()
            .set_app_name("llmtrim-tray")
            .set_app_path(path.as_ref())
            .build()
            .and_then(|auto| auto.is_enabled())
            .unwrap_or(false)
    }
}

#[cfg(windows)]
const VALUE_NAME_TRAY: &str = "llmtrim-tray";

#[cfg(windows)]
fn configure_tray_windows(enable: bool, exe: Option<&std::path::Path>) -> Result<()> {
    use winreg::RegKey;
    use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};

    let (key, _) = RegKey::predef(HKEY_CURRENT_USER)
        .create_subkey_with_flags(RUN_KEY, KEY_READ | KEY_WRITE)
        .with_context(|| format!("failed to open HKCU\\{RUN_KEY}"))?;
    if enable {
        // The tray is a windowless GUI exe (no console to hide), so just its path.
        let exe = exe.context("the llmtrim tray app isn't installed")?;
        let cmd = format!("\"{}\"", exe.display());
        key.set_value(VALUE_NAME_TRAY, &cmd)
            .context("failed to set llmtrim-tray autostart in the registry Run key")?;
    } else {
        // A missing value is fine (idempotent disable).
        let _ = key.delete_value(VALUE_NAME_TRAY);
    }
    Ok(())
}

#[cfg(target_os = "linux")]
fn tray_desktop_entry(exe: &std::path::Path) -> String {
    // XDG Desktop Entry spec: a literal `%` in Exec must be doubled (`%%`), and a
    // path with spaces must be quoted or it parses as program + arguments. Escape
    // `%` first, then wrap the whole path in double quotes.
    let path = exe.to_string_lossy().replace('%', "%%");
    format!(
        "[Desktop Entry]\n\
         Type=Application\n\
         Version=1.0\n\
         Name=llmtrim tray\n\
         Comment=llmtrim compression savings tray\n\
         Exec=\"{path}\"\n\
         StartupNotify=false\n\
         Terminal=false"
    )
}

/// Inner seam for the Linux tray entry: write/remove the `.desktop` file under
/// `base`. Tested with a temp dir and an explicit `exe` path.
#[cfg(target_os = "linux")]
fn configure_tray_in(
    enable: bool,
    exe: Option<&std::path::Path>,
    base: &std::path::Path,
) -> Result<()> {
    let dir = xdg_autostart_dir(base);
    let file = dir.join("llmtrim-tray.desktop");

    if enable {
        let exe = exe.context("the llmtrim tray app isn't installed")?;
        std::fs::create_dir_all(&dir)
            .with_context(|| format!("failed to create {}", dir.display()))?;
        std::fs::write(&file, tray_desktop_entry(exe))
            .with_context(|| format!("failed to write {}", file.display()))?;
    } else if file.exists() {
        std::fs::remove_file(&file)
            .with_context(|| format!("failed to remove {}", file.display()))?;
    }
    Ok(())
}

#[cfg(target_os = "linux")]
fn is_tray_enabled_in(base: &std::path::Path) -> bool {
    xdg_autostart_dir(base)
        .join("llmtrim-tray.desktop")
        .exists()
}

#[cfg(all(not(windows), not(target_os = "linux")))]
fn configure_tray_auto_launch(enable: bool, exe: Option<&std::path::Path>) -> Result<()> {
    use auto_launch::AutoLaunchBuilder;

    // auto-launch keys the launchd label off the app name; the path only matters
    // when enabling. On disable the tray may not be installed (no `exe`), so we
    // pass an empty path to build the handle, then probe `is_enabled` before
    // calling `disable` — that avoids relying on the library accepting a synthetic
    // path, and keeps disable an idempotent no-op when nothing was registered.
    let path = exe
        .map(|p| p.to_string_lossy().into_owned())
        .unwrap_or_default();
    let auto = AutoLaunchBuilder::new()
        .set_app_name("llmtrim-tray")
        .set_app_path(&path)
        .build()
        .map_err(|e| anyhow::anyhow!("failed to configure tray autostart: {e}"))?;

    if enable {
        auto.enable()
            .map_err(|e| anyhow::anyhow!("failed to enable tray autostart: {e}"))?;
    } else if auto.is_enabled().unwrap_or(false) {
        auto.disable()
            .map_err(|e| anyhow::anyhow!("failed to disable tray autostart: {e}"))?;
    }
    Ok(())
}

#[cfg(all(test, target_os = "linux"))]
mod tests {
    use super::*;
    use std::path::PathBuf;

    // Minimal RAII temp-dir guard — same approach as setup.rs tests.
    struct TempDir(PathBuf);

    impl TempDir {
        fn new(suffix: &str) -> Self {
            let dir = std::env::temp_dir().join(format!(
                "llmtrim-autostart-test-{}-{}",
                std::process::id(),
                suffix
            ));
            std::fs::create_dir_all(&dir).expect("create temp dir");
            Self(dir)
        }
        fn path(&self) -> &std::path::Path {
            &self.0
        }
    }

    impl Drop for TempDir {
        fn drop(&mut self) {
            let _ = std::fs::remove_dir_all(&self.0);
        }
    }

    // These tests exercise configure_in directly, which is only compiled on Linux.
    // On Windows the cfg(target_os = "linux") gate keeps them out; the Windows
    // registry arm has its own scratch-key test in setup.rs.
    #[cfg(target_os = "linux")]
    #[test]
    fn configure_in_install_creates_desktop_file_with_expected_content() {
        let dir = TempDir::new("install");
        let base = dir.path();

        configure_in(true, 8787, base).expect("configure_in enable");

        let file = base
            .join(".config")
            .join("autostart")
            .join("llmtrim.desktop");
        assert!(file.exists(), ".desktop file not created");

        let content = std::fs::read_to_string(&file).expect("read .desktop");
        assert!(
            content.contains("[Desktop Entry]"),
            "missing [Desktop Entry]"
        );
        assert!(content.contains("Name=llmtrim"), "missing Name=");
        assert!(
            content.contains("serve --port 8787 --supervised"),
            "missing serve invocation with port"
        );
        assert!(content.contains("Terminal=false"), "missing Terminal=false");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn configure_in_install_twice_is_idempotent() {
        let dir = TempDir::new("idempotent");
        let base = dir.path();

        configure_in(true, 8787, base).expect("first enable");
        configure_in(true, 8787, base).expect("second enable");

        // Exactly one .desktop file should exist (no duplicate, no error).
        let autostart_dir = base.join(".config").join("autostart");
        let count = std::fs::read_dir(&autostart_dir)
            .expect("read autostart dir")
            .filter_map(|e| e.ok())
            .count();
        assert_eq!(count, 1, "expected exactly 1 .desktop file, found {count}");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn configure_in_remove_deletes_desktop_file() {
        let dir = TempDir::new("remove");
        let base = dir.path();

        configure_in(true, 8787, base).expect("enable");
        let file = base
            .join(".config")
            .join("autostart")
            .join("llmtrim.desktop");
        assert!(file.exists(), "file should exist before disable");

        configure_in(false, 8787, base).expect("disable");
        assert!(!file.exists(), ".desktop file still present after disable");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn is_enabled_in_tracks_configure_in() {
        let dir = TempDir::new("probe");
        let base = dir.path();

        assert!(!is_enabled_in(base), "fresh home → not enabled");
        configure_in(true, 8787, base).expect("enable");
        assert!(is_enabled_in(base), "enabled after configure");
        configure_in(false, 8787, base).expect("disable");
        assert!(!is_enabled_in(base), "disabled after remove");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn configure_tray_in_writes_and_removes_distinct_desktop_file() {
        let dir = TempDir::new("tray");
        let base = dir.path();
        let exe = PathBuf::from("/usr/local/bin/llmtrim-tray");

        configure_tray_in(true, Some(&exe), base).expect("enable tray");
        let file = base
            .join(".config")
            .join("autostart")
            .join("llmtrim-tray.desktop");
        assert!(file.exists(), "tray .desktop not created");

        let content = std::fs::read_to_string(&file).expect("read tray .desktop");
        assert!(content.contains("Name=llmtrim tray"), "missing tray Name=");
        assert!(
            content.contains("Exec=\"/usr/local/bin/llmtrim-tray\""),
            "missing tray Exec="
        );
        // The daemon entry must be untouched by the tray entry.
        assert!(
            !base
                .join(".config")
                .join("autostart")
                .join("llmtrim.desktop")
                .exists(),
            "tray enable should not create the daemon entry"
        );

        configure_tray_in(false, None, base).expect("disable tray");
        assert!(!file.exists(), "tray .desktop still present after disable");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn is_tray_enabled_in_tracks_configure_tray_in() {
        let dir = TempDir::new("tray-probe");
        let base = dir.path();
        let exe = PathBuf::from("/usr/local/bin/llmtrim-tray");

        assert!(!is_tray_enabled_in(base), "fresh home → tray not enabled");
        configure_tray_in(true, Some(&exe), base).expect("enable tray");
        assert!(is_tray_enabled_in(base), "tray enabled after configure");
        configure_tray_in(false, None, base).expect("disable tray");
        assert!(!is_tray_enabled_in(base), "tray disabled after remove");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn configure_in_remove_when_absent_is_ok_noop() {
        let dir = TempDir::new("remove-absent");
        let base = dir.path();

        // Disable without ever enabling — must return Ok without error.
        configure_in(false, 8787, base).expect("disable when absent must be Ok");

        // The autostart dir itself may or may not exist; either way, no .desktop file.
        let file = base
            .join(".config")
            .join("autostart")
            .join("llmtrim.desktop");
        assert!(!file.exists(), ".desktop file should not exist");
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn configure_tray_in_remove_when_absent_is_ok() {
        let dir = TempDir::new("tray-remove-absent");
        let base = dir.path();

        // `uninstall` disables the tray even on a machine that never had it.
        // Disabling without ever enabling must be an Ok no-op.
        configure_tray_in(false, None, base).expect("disable tray when absent must be Ok");
        assert!(
            !is_tray_enabled_in(base),
            "no tray .desktop after no-op disable"
        );
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn tray_desktop_entry_quotes_and_escapes_exec_path() {
        // Paths with spaces or `%` must survive the XDG Exec= parser.
        let entry = tray_desktop_entry(std::path::Path::new("/opt/my apps/llmtrim-tray %v"));
        assert!(
            entry.contains("Exec=\"/opt/my apps/llmtrim-tray %%v\""),
            "Exec line not quoted/escaped: {entry}"
        );
    }
}
