//! TOTP (Time-based One-Time Password) implementation
//! RFC 6238 compliant with QR code generation

use chrono::Utc;
use image::Luma;
use qrcode::QrCode;
use rand::Rng;
use totp_rs::{Algorithm, Secret, TOTP};

use crate::error::{infrastructure, ControlCenterError, Result};
use crate::mfa::types::{BackupCode, TotpAlgorithm, TotpDevice, TotpEnrollResponse};

/// TOTP service for managing Time-based One-Time Passwords
pub struct TotpService {
    /// Issuer name for QR codes
    issuer: String,
    /// Clock drift tolerance in periods (±1 = 60 seconds total)
    tolerance: u8,
}

impl TotpService {
    /// Create new TOTP service
    pub fn new(issuer: String) -> Self {
        Self {
            issuer,
            tolerance: 1, // ±30 seconds
        }
    }

    /// Generate a new TOTP secret (Base32 encoded)
    pub fn generate_secret(&self) -> String {
        // Generate 20 random bytes (160 bits) for TOTP secret
        let mut rng = rand::rng();
        let secret_bytes: Vec<u8> = (0..20).map(|_| rng.random()).collect();
        Secret::Raw(secret_bytes).to_string()
    }

    /// Create enrollment response with QR code
    pub fn create_enrollment(
        &self,
        user_id: &str,
        user_email: &str,
    ) -> Result<(TotpDevice, TotpEnrollResponse)> {
        let secret = self.generate_secret();
        let mut device = TotpDevice::new(user_id.to_string(), secret.clone());

        // Generate backup codes
        let backup_codes = self.generate_backup_codes(10);
        device.backup_codes = backup_codes
            .iter()
            .map(|code| BackupCode::new(code.clone()))
            .collect();

        // Create TOTP instance
        let totp = self.create_totp(&device, user_email)?;

        // Generate QR code
        let otpauth_url = totp.get_url();
        let qr_code = self.generate_qr_code(&otpauth_url)?;

        let response = TotpEnrollResponse {
            device_id: device.id.clone(),
            secret: secret.clone(),
            qr_code,
            backup_codes: backup_codes.clone(),
            otpauth_url,
        };

        Ok((device, response))
    }

    /// Verify TOTP code
    pub fn verify_code(
        &self,
        device: &mut TotpDevice,
        code: &str,
        user_email: &str,
    ) -> Result<bool> {
        let totp = self.create_totp(device, user_email)?;

        let valid = totp.check_current(code).map_err(|e| {
            ControlCenterError::from(infrastructure::internal_error(&format!(
                "TOTP verification failed: {}",
                e
            )))
        })?;

        if valid {
            device.last_used = Some(Utc::now());
        }

        Ok(valid)
    }

    /// Verify backup code
    pub fn verify_backup_code(&self, device: &mut TotpDevice, code: &str) -> bool {
        for backup in &mut device.backup_codes {
            if backup.verify(code) {
                backup.mark_used();
                device.last_used = Some(Utc::now());
                return true;
            }
        }
        false
    }

    /// Generate backup codes
    pub fn generate_backup_codes(&self, count: usize) -> Vec<String> {
        let mut rng = rand::rng();
        let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        let char_vec: Vec<char> = chars.chars().collect();

        (0..count)
            .map(|_| {
                // Generate 10-character alphanumeric codes
                (0..10)
                    .map(|_| {
                        let idx = rng.random_range(0..char_vec.len());
                        char_vec[idx]
                    })
                    .collect()
            })
            .collect()
    }

    /// Check if device has unused backup codes
    pub fn has_backup_codes(&self, device: &TotpDevice) -> bool {
        device.backup_codes.iter().any(|code| !code.used)
    }

    /// Count remaining backup codes
    pub fn backup_codes_remaining(&self, device: &TotpDevice) -> usize {
        device.backup_codes.iter().filter(|code| !code.used).count()
    }

    /// Regenerate backup codes
    pub fn regenerate_backup_codes(&self, device: &mut TotpDevice) -> Vec<String> {
        let backup_codes = self.generate_backup_codes(10);
        device.backup_codes = backup_codes
            .iter()
            .map(|code| BackupCode::new(code.clone()))
            .collect();
        backup_codes
    }

    // Private helper methods

    /// Create TOTP instance from device
    fn create_totp(&self, device: &TotpDevice, account_name: &str) -> Result<TOTP> {
        let algorithm = match device.algorithm {
            TotpAlgorithm::Sha1 => Algorithm::SHA1,
            TotpAlgorithm::Sha256 => Algorithm::SHA256,
            TotpAlgorithm::Sha512 => Algorithm::SHA512,
        };

        TOTP::new(
            algorithm,
            device.digits as usize,
            self.tolerance,
            device.period as u64,
            Secret::Encoded(device.secret.clone())
                .to_bytes()
                .map_err(|e| {
                    ControlCenterError::from(infrastructure::internal_error(&format!(
                        "Invalid secret: {}",
                        e
                    )))
                })?,
            Some(self.issuer.clone()),
            account_name.to_string(),
        )
        .map_err(|e| {
            ControlCenterError::from(infrastructure::internal_error(&format!(
                "Failed to create TOTP: {}",
                e
            )))
        })
    }

