//! Multi-Factor Authentication (MFA) Module
//!
//! Implements TOTP-based multi-factor authentication for financial system security.
//! CRITICAL SECURITY: Addresses CVSS 9.1 vulnerability by enforcing MFA for all users.
//!
//! Features:
//! - TOTP (Time-Based One-Time Password) implementation (RFC 6238)
//! - QR code generation for authenticator app enrollment
//! - Backup codes for account recovery (10 codes, one-time use)
//! - Encrypted secret storage with secure key management
//! - Rate limiting and account lockout protection
//! - Comprehensive audit logging
//!
//! Security Standards:
//! - RFC 6238: TOTP Algorithm
//! - NIST SP 800-63B: Digital Identity Guidelines
//! - PCI DSS: Multi-factor authentication requirements
//! - SOX: Access control and authentication

pub mod backup_codes;
pub mod enrollment;
pub mod qr_code;
pub mod totp;
pub mod verification;

pub use backup_codes::{BackupCode, BackupCodeGenerator, BackupCodeValidator};
pub use enrollment::{EnrollmentError, EnrollmentSession, MfaEnrollment};
pub use qr_code::{QrCodeError, QrCodeGenerator};
pub use totp::{TotpConfig, TotpGenerator, TotpVerifier};
pub use verification::{MfaVerification, VerificationError, VerificationResult};

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::sync::Arc;
use tracing::{error, info, warn};
use uuid::Uuid;

// Re-export Secret types from secrecy crate
pub use secrecy::SecretString;

/// MFA configuration for a user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaConfig {
    pub id: Uuid,
    pub user_id: Uuid,
    pub is_enabled: bool,
    pub is_verified: bool,
    pub enrolled_at: Option<DateTime<Utc>>,
    pub verified_at: Option<DateTime<Utc>>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub backup_codes_remaining: i32,
    pub failed_verification_attempts: i32,
    pub last_failed_attempt_at: Option<DateTime<Utc>>,
    pub locked_until: Option<DateTime<Utc>>,
}

/// MFA method types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MfaMethod {
    /// Time-based One-Time Password (TOTP)
    Totp,
    /// Backup recovery code
    BackupCode,
    /// Trusted device (future enhancement)
    TrustedDevice,
}

impl std::fmt::Display for MfaMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MfaMethod::Totp => write!(f, "totp"),
            MfaMethod::BackupCode => write!(f, "backup_code"),
            MfaMethod::TrustedDevice => write!(f, "trusted_device"),
        }
    }
}

/// MFA manager - central coordinator for all MFA operations
#[derive(Clone)]
pub struct MfaManager {
    db_pool: Arc<PgPool>,
    totp_generator: Arc<TotpGenerator>,
    totp_verifier: Arc<TotpVerifier>,
    backup_code_generator: Arc<BackupCodeGenerator>,
    backup_code_validator: Arc<BackupCodeValidator>,
    qr_code_generator: Arc<QrCodeGenerator>,
}

impl MfaManager {
    /// Create new MFA manager
    pub fn new(db_pool: PgPool, _encryption_key: String) -> Result<Self> {
        let db_pool = Arc::new(db_pool);

        let totp_generator = Arc::new(TotpGenerator::new());
        let totp_verifier = Arc::new(TotpVerifier::new());
        let backup_code_generator = Arc::new(BackupCodeGenerator::new());
        let backup_code_validator = Arc::new(BackupCodeValidator::new(Arc::clone(&db_pool)));
        let qr_code_generator = Arc::new(QrCodeGenerator::new());

        Ok(Self {
            db_pool,
            totp_generator,
            totp_verifier,
            backup_code_generator,
            backup_code_validator,
            qr_code_generator,
        })
    }

    /// Check if MFA is required for a user
    pub async fn is_mfa_required(&self, user_id: Uuid) -> Result<bool> {
        let result = sqlx::query_scalar::<_, bool>("SELECT is_mfa_required($1)")
            .bind(user_id)
            .fetch_one(&*self.db_pool)
            .await
            .context("Failed to check if MFA is required")?;

        Ok(result)
    }

