//! In-app update check. Compares the compiled-in version against the latest
//! published release and lets the user update or skip. Network-optional: any
//! failure (offline, timeout) is silent — only OpenRouter requires internet.

use std::time::Duration;

pub const PKG: &str = env!("CARGO_PKG_NAME");
pub const CURRENT: &str = env!("CARGO_PKG_VERSION");

/// Fetch the latest published version. Defaults to PyPI (the wheel we publish);
/// override the source with LUCID_UPDATE_URL (must return JSON with the version
/// at `info.version`, PyPI-style) for crates.io/GitHub mirrors.
pub async fn latest_version() -> Option<String> {
    let url = std::env::var("LUCID_UPDATE_URL")
        .unwrap_or_else(|_| format!("https://pypi.org/pypi/{PKG}/json"));
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(6))
        .user_agent(format!("{PKG}/{CURRENT}"))
        .build()
        .ok()?;
    let v: serde_json::Value = client.get(&url).send().await.ok()?.json().await.ok()?;
    // PyPI: info.version; crates.io: crate.max_stable_version; GitHub: tag_name.
    v["info"]["version"]
        .as_str()
        .or_else(|| v["crate"]["max_stable_version"].as_str())
        .or_else(|| v["tag_name"].as_str())
        .map(|s| s.trim_start_matches('v').to_string())
}

/// True when `latest` is a strictly newer semver than `current`.
pub fn is_newer(latest: &str, current: &str) -> bool {
    parse(latest) > parse(current)
}

fn parse(v: &str) -> (u64, u64, u64) {
    // Take the numeric core, ignore pre-release/build suffixes.
    let core = v.trim().trim_start_matches('v');
    let core = core.split(['-', '+']).next().unwrap_or(core);
    let mut it = core.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
    (it.next().unwrap_or(0), it.next().unwrap_or(0), it.next().unwrap_or(0))
}

/// The upgrade command: re-run the installer, which upgrades in place across every
/// install method (prebuilt binary, uv, pipx, pip, cargo). Brew is not hosted, so we
/// never emit `brew upgrade` — the curl installer self-heals regardless of how the
/// current binary was installed. Returns (manager, shell command).
pub fn update_command() -> (&'static str, String) {
    ("curl", "curl -fsSL https://lucidtrain.com/install.sh | sh".to_string())
}

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

    #[test]
    fn version_compare() {
        assert!(is_newer("0.2.0", "0.1.0"));
        assert!(is_newer("0.1.1", "0.1.0"));
        assert!(is_newer("1.0.0", "0.9.9"));
        assert!(!is_newer("0.1.0", "0.1.0"));
        assert!(!is_newer("0.1.0", "0.2.0"));
        // Pre-release / v-prefix tolerance.
        assert!(is_newer("v0.2.0-rc1", "0.1.0"));
        assert!(!is_newer("0.1.0", "0.1.0+build5"));
    }

    #[test]
    fn update_uses_curl_installer() {
        let (_, cmd) = update_command();
        assert!(cmd.contains("install.sh"), "update command should re-run the installer: {cmd}");
        assert!(!cmd.contains("brew"), "update command must not use brew: {cmd}");
    }
}
