//! Key Manager Module
//!
//! Provides secure key derivation strategies for encrypting sensitive credentials:
//! 1. **`SystemSecretKey`**: Derives key from machine UUID (default)
//! 2. **`PasswordKey`**: Derives key from user password using Argon2id
//! 3. **`EnvVarKey`**: Reads key from `FOXHUNT_ENCRYPTION_KEY` environment variable
//!
//! Features:
//! - Key caching with 5-minute expiration
//! - Secure memory zeroing on drop (via Zeroize trait)
//! - Cross-platform machine ID support (Linux, macOS, Windows)

use anyhow::{bail, Context, Result};
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
    Algorithm, Argon2, Params, Version,
};
use sha2::{Digest, Sha256};
use std::time::{Duration, Instant};
use zeroize::Zeroize;

/// Key cache duration (5 minutes)
const KEY_CACHE_DURATION: Duration = Duration::from_secs(5 * 60);

/// Expected key length in bytes (256 bits)
const KEY_LENGTH: usize = 32;

/// Argon2id memory cost in KiB (19 MiB = 19456 KiB)
const ARGON2_M_COST: u32 = 19456;

/// Argon2id time cost (iterations)
const ARGON2_T_COST: u32 = 2;

/// Argon2id parallelism factor
const ARGON2_P_COST: u32 = 1;

/// Argon2id output length in bytes
const ARGON2_OUTPUT_LEN: usize = 32;

/// Key manager for deriving and caching encryption keys
pub struct KeyManager {
    cache: Option<CachedKey>,
}

/// Cached encryption key with expiration
struct CachedKey {
    key: Vec<u8>,
    expires_at: Instant,
}

impl Drop for CachedKey {
    fn drop(&mut self) {
        // Securely zero out the key material
        self.key.zeroize();
    }
}

impl KeyManager {
    /// Create a new key manager
    pub const fn new() -> Self {
        Self { cache: None }
    }

    /// Derive key from system secret (machine UUID)
    ///
    /// This is the default key derivation strategy:
    /// - Linux: Reads `/etc/machine-id`
    /// - macOS/Windows: Uses system UUID or fallback to random seed
    /// - Uses SHA-256 to derive 32-byte key from UUID
    pub fn derive_key(&mut self) -> Result<Vec<u8>> {
        // Check cache first
        if let Some(cached) = &self.cache {
            if Instant::now() < cached.expires_at {
                return Ok(cached.key.clone());
            }
        }

        // Derive new key
        let machine_id =
            Self::get_machine_id().context("Failed to get machine ID for key derivation")?;

        let key = Self::derive_key_from_machine_id(&machine_id)?;

        // Cache the key
        self.cache = Some(CachedKey {
            key: key.clone(),
            expires_at: Instant::now() + KEY_CACHE_DURATION,
        });

        Ok(key)
    }

    /// Derive key from user password using Argon2id
    ///
    /// This is used when the `--secure` flag is enabled:
    /// - Uses Argon2id with parameters: `m_cost=19MB`, `t_cost=2`, `p_cost=1`
    /// - Generates random salt (16 bytes)
    /// - Outputs 32-byte key
    pub fn derive_key_from_password(&mut self, password: &str) -> Result<Vec<u8>> {
        // Check cache first
        if let Some(cached) = &self.cache {
            if Instant::now() < cached.expires_at {
                return Ok(cached.key.clone());
            }
        }

        // Validate password
        if password.is_empty() {
            bail!("Password cannot be empty");
        }

        // Generate random salt
        let salt = SaltString::generate(&mut OsRng);

        // Configure Argon2id parameters
        let params = Params::new(
            ARGON2_M_COST,
            ARGON2_T_COST,
            ARGON2_P_COST,
            Some(ARGON2_OUTPUT_LEN),
        )
        .map_err(|e| anyhow::anyhow!("Failed to create Argon2 parameters: {}", e))?;

        let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);

        // Hash the password
        let password_hash = argon2
            .hash_password(password.as_bytes(), &salt)
            .map_err(|e| anyhow::anyhow!("Failed to hash password with Argon2id: {}", e))?;

        // Extract the hash bytes
        let hash = password_hash
            .hash
            .context("Password hash missing hash field")?;

        let key = hash.as_bytes().to_vec();

        // Validate key length
        if key.len() != KEY_LENGTH {
            bail!(
                "Derived key has incorrect length: expected {}, got {}",
                KEY_LENGTH,
                key.len()
            );
        }

        // Cache the key
        self.cache = Some(CachedKey {
            key: key.clone(),
            expires_at: Instant::now() + KEY_CACHE_DURATION,
        });

