//! Deterministic, recomputable machine identity (`machine_uid`).
//!
//! SPEC-004 / v2-stable-identity Task 1.
//!
//! `machine_uid()` returns a stable, opaque identifier for *this physical
//! machine*. Unlike `agent_id` (a random UUID persisted in the config file,
//! which mints a fresh value — and thus a duplicate server row — whenever the
//! config is lost), `machine_uid` is **derived from the hardware/OS** and is
//! **recomputable**: the same machine yields the same id on every call with no
//! persistence required.
//!
//! - **Windows:** SHA-256 hash of the OS machine GUID read from
//!   `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (a `REG_SZ`). The raw
//!   GUID is never returned — only the opaque `muid_<hex>` derived from it.
//! - **Non-Windows (and Windows registry failure):** a random UUID persisted in
//!   the agent's data directory, read back on subsequent runs so it is stable
//!   across calls and process restarts.
//!
//! This module deliberately does NOT change `agent_id`/`generate_agent_id`.
//! `machine_uid` is reported *alongside* `agent_id`; the server-side dedup that
//! consumes it is a separate task.

use std::sync::OnceLock;

/// Prefix marking the value as an opaque machine-uid (vs. a raw GUID/UUID).
const MUID_PREFIX: &str = "muid_";

/// Cached value — `machine_uid()` reads the registry / a file, so compute once
/// and reuse for the lifetime of the process.
static MACHINE_UID: OnceLock<String> = OnceLock::new();

/// Return a deterministic, recomputable opaque machine identifier.
///
/// The result is non-empty and prefixed with [`MUID_PREFIX`]. It is cached after
/// the first call. On Windows it is derived purely from the OS machine GUID (no
/// persistence). If the Windows registry read fails — or on any non-Windows
/// platform — it degrades to a persisted random UUID (today's-behavior-equivalent
/// stability) rather than panicking.
pub fn machine_uid() -> String {
    MACHINE_UID.get_or_init(compute_machine_uid).clone()
}

/// Derive the opaque id from a raw machine-identity string via SHA-256.
///
/// Returns `muid_<first-16-bytes-of-sha256, hex>`. Hashing makes the value
/// opaque (the raw `MachineGuid` is never exposed) while staying fully
/// deterministic for a given input.
fn derive_uid(raw: &str) -> String {
    use sha2::{Digest, Sha256};

    let mut hasher = Sha256::new();
    hasher.update(raw.as_bytes());
    let hash = hasher.finalize();
    format!("{}{}", MUID_PREFIX, hex::encode(&hash[..16]))
}

#[cfg(windows)]
fn compute_machine_uid() -> String {
    match read_machine_guid() {
        Ok(guid) if !guid.trim().is_empty() => derive_uid(guid.trim()),
        Ok(_) => {
            tracing::warn!(
                "MachineGuid registry value was empty; falling back to persisted machine_uid"
            );
            persisted_uid()
        }
        Err(e) => {
            tracing::warn!(
                "Failed to read MachineGuid from registry ({e}); falling back to persisted machine_uid"
            );
            persisted_uid()
        }
    }
}

#[cfg(not(windows))]
fn compute_machine_uid() -> String {
    // No OS machine GUID available — use the persisted random UUID, hashed for a
    // uniform opaque shape with the Windows path.
    persisted_uid()
}

/// Read `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (REG_SZ).
///
/// Uses `RegGetValueW`, which opens, queries, null-terminates, and (with
/// `RRF_RT_REG_SZ`) type-checks the value in one call.
#[cfg(windows)]
fn read_machine_guid() -> anyhow::Result<String> {
    use anyhow::{anyhow, Context};
    use windows::core::PCWSTR;
    use windows::Win32::Foundation::ERROR_SUCCESS;
    use windows::Win32::System::Registry::{RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ};

    fn to_wide(s: &str) -> Vec<u16> {
        s.encode_utf16().chain(std::iter::once(0)).collect()
    }

    let subkey = to_wide(r"SOFTWARE\Microsoft\Cryptography");
    let value = to_wide("MachineGuid");

    unsafe {
        // First query the required buffer size (in bytes).
        let mut size: u32 = 0;
        let status = RegGetValueW(
            HKEY_LOCAL_MACHINE,
            PCWSTR(subkey.as_ptr()),
            PCWSTR(value.as_ptr()),
            RRF_RT_REG_SZ,
            None,
            None,
            Some(&mut size),
        );
        if status != ERROR_SUCCESS {
            return Err(anyhow!("RegGetValueW(size) failed: {:?}", status));
        }
        if size == 0 {
            return Err(anyhow!("MachineGuid reported zero length"));
        }

        // `size` is bytes; allocate a u16 buffer large enough to hold it.
        let len_u16 = size.div_ceil(2) as usize;
        let mut buffer = vec![0u16; len_u16];
        let mut size_out = size;
        let status = RegGetValueW(
            HKEY_LOCAL_MACHINE,
            PCWSTR(subkey.as_ptr()),
            PCWSTR(value.as_ptr()),
            RRF_RT_REG_SZ,
            None,
            Some(buffer.as_mut_ptr() as *mut _),
            Some(&mut size_out),
        );
        if status != ERROR_SUCCESS {
            return Err(anyhow!("RegGetValueW(read) failed: {:?}", status));
        }

        // Trim the trailing NUL(s) that RegGetValueW guarantees.
        let chars = size_out as usize / 2;
        let slice = &buffer[..chars.min(buffer.len())];
        let end = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
        String::from_utf16(&slice[..end]).context("MachineGuid was not valid UTF-16")
    }
}