    /// Get MFA configuration for a user
    pub async fn get_mfa_config(&self, user_id: Uuid) -> Result<Option<MfaConfig>> {
        use sqlx::Row;

        let result = sqlx::query(
            r#"
            SELECT
                id, user_id, is_enabled, is_verified,
                enrolled_at, verified_at, last_used_at,
                backup_codes_remaining, failed_verification_attempts,
                last_failed_attempt_at, locked_until
            FROM mfa_config
            WHERE user_id = $1
            "#,
        )
        .bind(user_id)
        .fetch_optional(&*self.db_pool)
        .await
        .context("Failed to fetch MFA config")?;

        Ok(result.map(|r| MfaConfig {
            id: r.get("id"),
            user_id: r.get("user_id"),
            is_enabled: r.get("is_enabled"),
            is_verified: r.get("is_verified"),
            enrolled_at: r.get("enrolled_at"),
            verified_at: r.get("verified_at"),
            last_used_at: r.get("last_used_at"),
            backup_codes_remaining: r.get("backup_codes_remaining"),
            failed_verification_attempts: r.get("failed_verification_attempts"),
            last_failed_attempt_at: r.get("last_failed_attempt_at"),
            locked_until: r.get("locked_until"),
        }))
    }

    /// Check if user's MFA is locked due to failed attempts
    pub async fn is_mfa_locked(&self, user_id: Uuid) -> Result<bool> {
        if let Some(config) = self.get_mfa_config(user_id).await? {
            if let Some(locked_until) = config.locked_until {
                return Ok(locked_until > Utc::now());
            }
        }
        Ok(false)
    }

    /// Start MFA enrollment for a user
    pub async fn start_enrollment(
        &self,
        user_id: Uuid,
        issuer: &str,
        account_name: &str,
    ) -> Result<EnrollmentSession> {
        info!("Starting MFA enrollment for user: {}", user_id);

        // Generate TOTP secret
        let secret = self.totp_generator.generate_secret()?;

        // Create QR code data
        let qr_uri = self
            .totp_generator
            .generate_qr_uri(&secret, issuer, account_name)?;

        // Encrypt the secret for database storage using pgcrypto
        let encrypted_secret = self.encrypt_totp_secret(secret.expose_secret()).await?;

        // Generate QR code image
        let qr_code_png = self.qr_code_generator.generate_png(&qr_uri)?;

        // Create enrollment session
        let session_id = Uuid::new_v4();
        let expires_at = Utc::now() + chrono::Duration::minutes(15);

        sqlx::query(
            r#"
            INSERT INTO mfa_enrollment_sessions (
                id, user_id, temp_totp_secret_encrypted, qr_code_data,
                is_active, expires_at
            ) VALUES ($1, $2, $3, $4, true, $5)
            "#,
        )
        .bind(session_id)
        .bind(user_id)
        .bind(encrypted_secret)
        .bind(&qr_uri)
        .bind(expires_at)
        .execute(&*self.db_pool)
        .await
        .context("Failed to create enrollment session")?;

        info!("MFA enrollment session created: {}", session_id);

        Ok(EnrollmentSession {
            session_id,
            user_id,
            qr_code_uri: qr_uri,
            qr_code_png,
            manual_entry_key: secret.expose_secret().to_string(),
            expires_at,
        })
    }

    /// Complete MFA enrollment by verifying first TOTP code
    pub async fn complete_enrollment(
        &self,
        session_id: Uuid,
        user_id: Uuid,
        totp_code: &str,
    ) -> Result<Vec<BackupCode>> {
        info!("Completing MFA enrollment for user: {}", user_id);

        // Get enrollment session
        use sqlx::Row;

        let session = sqlx::query(
            r#"
            SELECT
                temp_totp_secret_encrypted,
                is_active,
                expires_at,
                verification_attempts
            FROM mfa_enrollment_sessions
            WHERE id = $1 AND user_id = $2
            "#,
        )
        .bind(session_id)
        .bind(user_id)
        .fetch_optional(&*self.db_pool)
        .await
        .context("Failed to fetch enrollment session")?
        .ok_or_else(|| anyhow::anyhow!("Enrollment session not found"))?;

        // Extract session fields
        let temp_totp_secret_encrypted: Vec<u8> = session.get("temp_totp_secret_encrypted");
        let is_active: bool = session.get("is_active");
        let expires_at: DateTime<Utc> = session.get("expires_at");
        let verification_attempts: i32 = session.get("verification_attempts");

        // Validate session
        if !is_active {
            return Err(anyhow::anyhow!("Enrollment session is not active"));
        }
        if expires_at < Utc::now() {
            return Err(anyhow::anyhow!("Enrollment session has expired"));
        }
        if verification_attempts >= 3 {
            return Err(anyhow::anyhow!("Maximum verification attempts exceeded"));
        }

        // Decrypt secret and verify TOTP code using pgcrypto
        let secret = self
            .decrypt_totp_secret(&temp_totp_secret_encrypted)
            .await?;
        let is_valid = self.totp_verifier.verify(&secret, totp_code, 1)?;

        if !is_valid {
            // Increment verification attempts
            sqlx::query(
                "UPDATE mfa_enrollment_sessions SET verification_attempts = verification_attempts + 1 WHERE id = $1"
            )
            .bind(session_id)
            .execute(&*self.db_pool)
            .await?;

            return Err(anyhow::anyhow!("Invalid TOTP code"));
        }

        // Generate backup codes
        let backup_codes = self.backup_code_generator.generate_codes(10)?;

        // Create MFA config
        let config_id = Uuid::new_v4();
        let now = Utc::now();

        sqlx::query(
            r#"
            INSERT INTO mfa_config (
                id, user_id, totp_secret_encrypted, totp_algorithm, totp_digits, totp_period,
                is_enabled, is_verified, enrolled_at, verified_at, backup_codes_remaining
            ) VALUES ($1, $2, $3, 'SHA1', 6, 30, true, true, $4, $4, 10)
            ON CONFLICT (user_id) DO UPDATE SET
                totp_secret_encrypted = EXCLUDED.totp_secret_encrypted,
                is_enabled = true,
                is_verified = true,
                verified_at = $4,
                backup_codes_remaining = 10
            "#,
        )
        .bind(config_id)
        .bind(user_id)
        .bind(&temp_totp_secret_encrypted)
        .bind(now)
        .execute(&*self.db_pool)
        .await
        .context("Failed to create MFA config")?;

        // Store backup codes
        self.store_backup_codes(user_id, &backup_codes).await?;

        // Mark enrollment session as completed
        sqlx::query(
            "UPDATE mfa_enrollment_sessions SET is_active = false, completed_at = NOW() WHERE id = $1"
        )
        .bind(session_id)
        .execute(&*self.db_pool)
        .await?;

        info!(
            "MFA enrollment completed successfully for user: {}",
            user_id
        );

        Ok(backup_codes)
    }