        Ok(key)
    }

    /// Derive key from `FOXHUNT_ENCRYPTION_KEY` environment variable
    ///
    /// Reads key from environment variable:
    /// - Expects hex-encoded 32-byte key (64 hex characters)
    /// - Validates length
    pub fn derive_key_from_env(&mut self) -> Result<Vec<u8>> {
        // Check cache first
        if let Some(cached) = &self.cache {
            if Instant::now() < cached.expires_at {
                return Ok(cached.key.clone());
            }
        }

        // Read from environment variable
        let hex_key = std::env::var("FOXHUNT_ENCRYPTION_KEY")
            .context("FOXHUNT_ENCRYPTION_KEY environment variable not set")?;

        // Decode from hex
        let key = hex::decode(&hex_key)
            .context("Failed to decode hex-encoded key from FOXHUNT_ENCRYPTION_KEY")?;

        // Validate length
        if key.len() != KEY_LENGTH {
            bail!(
                "Invalid key length in FOXHUNT_ENCRYPTION_KEY: expected {} bytes, got {}",
                KEY_LENGTH,
                key.len()
            );
        }

        // Cache the key
        self.cache = Some(CachedKey {
            key: key.clone(),
            expires_at: Instant::now() + KEY_CACHE_DURATION,
        });

        Ok(key)
    }

    /// Clear the cached key (forces re-derivation on next access)
    pub fn clear_cache(&mut self) {
        self.cache = None;
    }

    /// Get machine ID for key derivation
    fn get_machine_id() -> Result<String> {
        #[cfg(target_os = "linux")]
        {
            Self::get_linux_machine_id()
        }

        #[cfg(target_os = "macos")]
        {
            Self::get_macos_machine_id()
        }

        #[cfg(target_os = "windows")]
        {
            Self::get_windows_machine_id()
        }

        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
        {
            Self::get_fallback_machine_id()
        }
    }

    #[cfg(target_os = "linux")]
    fn get_linux_machine_id() -> Result<String> {
        std::fs::read_to_string("/etc/machine-id")
            .or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id"))
            .map(|id| id.trim().to_owned())
            .context(
                "Failed to read Linux machine ID from /etc/machine-id or /var/lib/dbus/machine-id",
            )
    }

    #[cfg(target_os = "macos")]
    fn get_macos_machine_id() -> Result<String> {
        use std::process::Command;

        let output = Command::new("ioreg")
            .args(&["-rd1", "-c", "IOPlatformExpertDevice"])
            .output()
            .context("Failed to execute ioreg command on macOS")?;

        if !output.status.success() {
            bail!("ioreg command failed");
        }

        let output_str =
            String::from_utf8(output.stdout).context("Failed to parse ioreg output as UTF-8")?;

        // Extract IOPlatformUUID
        for line in output_str.lines() {
            if line.contains("IOPlatformUUID") {
                if let Some(uuid) = line.split('"').nth(3) {
                    return Ok(uuid.to_string());
                }
            }
        }

        bail!("Failed to extract IOPlatformUUID from ioreg output")
    }

    #[cfg(target_os = "windows")]
    fn get_windows_machine_id() -> Result<String> {
        use std::process::Command;

        let output = Command::new("wmic")
            .args(&["csproduct", "get", "UUID"])
            .output()
            .context("Failed to execute wmic command on Windows")?;

        if !output.status.success() {
            bail!("wmic command failed");
        }

        let output_str =
            String::from_utf8(output.stdout).context("Failed to parse wmic output as UTF-8")?;

        // Extract UUID (skip header line)
        for line in output_str.lines().skip(1) {
            let trimmed = line.trim();
            if !trimmed.is_empty() {
                return Ok(trimmed.to_string());
            }
        }

        bail!("Failed to extract UUID from wmic output")
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    fn get_fallback_machine_id() -> Result<String> {
        use getrandom::getrandom;

        // Generate random seed as fallback
        let mut buf = [0u8; 16];
        getrandom(&mut buf).context("Failed to generate random machine ID fallback")?;

        Ok(hex::encode(buf))
    }

    /// Derive 32-byte key from machine ID using SHA-256
    fn derive_key_from_machine_id(machine_id: &str) -> Result<Vec<u8>> {
        let mut hasher = Sha256::new();
        hasher.update(machine_id.as_bytes());
        let result = hasher.finalize();
        Ok(result.to_vec())
    }
}

impl Default for KeyManager {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_system_key_derivation() {
        let mut manager = KeyManager::new();

        // Derive key twice - should use cache on second call
        let key1 = manager.derive_key().expect("Failed to derive key");
        let key2 = manager
            .derive_key()
            .expect("Failed to derive key from cache");

        assert_eq!(key1.len(), KEY_LENGTH, "Key should be 32 bytes");
        assert_eq!(key1, key2, "Cached key should match original");
    }

    #[test]
    fn test_password_key_derivation() {
        let mut manager = KeyManager::new();
        let password = "test_password_123!@#";

        // Derive key twice - should use cache on second call
        let key1 = manager
            .derive_key_from_password(password)
            .expect("Failed to derive key from password");
        let key2 = manager
            .derive_key_from_password(password)
            .expect("Failed to derive key from password (cache)");

        assert_eq!(key1.len(), KEY_LENGTH, "Key should be 32 bytes");
        assert_eq!(key1, key2, "Cached key should match original");
    }

