use crate::auth::{AuthError, AuthResult};
use sha2::{Digest, Sha256};
use hmac::{Hmac, Mac};
use base64::{Engine as _, engine::general_purpose};
use rand::Rng;

type HmacSha256 = Hmac<Sha256>;

pub struct CryptoUtils;

impl CryptoUtils {
    /// Generate a cryptographically secure random string
    pub fn generate_random_string(length: usize) -> String {
        use rand::distributions::Alphanumeric;
        use rand::seq::SliceRandom;

        const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                                abcdefghijklmnopqrstuvwxyz\
                                0123456789";

        let mut rng = rand::thread_rng();
        (0..length)
            .map(|_| {
                let idx = rng.gen_range(0..CHARSET.len());
                CHARSET[idx] as char
            })
            .collect()
    }

    /// Generate a cryptographically secure random bytes
    pub fn generate_random_bytes(length: usize) -> Vec<u8> {
        let mut rng = rand::thread_rng();
        (0..length).map(|_| rng.gen()).collect()
    }

    /// Create SHA256 hash of input data
    pub fn sha256_hash(data: &[u8]) -> String {
        let hash = Sha256::digest(data);
        format!("{:x}", hash)
    }

    /// Create SHA256 hash of a string
    pub fn sha256_hash_string(data: &str) -> String {
        Self::sha256_hash(data.as_bytes())
    }

    /// Create HMAC-SHA256 signature
    pub fn hmac_sha256(key: &[u8], data: &[u8]) -> AuthResult<Vec<u8>> {
        let mut mac = HmacSha256::new_from_slice(key)
            .map_err(|e| AuthError {
                message: format!("Failed to create HMAC: {}", e),
                code: Some("HMAC_CREATE_ERROR".to_string()),
                field: None,
            })?;

        mac.update(data);
        Ok(mac.finalize().into_bytes().to_vec())
    }

    /// Create HMAC-SHA256 signature of a string
    pub fn hmac_sha256_string(key: &str, data: &str) -> AuthResult<String> {
        let signature = Self::hmac_sha256(key.as_bytes(), data.as_bytes())?;
        Ok(general_purpose::STANDARD.encode(signature))
    }

    /// Verify HMAC-SHA256 signature
    pub fn verify_hmac_sha256(key: &[u8], data: &[u8], signature: &[u8]) -> AuthResult<bool> {
        let mut mac = HmacSha256::new_from_slice(key)
            .map_err(|e| AuthError {
                message: format!("Failed to create HMAC: {}", e),
                code: Some("HMAC_CREATE_ERROR".to_string()),
                field: None,
            })?;

        mac.update(data);

        match mac.verify_slice(signature) {
            Ok(()) => Ok(true),
            Err(_) => Ok(false),
        }
    }

    /// Generate a secure token for password reset, email verification, etc.
    pub fn generate_secure_token() -> String {
        let random_bytes = Self::generate_random_bytes(32);
        general_purpose::URL_SAFE_NO_PAD.encode(random_bytes)
    }

    /// Generate a numeric code (for MFA, etc.)
    pub fn generate_numeric_code(digits: usize) -> String {
        let mut rng = rand::thread_rng();
        (0..digits)
            .map(|_| rng.gen_range(0..10).to_string())
            .collect()
    }

    /// Constant-time string comparison to prevent timing attacks
    pub fn secure_compare(a: &str, b: &str) -> bool {
        if a.len() != b.len() {
            return false;
        }

        let a_bytes = a.as_bytes();
        let b_bytes = b.as_bytes();

        let mut result = 0u8;
        for i in 0..a_bytes.len() {
            result |= a_bytes[i] ^ b_bytes[i];
        }

        result == 0
    }

    /// Generate a device fingerprint based on available browser information
    pub fn generate_device_fingerprint(
        user_agent: &str,
        screen_resolution: &str,
        timezone_offset: i32,
        language: &str,
    ) -> String {
        let combined = format!(
            "{}|{}|{}|{}",
            user_agent,
            screen_resolution,
            timezone_offset,
            language
        );

        Self::sha256_hash_string(&combined)
    }

    /// Create a time-based hash for session validation
    pub fn create_time_based_hash(data: &str, timestamp: u64, secret: &str) -> AuthResult<String> {
        let combined = format!("{}|{}|{}", data, timestamp, secret);
        Ok(Self::sha256_hash_string(&combined))
    }

    /// Verify a time-based hash
    pub fn verify_time_based_hash(
        data: &str,
        timestamp: u64,
        secret: &str,
        expected_hash: &str,
        max_age_seconds: u64,
    ) -> AuthResult<bool> {
        // Check if timestamp is within acceptable range
        let current_timestamp = js_sys::Date::now() as u64 / 1000;
        if current_timestamp.saturating_sub(timestamp) > max_age_seconds {
            return Ok(false);
        }

        let computed_hash = Self::create_time_based_hash(data, timestamp, secret)?;
        Ok(Self::secure_compare(&computed_hash, expected_hash))
    }

