//! Backup Codes for MFA Recovery
//!
//! Provides one-time use backup codes for account recovery when TOTP is unavailable.
//! Implements secure generation, storage, and validation of backup codes.

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use rand::Rng;
use secrecy::{ExposeSecret, Secret};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::PgPool;
use std::sync::Arc;
use tracing::{debug, error, info};
use uuid::Uuid;

/// Backup code with display format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupCode {
    /// The actual code (should be kept secret)
    #[serde(skip_serializing)]
    pub code: Secret<String>,
    /// First 4 characters as hint for user reference
    pub hint: String,
    /// Display format (e.g., "ABCD-EFGH-IJKL")
    pub display: String,
}

impl BackupCode {
    /// Create new backup code from random string
    fn new(code: String) -> Self {
        let hint = code.chars().take(4).collect();
        let display = format_backup_code(&code);

        Self {
            code: Secret::new(code),
            hint,
            display,
        }
    }

    /// Get the code value (for verification purposes only)
    pub fn expose(&self) -> &str {
        self.code.expose_secret()
    }
}

/// Backup code generator
pub struct BackupCodeGenerator {
    /// Code length (default: 16 characters)
    code_length: usize,
    /// Character set for codes (alphanumeric, excluding ambiguous characters)
    charset: Vec<char>,
}

impl BackupCodeGenerator {
    /// Create new backup code generator
    pub fn new() -> Self {
        // Exclude ambiguous characters: 0, O, 1, I, l
        let charset: Vec<char> = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ".chars().collect();

        Self {
            code_length: 16,
            charset,
        }
    }

    /// Generate specified number of backup codes
    pub fn generate_codes(&self, count: usize) -> Result<Vec<BackupCode>> {
        if count == 0 || count > 20 {
            return Err(anyhow::anyhow!("Code count must be between 1 and 20"));
        }

        let mut codes = Vec::with_capacity(count);
        let mut rng = rand::thread_rng();

        for _ in 0..count {
            let code: String = (0..self.code_length)
                .map(|_| {
                    let idx = rng.gen_range(0..self.charset.len());
                    self.charset[idx]
                })
                .collect();

            codes.push(BackupCode::new(code));
        }

        info!("Generated {} backup codes", count);
        Ok(codes)
    }

    /// Generate single backup code
    pub fn generate_single_code(&self) -> Result<BackupCode> {
        let mut codes = self.generate_codes(1)?;
        codes
            .pop()
            .ok_or_else(|| anyhow::anyhow!("Failed to generate backup code"))
    }
}

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

/// Backup code validator
pub struct BackupCodeValidator {
    db_pool: Arc<PgPool>,
}

impl BackupCodeValidator {
    /// Create new backup code validator
    pub fn new(db_pool: Arc<PgPool>) -> Self {
        Self { db_pool }
    }

    /// Validate backup code for a user
    pub async fn validate(
        &self,
        user_id: Uuid,
        backup_code: &str,
        ip_address: Option<&str>,
    ) -> Result<bool> {
        // Normalize code (remove spaces, hyphens, convert to uppercase)
        let normalized_code = normalize_backup_code(backup_code);

        // Validate format
        if !is_valid_backup_code_format(&normalized_code) {
            debug!("Invalid backup code format");
            return Ok(false);
        }

        let ip: Option<std::net::IpAddr> = ip_address.and_then(|s| s.parse().ok());

        // Convert IpAddr to String for database binding
        let ip_string: Option<String> = ip.map(|addr| addr.to_string());

        // Use database function for validation
        let is_valid = sqlx::query_scalar::<_, bool>("SELECT validate_backup_code($1, $2, $3)")
            .bind(user_id)
            .bind(&normalized_code)
            .bind(ip_string)
            .fetch_one(&*self.db_pool)
            .await
            .context("Failed to validate backup code")?;

        if is_valid {
            info!("Backup code successfully validated for user: {}", user_id);
        } else {
            error!("Invalid backup code attempt for user: {}", user_id);
        }

        Ok(is_valid)
    }

    /// Get remaining backup codes count for a user
    pub async fn get_remaining_count(&self, user_id: Uuid) -> Result<i32> {
        let count = sqlx::query_scalar::<_, i32>(
            r#"
            SELECT COUNT(*)::int
            FROM mfa_backup_codes
            WHERE user_id = $1 AND is_used = false AND expires_at > NOW()
            "#,
        )
        .bind(user_id)
        .fetch_one(&*self.db_pool)
        .await
        .context("Failed to get backup codes count")?;

        Ok(count)
    }

    /// Check if user needs to regenerate backup codes
    pub async fn needs_regeneration(&self, user_id: Uuid) -> Result<bool> {
        let remaining = self.get_remaining_count(user_id).await?;

        // Warn if less than 3 codes remaining
        Ok(remaining < 3)
    }

