//! Secure token generation and hashing
//! 
//! Provides cryptographically secure random token generation and hashing
//! for use in password reset, email verification, and refresh tokens.

use crate::error::{AuthError, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use rand::{rngs::OsRng, RngCore};
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;

/// Generate a cryptographically secure random token
/// 
/// # Arguments
/// 
/// * `length` - Number of random bytes to generate (default: 32)
/// 
/// # Returns
/// 
/// URL-safe base64 encoded token string
/// 
/// # Security
/// 
/// - Uses OS-provided CSPRNG (CryptGenRandom on Windows, /dev/urandom on Unix)
/// - URL-safe encoding (safe for use in URLs and emails)
/// - 32 bytes = 256 bits of entropy (default)
/// 
/// # Example
/// 
/// ```rust
/// use irene_auth::generate_token;
/// 
/// // Generate default 32-byte token
/// let token = generate_token(32).unwrap();
/// assert_eq!(token.len(), 43); // base64 encoded length
/// ```
pub fn generate_token(length: usize) -> Result<String> {
    if length == 0 || length > 1024 {
        return Err(AuthError::TokenGenerationError(
            "Length must be between 1 and 1024 bytes".to_string(),
        ));
    }

    let mut bytes = vec![0u8; length];
    OsRng.fill_bytes(&mut bytes);

    // Encode as URL-safe base64 (no padding)
    let token = URL_SAFE_NO_PAD.encode(&bytes);

    Ok(token)
}

/// Hash a token for secure database storage
/// 
/// # Arguments
/// 
/// * `token` - The plaintext token to hash
/// 
/// # Returns
/// 
/// Hex-encoded SHA-256 hash of the token
/// 
/// # Security
/// 
/// - Uses SHA-256 (one-way hash function)
/// - Tokens should NEVER be stored in plaintext
/// - Compare using `verify_token_hash()` for constant-time comparison
/// 
/// # Example
/// 
/// ```rust
/// use irene_auth::{generate_token, hash_token};
/// 
/// let token = generate_token(32).unwrap();
/// let token_hash = hash_token(&token).unwrap();
/// 
/// // Store token_hash in database, send token to user
/// ```
pub fn hash_token(token: &str) -> Result<String> {
    if token.is_empty() {
        return Err(AuthError::TokenHashError("Token cannot be empty".to_string()));
    }

    let mut hasher = Sha256::new();
    hasher.update(token.as_bytes());
    let result = hasher.finalize();

    // Return hex-encoded hash
    Ok(hex::encode(result))
}

/// Verify a token against a stored hash using constant-time comparison
/// 
/// # Arguments
/// 
/// * `token` - The plaintext token to verify
/// * `token_hash` - The stored hash to compare against
/// 
/// # Returns
/// 
/// `true` if token matches hash, `false` otherwise
/// 
/// # Security
/// 
/// - Uses constant-time comparison to prevent timing attacks
/// - Prevents attackers from learning information about valid tokens
///   through timing analysis
/// 
/// # Example
/// 
/// ```rust
/// use irene_auth::{generate_token, hash_token, verify_token_hash};
/// 
/// let token = generate_token(32).unwrap();
/// let token_hash = hash_token(&token).unwrap();
/// 
/// // Verify correct token
/// assert!(verify_token_hash(&token, &token_hash));
/// 
/// // Verify wrong token
/// let wrong_token = generate_token(32).unwrap();
/// assert!(!verify_token_hash(&wrong_token, &token_hash));
/// ```
pub fn verify_token_hash(token: &str, token_hash: &str) -> bool {
    // Hash the provided token
    let computed_hash = match hash_token(token) {
        Ok(h) => h,
        Err(_) => return false,
    };

    // Constant-time comparison
    let computed_bytes = computed_hash.as_bytes();
    let stored_bytes = token_hash.as_bytes();

    if computed_bytes.len() != stored_bytes.len() {
        return false;
    }

    // Use subtle crate for constant-time comparison
    computed_bytes.ct_eq(stored_bytes).into()
}

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

    #[test]
    fn test_generate_token() {
        let token = generate_token(32).unwrap();
        
        // URL-safe base64 without padding has specific length
        assert!(token.len() >= 40);
        
        // Should only contain URL-safe characters
        assert!(token.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_'));
    }

    #[test]
    fn test_token_uniqueness() {
        let token1 = generate_token(32).unwrap();
        let token2 = generate_token(32).unwrap();
        
        assert_ne!(token1, token2);
    }

    #[test]
    fn test_token_length_validation() {
        assert!(generate_token(0).is_err());
        assert!(generate_token(1025).is_err());
        assert!(generate_token(1).is_ok());
        assert!(generate_token(1024).is_ok());
    }

    #[test]
    fn test_hash_token() {
        let token = "test_token_123";
        let hash = hash_token(token).unwrap();
        
        // SHA-256 produces 64 hex characters
        assert_eq!(hash.len(), 64);
        
        // Should be hex
        assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn test_hash_deterministic() {
        let token = "same_token";
        let hash1 = hash_token(token).unwrap();
        let hash2 = hash_token(token).unwrap();
        
        // Same token should produce same hash
        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_verify_token_hash_correct() {
        let token = generate_token(32).unwrap();
        let hash = hash_token(&token).unwrap();
        
        assert!(verify_token_hash(&token, &hash));
    }

    #[test]
    fn test_verify_token_hash_incorrect() {
        let token1 = generate_token(32).unwrap();
        let token2 = generate_token(32).unwrap();
        let hash1 = hash_token(&token1).unwrap();
        
        assert!(!verify_token_hash(&token2, &hash1));
    }

    #[test]
    fn test_verify_empty_token() {
        let hash = hash_token("valid_token").unwrap();
        assert!(!verify_token_hash("", &hash));
    }

    #[test]
    fn test_constant_time_comparison() {
        // This test verifies that the function runs,
        // but timing analysis would be needed to verify constant-time property
        let token = "test_token";
        let hash = hash_token(token).unwrap();
        
        let wrong_token = "wrong_token";
        assert!(!verify_token_hash(wrong_token, &hash));
    }
}