    /// Generate a JWT-like token structure (simplified)
    pub fn generate_token_with_expiry(
        payload: &str,
        secret: &str,
        expires_in_seconds: u64,
    ) -> AuthResult<String> {
        let current_timestamp = js_sys::Date::now() as u64 / 1000;
        let expires_at = current_timestamp + expires_in_seconds;

        let header = r#"{"alg":"HS256","typ":"JWT"}"#;
        let claims = format!(
            r#"{{"sub":"{}","exp":{},"iat":{}}}"#,
            payload,
            expires_at,
            current_timestamp
        );

        let header_b64 = general_purpose::URL_SAFE_NO_PAD.encode(header.as_bytes());
        let claims_b64 = general_purpose::URL_SAFE_NO_PAD.encode(claims.as_bytes());

        let unsigned_token = format!("{}.{}", header_b64, claims_b64);
        let signature = Self::hmac_sha256_string(secret, &unsigned_token)?;
        let signature_b64 = general_purpose::URL_SAFE_NO_PAD.encode(
            general_purpose::STANDARD.decode(signature)
                .map_err(|e| AuthError {
                    message: format!("Failed to decode signature: {}", e),
                    code: Some("SIGNATURE_DECODE_ERROR".to_string()),
                    field: None,
                })?
        );

        Ok(format!("{}.{}", unsigned_token, signature_b64))
    }

    /// Encode data using URL-safe base64
    pub fn base64_url_encode(data: &[u8]) -> String {
        general_purpose::URL_SAFE_NO_PAD.encode(data)
    }

    /// Decode URL-safe base64 data
    pub fn base64_url_decode(data: &str) -> AuthResult<Vec<u8>> {
        general_purpose::URL_SAFE_NO_PAD.decode(data)
            .map_err(|e| AuthError {
                message: format!("Base64 decode error: {}", e),
                code: Some("BASE64_DECODE_ERROR".to_string()),
                field: None,
            })
    }

    /// Generate a CSRF token
    pub fn generate_csrf_token() -> String {
        Self::generate_secure_token()
    }

    /// Create a secure session identifier
    pub fn generate_session_id() -> String {
        let timestamp = js_sys::Date::now() as u64;
        let random_data = Self::generate_random_bytes(16);
        let combined = format!("{}{}", timestamp, general_purpose::STANDARD.encode(random_data));
        Self::sha256_hash_string(&combined)
    }

    /// Create a password hash (simplified - in production use bcrypt or similar)
    pub fn hash_password(password: &str, salt: &str) -> String {
        let combined = format!("{}${}", password, salt);
        Self::sha256_hash_string(&combined)
    }

    /// Generate a random salt for password hashing
    pub fn generate_salt() -> String {
        let salt_bytes = Self::generate_random_bytes(16);
        general_purpose::STANDARD.encode(salt_bytes)
    }

    /// Verify password against hash
    pub fn verify_password(password: &str, hash: &str, salt: &str) -> bool {
        let computed_hash = Self::hash_password(password, salt);
        Self::secure_compare(&computed_hash, hash)
    }
}

/// Utility functions for working with browser crypto APIs
pub struct BrowserCrypto;

impl BrowserCrypto {
    /// Get random values using browser's crypto API
    pub fn get_random_values(length: usize) -> AuthResult<Vec<u8>> {
        use web_sys::window;

        let window = window().ok_or_else(|| AuthError {
            message: "Window not available".to_string(),
            code: Some("WINDOW_NOT_AVAILABLE".to_string()),
            field: None,
        })?;

        let crypto = window.crypto().map_err(|_| AuthError {
            message: "Crypto API not available".to_string(),
            code: Some("CRYPTO_NOT_AVAILABLE".to_string()),
            field: None,
        })?;

        let mut buffer = vec![0u8; length];
        let uint8_array = js_sys::Uint8Array::new_with_length(length as u32);

        crypto.get_random_values_with_u8_array(&uint8_array).map_err(|_| AuthError {
            message: "Failed to get random values".to_string(),
            code: Some("GET_RANDOM_VALUES_ERROR".to_string()),
            field: None,
        })?;

        uint8_array.copy_to(&mut buffer);
        Ok(buffer)
    }

    /// Check if Web Crypto API is available
    pub fn is_available() -> bool {
        web_sys::window()
            .and_then(|w| w.crypto().ok())
            .is_some()
    }
}

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

    #[test]
    fn test_generate_random_string() {
        let result = CryptoUtils::generate_random_string(10);
        assert_eq!(result.len(), 10);
    }

    #[test]
    fn test_sha256_hash() {
        let result = CryptoUtils::sha256_hash_string("hello world");
        assert_eq!(result.len(), 64); // SHA256 produces 32 bytes = 64 hex chars
    }

    #[test]
    fn test_secure_compare() {
        assert!(CryptoUtils::secure_compare("hello", "hello"));
        assert!(!CryptoUtils::secure_compare("hello", "world"));
        assert!(!CryptoUtils::secure_compare("hello", "hello world"));
    }

    #[test]
    fn test_base64_url_encode_decode() {
        let data = b"hello world";
        let encoded = CryptoUtils::base64_url_encode(data);
        let decoded = CryptoUtils::base64_url_decode(&encoded).unwrap();
        assert_eq!(data, decoded.as_slice());
    }
}