    #[test]
    fn test_password_key_empty_password() {
        let mut manager = KeyManager::new();
        let result = manager.derive_key_from_password("");

        assert!(result.is_err(), "Empty password should fail");
        assert!(result.unwrap_err().to_string().contains("empty"));
    }

    #[test]
    #[serial_test::serial]
    fn test_env_key_derivation() {
        let mut manager = KeyManager::new();

        // Set environment variable with valid hex-encoded 32-byte key
        let test_key = hex::encode([0x42_u8; 32]);
        std::env::set_var("FOXHUNT_ENCRYPTION_KEY", &test_key);

        let key = manager
            .derive_key_from_env()
            .expect("Failed to derive key from environment variable");

        assert_eq!(key.len(), KEY_LENGTH, "Key should be 32 bytes");
        assert_eq!(key, vec![0x42_u8; 32], "Key should match expected value");

        // Clean up
        std::env::remove_var("FOXHUNT_ENCRYPTION_KEY");
    }

    #[test]
    #[serial_test::serial]
    fn test_env_key_invalid_hex() {
        let mut manager = KeyManager::new();

        // Set invalid hex string
        std::env::set_var("FOXHUNT_ENCRYPTION_KEY", "invalid_hex");

        let result = manager.derive_key_from_env();
        assert!(result.is_err(), "Invalid hex should fail");

        // Clean up
        std::env::remove_var("FOXHUNT_ENCRYPTION_KEY");
    }

    #[test]
    #[serial_test::serial]
    fn test_env_key_wrong_length() {
        let mut manager = KeyManager::new();

        // Set key with wrong length (16 bytes instead of 32)
        let test_key = hex::encode([0x42_u8; 16]);
        std::env::set_var("FOXHUNT_ENCRYPTION_KEY", &test_key);

        let result = manager.derive_key_from_env();
        assert!(result.is_err(), "Wrong key length should fail");
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("Invalid key length"));

        // Clean up
        std::env::remove_var("FOXHUNT_ENCRYPTION_KEY");
    }

    #[test]
    #[serial_test::serial]
    fn test_env_key_missing() {
        let mut manager = KeyManager::new();

        // Ensure variable is not set
        std::env::remove_var("FOXHUNT_ENCRYPTION_KEY");

        let result = manager.derive_key_from_env();
        assert!(result.is_err(), "Missing environment variable should fail");
        assert!(result.unwrap_err().to_string().contains("not set"));
    }

    #[test]
    fn test_cache_expiration() {
        let mut manager = KeyManager::new();
        let password = "test_password";

        // Derive key
        let key1 = manager
            .derive_key_from_password(password)
            .expect("Failed to derive key");

        // Clear cache
        manager.clear_cache();

        // Derive again (should not use cache)
        let key2 = manager
            .derive_key_from_password(password)
            .expect("Failed to derive key after cache clear");

        // Keys should be different due to different salts
        assert_ne!(key1, key2, "Keys should differ with different salts");
    }

    #[test]
    #[ignore] // Requires /etc/machine-id (not available in containers)
    fn test_machine_id_derivation() {
        // This test verifies we can get a machine ID
        let machine_id = KeyManager::get_machine_id().expect("Failed to get machine ID");

        assert!(!machine_id.is_empty(), "Machine ID should not be empty");

        // Derive key from machine ID
        let key = KeyManager::derive_key_from_machine_id(&machine_id)
            .expect("Failed to derive key from machine ID");

        assert_eq!(key.len(), KEY_LENGTH, "Derived key should be 32 bytes");
    }

    #[test]
    fn test_zeroize_on_drop() {
        // Create a cached key
        let key = vec![0x42_u8; 32];
        let cached = CachedKey {
            key,
            expires_at: Instant::now() + KEY_CACHE_DURATION,
        };

        // Get pointer to key data
        let key_ptr = cached.key.as_ptr();

        // Drop the cached key (triggers Zeroize)
        drop(cached);

        // Note: We can't directly verify memory was zeroed due to Rust's
        // memory safety guarantees, but Zeroize ensures it happens
        // This test verifies the Drop implementation compiles and runs

        // Verify key_ptr is still valid (points to deallocated memory)
        // We can't safely read it, but the test confirms Drop was called
        let _ = key_ptr;
    }

    #[test]
    fn test_key_length_validation() {
        // Verify KEY_LENGTH constant is correct
        assert_eq!(KEY_LENGTH, 32, "Key length should be 32 bytes (256 bits)");
    }

    #[test]
    fn test_cache_duration() {
        // Verify cache duration is 5 minutes
        assert_eq!(KEY_CACHE_DURATION, Duration::from_secs(5 * 60));
    }

    #[test]
    fn test_argon2_parameters() {
        // Verify Argon2 parameters match specification
        assert_eq!(
            ARGON2_M_COST, 19456,
            "Memory cost should be 19 MiB (19456 KiB)"
        );
        assert_eq!(ARGON2_T_COST, 2, "Time cost should be 2 iterations");
        assert_eq!(ARGON2_P_COST, 1, "Parallelism should be 1");
        assert_eq!(ARGON2_OUTPUT_LEN, 32, "Output length should be 32 bytes");
    }
}