    /// Generate QR code as base64 data URL
    fn generate_qr_code(&self, otpauth_url: &str) -> Result<String> {
        // Create QR code
        let code = QrCode::new(otpauth_url).map_err(|e| {
            ControlCenterError::from(infrastructure::internal_error(&format!(
                "Failed to generate QR code: {}",
                e
            )))
        })?;

        // Render to image
        let image = code
            .render::<Luma<u8>>()
            .max_dimensions(512, 512)
            .min_dimensions(256, 256)
            .build();

        // Convert to PNG bytes
        let mut png_bytes = Vec::new();
        image
            .write_to(
                &mut std::io::Cursor::new(&mut png_bytes),
                image::ImageFormat::Png,
            )
            .map_err(|e| {
                ControlCenterError::from(infrastructure::internal_error(&format!(
                    "Failed to encode QR code: {}",
                    e
                )))
            })?;

        // Convert to base64 data URL
        use base64::{engine::general_purpose::STANDARD, Engine};
        let base64_image = STANDARD.encode(&png_bytes);
        Ok(format!("data:image/png;base64,{}", base64_image))
    }
}

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

    #[test]
    fn test_secret_generation() {
        let service = TotpService::new("TestApp".to_string());
        let secret = service.generate_secret();

        // Base32 secrets should be valid
        assert!(!secret.is_empty());
        assert!(secret
            .chars()
            .all(|c| "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".contains(c)));
    }

    #[test]
    fn test_backup_code_generation() {
        let service = TotpService::new("TestApp".to_string());
        let codes = service.generate_backup_codes(10);

        assert_eq!(codes.len(), 10);
        for code in &codes {
            assert_eq!(code.len(), 10);
            assert!(code.chars().all(|c| c.is_alphanumeric()));
        }

        // Codes should be unique
        let unique_codes: std::collections::HashSet<_> = codes.iter().collect();
        assert_eq!(unique_codes.len(), 10);
    }

    #[test]
    fn test_enrollment_creation() {
        let service = TotpService::new("TestApp".to_string());
        let result = service.create_enrollment("user123", "test@example.com");

        assert!(result.is_ok());
        let (device, response) = result.unwrap();

        assert_eq!(device.user_id, "user123");
        assert_eq!(device.digits, 6);
        assert_eq!(device.period, 30);
        assert_eq!(device.backup_codes.len(), 10);
        assert_eq!(response.backup_codes.len(), 10);
        assert!(response.qr_code.starts_with("data:image/png;base64,"));
        assert!(response.otpauth_url.contains("TestApp"));
        assert!(response.otpauth_url.contains("test@example.com"));
    }

    #[test]
    fn test_totp_verification() {
        let service = TotpService::new("TestApp".to_string());
        let (mut device, _) = service
            .create_enrollment("user123", "test@example.com")
            .unwrap();

        // Create TOTP and generate current code
        let totp = service.create_totp(&device, "test@example.com").unwrap();
        let code = totp.generate_current().unwrap();

        // Verify the code
        let result = service.verify_code(&mut device, &code, "test@example.com");
        assert!(result.is_ok());
        assert!(result.unwrap());
        assert!(device.last_used.is_some());
    }

    #[test]
    fn test_backup_code_verification() {
        let service = TotpService::new("TestApp".to_string());
        let (mut device, response) = service
            .create_enrollment("user123", "test@example.com")
            .unwrap();

        let backup_code = response.backup_codes[0].clone();

        // Verify backup code
        assert!(service.verify_backup_code(&mut device, &backup_code));
        assert!(device.last_used.is_some());

        // Should not work twice
        assert!(!service.verify_backup_code(&mut device, &backup_code));
    }

    #[test]
    fn test_backup_codes_remaining() {
        let service = TotpService::new("TestApp".to_string());
        let (mut device, response) = service
            .create_enrollment("user123", "test@example.com")
            .unwrap();

        assert_eq!(service.backup_codes_remaining(&device), 10);
        assert!(service.has_backup_codes(&device));

        // Use one code
        let backup_code = response.backup_codes[0].clone();
        service.verify_backup_code(&mut device, &backup_code);

        assert_eq!(service.backup_codes_remaining(&device), 9);
        assert!(service.has_backup_codes(&device));
    }

    #[test]
    fn test_regenerate_backup_codes() {
        let service = TotpService::new("TestApp".to_string());
        let (mut device, response) = service
            .create_enrollment("user123", "test@example.com")
            .unwrap();

        let old_codes = response.backup_codes;
        let new_codes = service.regenerate_backup_codes(&mut device);

        assert_eq!(new_codes.len(), 10);
        assert_eq!(device.backup_codes.len(), 10);

        // New codes should be different
        assert_ne!(old_codes, new_codes);

        // Old codes should not work
        assert!(!service.verify_backup_code(&mut device, &old_codes[0]));

        // New codes should work
        assert!(service.verify_backup_code(&mut device, &new_codes[0]));
    }
}