    /// Get backup code usage history for a user
    pub async fn get_usage_history(&self, user_id: Uuid) -> Result<Vec<BackupCodeUsage>> {
        use sqlx::Row;

        let results = sqlx::query(
            r#"
            SELECT
                id, code_hint as hint, is_used, used_at,
                used_from_ip::text as used_from_ip_str, expires_at, created_at
            FROM mfa_backup_codes
            WHERE user_id = $1
            ORDER BY created_at DESC
            "#,
        )
        .bind(user_id)
        .fetch_all(&*self.db_pool)
        .await
        .context("Failed to fetch backup code usage history")?;

        let history = results
            .into_iter()
            .map(|r| BackupCodeUsage {
                id: r.get("id"),
                hint: r.get("hint"),
                is_used: r.get("is_used"),
                used_at: r.get("used_at"),
                used_from_ip_str: r.get("used_from_ip_str"),
                expires_at: r.get("expires_at"),
                created_at: r.get("created_at"),
            })
            .collect();

        Ok(history)
    }
}

/// Backup code usage record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupCodeUsage {
    pub id: Uuid,
    pub hint: String,
    pub is_used: bool,
    pub used_at: Option<DateTime<Utc>>,
    pub used_from_ip_str: Option<String>,
    pub expires_at: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
}

/// Format backup code for display (e.g., "ABCD-EFGH-IJKL-MNOP")
fn format_backup_code(code: &str) -> String {
    code.chars()
        .collect::<Vec<char>>()
        .chunks(4)
        .map(|chunk| chunk.iter().collect::<String>())
        .collect::<Vec<String>>()
        .join("-")
}

/// Normalize backup code (remove formatting, uppercase)
fn normalize_backup_code(code: &str) -> String {
    code.chars()
        .filter(|c| c.is_alphanumeric())
        .map(|c| c.to_ascii_uppercase())
        .collect()
}

/// Validate backup code format
fn is_valid_backup_code_format(code: &str) -> bool {
    // Must be 16 characters
    if code.len() != 16 {
        return false;
    }

    // Must be alphanumeric uppercase
    code.chars()
        .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
}

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

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

    #[test]
    fn test_generate_backup_codes() {
        let generator = BackupCodeGenerator::new();
        let codes = generator.generate_codes(10).unwrap();

        assert_eq!(codes.len(), 10);

        for code in &codes {
            // Check code length
            assert_eq!(code.code.expose_secret().len(), 16);

            // Check hint
            assert_eq!(code.hint.len(), 4);

            // Check display format (contains hyphens)
            assert!(code.display.contains('-'));
        }
    }

    #[test]
    fn test_generate_codes_invalid_count() {
        let generator = BackupCodeGenerator::new();

        assert!(generator.generate_codes(0).is_err());
        assert!(generator.generate_codes(21).is_err());
    }

    #[test]
    fn test_format_backup_code() {
        let code = "ABCDEFGHIJKLMNOP";
        let formatted = format_backup_code(code);

        assert_eq!(formatted, "ABCD-EFGH-IJKL-MNOP");
    }

    #[test]
    fn test_normalize_backup_code() {
        assert_eq!(
            normalize_backup_code("ABCD-EFGH-IJKL-MNOP"),
            "ABCDEFGHIJKLMNOP"
        );
        assert_eq!(
            normalize_backup_code("abcd efgh ijkl mnop"),
            "ABCDEFGHIJKLMNOP"
        );
        assert_eq!(normalize_backup_code("  A-B-C-D  "), "ABCD");
    }

    #[test]
    fn test_is_valid_backup_code_format() {
        assert!(is_valid_backup_code_format("ABCDEFGH23456789"));
        assert!(is_valid_backup_code_format("2345678923456789"));

        assert!(!is_valid_backup_code_format("ABCDEFGH2345678")); // Too short
        assert!(!is_valid_backup_code_format("ABCDEFGH234567890")); // Too long
        assert!(!is_valid_backup_code_format("abcdefgh23456789")); // Lowercase
        assert!(!is_valid_backup_code_format("ABCDEFGH2345678!")); // Special char
    }

    #[test]
    fn test_hash_backup_code() {
        let code = "ABCDEFGH23456789";
        let hash = hash_backup_code(code);

        // SHA-256 produces 64 hex characters
        assert_eq!(hash.len(), 64);

        // Hash should be deterministic
        assert_eq!(hash_backup_code(code), hash);

        // Different codes should have different hashes
        assert_ne!(hash_backup_code("DIFFERENT23456789"), hash);
    }

    #[test]
    fn test_backup_code_new() {
        let code = BackupCode::new("ABCDEFGH23456789".to_string());

        assert_eq!(code.code.expose_secret(), "ABCDEFGH23456789");
        assert_eq!(code.hint, "ABCD");
        assert_eq!(code.display, "ABCD-EFGH-2345-6789");
    }
}