    /// Verify TOTP code for authentication
    pub async fn verify_totp(
        &self,
        user_id: Uuid,
        totp_code: &str,
        ip_address: Option<String>,
    ) -> Result<bool> {
        // Check if account is locked
        if self.is_mfa_locked(user_id).await? {
            error!("MFA verification attempted for locked account: {}", user_id);
            return Err(anyhow::anyhow!(
                "Account is locked due to too many failed attempts"
            ));
        }

        // Get MFA config
        let config = self
            .get_mfa_config(user_id)
            .await?
            .ok_or_else(|| anyhow::anyhow!("MFA not configured for user"))?;

        if !config.is_enabled {
            return Err(anyhow::anyhow!("MFA is not enabled for user"));
        }

        // Get encrypted secret
        let encrypted_secret = sqlx::query_scalar::<_, Vec<u8>>(
            "SELECT totp_secret_encrypted FROM mfa_config WHERE user_id = $1",
        )
        .bind(user_id)
        .fetch_one(&*self.db_pool)
        .await
        .context("Failed to fetch TOTP secret")?;

        // Decrypt and verify using pgcrypto
        let secret = self.decrypt_totp_secret(&encrypted_secret).await?;
        let is_valid = self.totp_verifier.verify(&secret, totp_code, 1)?;

        // Record attempt
        self.record_verification_attempt(
            user_id,
            MfaMethod::Totp,
            is_valid,
            ip_address,
            None,
            if is_valid {
                None
            } else {
                Some("INVALID_TOTP_CODE".to_string())
            },
        )
        .await?;

        Ok(is_valid)
    }

    /// Verify backup code for authentication
    pub async fn verify_backup_code(
        &self,
        user_id: Uuid,
        backup_code: &str,
        ip_address: Option<String>,
    ) -> Result<bool> {
        let is_valid = self
            .backup_code_validator
            .validate(user_id, backup_code, ip_address.as_deref())
            .await?;

        Ok(is_valid)
    }

    /// Record MFA verification attempt
    async fn record_verification_attempt(
        &self,
        user_id: Uuid,
        method: MfaMethod,
        success: bool,
        ip_address: Option<String>,
        user_agent: Option<String>,
        error_code: Option<String>,
    ) -> Result<Uuid> {
        let ip: Option<std::net::IpAddr> = ip_address.as_ref().and_then(|s| s.parse().ok());

        let log_id = sqlx::query_scalar::<_, Uuid>(
            r#"
            SELECT record_mfa_attempt($1, $2, $3, $4, $5, NULL, $6)
            "#,
        )
        .bind(user_id)
        .bind(method.to_string())
        .bind(success)
        .bind(ip.map(|addr| addr.to_string()))
        .bind(user_agent)
        .bind(error_code)
        .fetch_one(&*self.db_pool)
        .await
        .context("Failed to record MFA attempt")?;

        Ok(log_id)
    }

