//! Per-agent key minting, hashing, and verification (auth layer).
//!
//! This is the auth-plane counterpart to the persistence-only
//! [`crate::db::agent_keys`] module. It owns the *secret* side of the per-agent
//! key lifecycle:
//!
//! - [`generate_agent_key`] mints a high-entropy, `cak_`-prefixed plaintext key.
//!   The plaintext is shown to the operator exactly once at issuance and is
//!   NEVER persisted or logged.
//! - [`hash_agent_key`] computes the SHA-256 hex digest that the database
//!   stores and looks up by. SHA-256 (not Argon2) is deliberate here: the key is
//!   already high-entropy random, so the slow, salted password KDF buys nothing,
//!   and a deterministic digest is required for the constant-shape hash-equality
//!   lookup. Argon2id remains reserved for *passwords* (see
//!   [`crate::auth::password`]).
//! - [`verify_agent_key`] hashes a presented key, looks it up against the
//!   active (non-revoked) rows, stamps `last_used_at` on a hit, and returns the
//!   owning machine id.
//!
//! This mirrors the GuruRMM `agk_` enrollment pattern
//! (`guru-rmm/server/src/ws/mod.rs`): random bytes -> prefixed token ->
//! hex SHA-256 stored hash.
//!
//! SECURITY: never log a plaintext key or its hash. Functions here return the
//! plaintext to the caller (issuance endpoint) but emit no `tracing` output
//! containing key material.

use rand::RngCore;
use ring::digest;
use sqlx::PgPool;
use uuid::Uuid;

use crate::db;

/// Prefix marking a GuruConnect per-agent key. Mirrors GuruRMM's `agk_`.
pub const AGENT_KEY_PREFIX: &str = "cak_";

/// Number of random bytes behind a per-agent key (256 bits of entropy).
const AGENT_KEY_RANDOM_BYTES: usize = 32;

/// Generate a new high-entropy, `cak_`-prefixed per-agent key (plaintext).
///
/// The returned string is the ONLY time the plaintext exists; the caller must
/// surface it to the operator once and store only [`hash_agent_key`] of it.
/// Uses the OS CSPRNG via `rand::rngs::OsRng`.
pub fn generate_agent_key() -> String {
    let mut bytes = [0u8; AGENT_KEY_RANDOM_BYTES];
    rand::rngs::OsRng.fill_bytes(&mut bytes);
    format!("{}{}", AGENT_KEY_PREFIX, hex_encode(&bytes))
}

/// Compute the storage/lookup hash for a per-agent key.
///
/// SHA-256, hex-encoded lowercase — identical in shape to GuruRMM's
/// `hash_api_key`, so the two systems' hashed-key columns are interchangeable
/// in format. Deterministic by design: the database looks keys up by this exact
/// value.
pub fn hash_agent_key(presented: &str) -> String {
    let digest = digest::digest(&digest::SHA256, presented.as_bytes());
    hex_encode(digest.as_ref())
}

/// Verify a presented per-agent key.
///
/// Returns the owning `machine_id` if the key hashes to an active (non-revoked)
/// row, stamping `last_used_at` as a side effect. Returns `None` if the key is
/// unknown or revoked (`find_active_by_hash` already filters `revoked_at`).
///
/// SECURITY: only the hash touches the database; the plaintext never leaves
/// this function and is never logged.
pub async fn verify_agent_key(pool: &PgPool, presented: &str) -> Option<Uuid> {
    // Cheap structural reject before hashing/DB work: a real key is prefixed.
    if !presented.starts_with(AGENT_KEY_PREFIX) {
        return None;
    }

    let key_hash = hash_agent_key(presented);

    match db::agent_keys::find_active_by_hash(pool, &key_hash).await {
        Ok(Some(key)) => {
            // Best-effort usage stamp; a failure here must not block auth.
            if let Err(e) = db::agent_keys::touch_last_used(pool, key.id).await {
                tracing::warn!("Failed to stamp last_used_at for agent key: {}", e);
            }
            Some(key.machine_id)
        }
        Ok(None) => None,
        Err(e) => {
            tracing::error!("Database error during agent key verification: {}", e);
            None
        }
    }
}

/// Lowercase hex encoding without pulling in the `hex` crate.
fn hex_encode(bytes: &[u8]) -> String {
    use std::fmt::Write;
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        let _ = write!(s, "{:02x}", b);
    }
    s
}

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

    #[test]
    fn generated_key_is_prefixed_and_high_entropy() {
        let key = generate_agent_key();
        assert!(key.starts_with(AGENT_KEY_PREFIX));
        // prefix + 32 bytes * 2 hex chars
        assert_eq!(
            key.len(),
            AGENT_KEY_PREFIX.len() + AGENT_KEY_RANDOM_BYTES * 2
        );
    }

    #[test]
    fn generated_keys_are_unique() {
        let a = generate_agent_key();
        let b = generate_agent_key();
        assert_ne!(a, b);
    }

    #[test]
    fn hash_is_stable_and_hex() {
        let key = "cak_deadbeef";
        let h1 = hash_agent_key(key);
        let h2 = hash_agent_key(key);
        assert_eq!(h1, h2);
        assert_eq!(h1.len(), 64); // SHA-256 -> 32 bytes -> 64 hex chars
        assert!(h1.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn hash_differs_per_key() {
        assert_ne!(hash_agent_key("cak_aaa"), hash_agent_key("cak_bbb"));
    }

    #[test]
    fn hash_matches_known_sha256_vector() {
        // SHA-256("abc") well-known test vector.
        assert_eq!(
            hex_encode(digest::digest(&digest::SHA256, b"abc").as_ref()),
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }
}