/// Read (or, on first use, generate and persist) a random UUID, then derive the
/// opaque id from it. This is the fallback identity: stable across calls and
/// process restarts because it is persisted to disk.
fn persisted_uid() -> String {
    let path = fallback_uid_path();

    // Try to read an existing value.
    if let Some(ref p) = path {
        if let Ok(contents) = std::fs::read_to_string(p) {
            let trimmed = contents.trim();
            if !trimmed.is_empty() {
                return derive_uid(trimmed);
            }
        }
    }

    // Generate a new random seed and persist it (best-effort).
    let seed = uuid::Uuid::new_v4().to_string();
    if let Some(ref p) = path {
        if let Some(parent) = p.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        if let Err(e) = std::fs::write(p, &seed) {
            tracing::warn!(
                "Could not persist fallback machine_uid seed to {:?} ({e}); \
                 id will be stable for this process only",
                p
            );
        }
    } else {
        tracing::warn!(
            "No writable data directory for fallback machine_uid seed; \
             id will be stable for this process only"
        );
    }

    derive_uid(&seed)
}

/// Location of the persisted fallback seed file.
///
/// - **Windows:** `%ProgramData%\GuruConnect\machine_uid` (mirrors the agent
///   config location), used only when the registry read fails.
/// - **Non-Windows:** `$XDG_DATA_HOME/guruconnect/machine_uid`, falling back to
///   `$HOME/.local/share/guruconnect/machine_uid`, then a temp-dir path.
fn fallback_uid_path() -> Option<std::path::PathBuf> {
    #[cfg(windows)]
    {
        if let Ok(program_data) = std::env::var("ProgramData") {
            return Some(
                std::path::PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("machine_uid"),
            );
        }
    }

    #[cfg(not(windows))]
    {
        if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
            if !xdg.is_empty() {
                return Some(
                    std::path::PathBuf::from(xdg)
                        .join("guruconnect")
                        .join("machine_uid"),
                );
            }
        }
        if let Ok(home) = std::env::var("HOME") {
            if !home.is_empty() {
                return Some(
                    std::path::PathBuf::from(home)
                        .join(".local")
                        .join("share")
                        .join("guruconnect")
                        .join("machine_uid"),
                );
            }
        }
    }

    // Last resort: a stable name in the system temp dir.
    Some(std::env::temp_dir().join("guruconnect_machine_uid"))
}

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

    #[test]
    fn machine_uid_is_non_empty_and_prefixed() {
        let uid = machine_uid();
        assert!(!uid.is_empty(), "machine_uid must not be empty");
        assert!(
            uid.starts_with(MUID_PREFIX),
            "machine_uid must start with {MUID_PREFIX}: got {uid}"
        );
        // muid_ + 16 bytes hex (32 chars).
        assert_eq!(
            uid.len(),
            MUID_PREFIX.len() + 32,
            "unexpected machine_uid length: {uid}"
        );
        assert!(
            uid[MUID_PREFIX.len()..]
                .chars()
                .all(|c| c.is_ascii_hexdigit()),
            "machine_uid suffix must be lowercase hex: {uid}"
        );
    }

    #[test]
    fn machine_uid_is_deterministic_across_calls() {
        // The cached public API must be stable.
        assert_eq!(machine_uid(), machine_uid());
    }

    #[test]
    fn derive_uid_is_deterministic() {
        // Same input -> same output; different input -> different output.
        let a = derive_uid("the-same-input");
        let b = derive_uid("the-same-input");
        let c = derive_uid("a-different-input");
        assert_eq!(a, b);
        assert_ne!(a, c);
        assert!(a.starts_with(MUID_PREFIX));
    }

    /// The non-Windows fallback must be stable across calls because it persists
    /// its seed. We exercise `persisted_uid()` directly (the public `machine_uid`
    /// is cached, so it cannot demonstrate persistence on its own).
    #[test]
    fn persisted_uid_is_stable_across_calls() {
        let first = persisted_uid();
        let second = persisted_uid();
        assert_eq!(
            first, second,
            "persisted fallback uid must be stable across calls"
        );
        assert!(first.starts_with(MUID_PREFIX));
    }

    /// On Windows specifically, the registry-derived path must be deterministic:
    /// reading the same `MachineGuid` twice yields the same uid.
    #[cfg(windows)]
    #[test]
    fn windows_machine_guid_path_is_deterministic() {
        // If the registry read succeeds, two reads must agree and the derived
        // uid must match. If it fails (unusual), the test still validates the
        // fallback determinism via compute_machine_uid().
        let a = compute_machine_uid();
        let b = compute_machine_uid();
        assert_eq!(a, b, "compute_machine_uid must be deterministic on Windows");
        assert!(a.starts_with(MUID_PREFIX));
    }
}