    /// Store backup codes in database
    async fn store_backup_codes(&self, user_id: Uuid, codes: &[BackupCode]) -> Result<()> {
        let expires_at = Utc::now() + chrono::Duration::days(365);

        for code in codes {
            let code_id = Uuid::new_v4();
            let code_hash = self.hash_backup_code(code.code.expose_secret());

            sqlx::query(
                r#"
                INSERT INTO mfa_backup_codes (
                    id, user_id, code_hash, code_hint, expires_at
                ) VALUES ($1, $2, $3, $4, $5)
                "#,
            )
            .bind(code_id)
            .bind(user_id)
            .bind(&code_hash)
            .bind(&code.hint)
            .bind(expires_at)
            .execute(&*self.db_pool)
            .await
            .context("Failed to store backup code")?;
        }

        Ok(())
    }

    /// Encrypt TOTP secret for storage using Postgre`SQL` pgcrypto
    ///
    /// Wave 121 Agent 1: Implemented proper encryption using pgcrypto AES-256-CBC
    ///
    /// Resolves CRITICAL security blocker (plaintext TOTP secrets)
    async fn encrypt_totp_secret(&self, secret: &str) -> Result<Vec<u8>> {
        let encrypted: Vec<u8> = sqlx::query_scalar("SELECT encrypt_mfa_secret($1)")
            .bind(secret)
            .fetch_one(&*self.db_pool)
            .await
            .context("Failed to encrypt TOTP secret using pgcrypto")?;

        Ok(encrypted)
    }

    /// Decrypt TOTP secret from storage using Postgre`SQL` pgcrypto
    ///
    /// Wave 121 Agent 1: Implemented proper decryption using pgcrypto AES-256-CBC
    ///
    /// Resolves CRITICAL security blocker (plaintext TOTP secrets)
    async fn decrypt_totp_secret(&self, encrypted: &[u8]) -> Result<String> {
        let decrypted: String = sqlx::query_scalar("SELECT decrypt_mfa_secret($1)")
            .bind(encrypted)
            .fetch_one(&*self.db_pool)
            .await
            .context("Failed to decrypt TOTP secret using pgcrypto")?;

        Ok(decrypted)
    }

    /// Hash backup code for secure storage
    fn hash_backup_code(&self, code: &str) -> String {
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::new();
        hasher.update(code.as_bytes());
        format!("{:x}", hasher.finalize())
    }

    /// Disable MFA for a user (admin only)
    pub async fn disable_mfa(&self, user_id: Uuid) -> Result<()> {
        warn!(
            "Disabling MFA for user: {} - This should only be done by administrators",
            user_id
        );

        sqlx::query(
            "UPDATE mfa_config SET is_enabled = false, updated_at = NOW() WHERE user_id = $1",
        )
        .bind(user_id)
        .execute(&*self.db_pool)
        .await
        .context("Failed to disable MFA")?;

        // Invalidate all backup codes
        sqlx::query(
            "UPDATE mfa_backup_codes SET is_used = true WHERE user_id = $1 AND is_used = false",
        )
        .bind(user_id)
        .execute(&*self.db_pool)
        .await?;

        Ok(())
    }

    /// Get backup codes status for a user
    pub async fn get_backup_codes_status(&self, user_id: Uuid) -> Result<BackupCodesStatus> {
        use sqlx::Row;

        let result = sqlx::query(
            r#"
            SELECT
                COUNT(*) FILTER (WHERE is_used = false) as remaining,
                COUNT(*) FILTER (WHERE is_used = true) as used,
                MIN(expires_at) FILTER (WHERE is_used = false) as earliest_expiry
            FROM mfa_backup_codes
            WHERE user_id = $1
            "#,
        )
        .bind(user_id)
        .fetch_one(&*self.db_pool)
        .await
        .context("Failed to fetch backup codes status")?;

        Ok(BackupCodesStatus {
            remaining: u32::try_from(result.get::<i64, _>("remaining"))
                .map_err(|_| anyhow::anyhow!("Remaining backup codes count exceeds u32 range"))?,
            used: u32::try_from(result.get::<i64, _>("used"))
                .map_err(|_| anyhow::anyhow!("Used backup codes count exceeds u32 range"))?,
            earliest_expiry: result.get("earliest_expiry"),
        })
    }
}

/// Backup codes status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupCodesStatus {
    pub remaining: u32,
    pub used: u32,
    pub earliest_expiry: Option<DateTime<Utc>>,
}

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

    #[test]
    fn test_mfa_method_display() {
        assert_eq!(MfaMethod::Totp.to_string(), "totp");
        assert_eq!(MfaMethod::BackupCode.to_string(), "backup_code");
        assert_eq!(MfaMethod::TrustedDevice.to_string(), "trusted_device");
    }
}
