//! TOTP (Time-Based One-Time Password) Implementation
//!
//! Implements RFC 6238 TOTP algorithm for multi-factor authentication.
//! Uses HMAC-based One-Time Password (HOTP) as defined in RFC 4226.

use anyhow::Result;
use base32::Alphabet;
use chrono::Utc;
use hmac::{Hmac, Mac};
use rand::Rng;
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use tracing::warn;

// Use secrecy::Secret from workspace dependencies
use secrecy::{ExposeSecret, SecretString};

type HmacSha1 = Hmac<Sha1>;

/// TOTP configuration parameters (RFC 6238)
#[derive(Debug, Clone, Serialize)]
pub struct TotpConfig {
    /// Secret key (Base32 encoded)
    #[serde(skip)]
    pub secret: SecretString,
    /// Number of digits in TOTP code (6 or 8)
    pub digits: u32,
    /// Time step in seconds (usually 30)
    pub period: u64,
    /// Algorithm (SHA1, SHA256, SHA512)
    pub algorithm: TotpAlgorithm,
}

impl Default for TotpConfig {
    fn default() -> Self {
        Self {
            secret: SecretString::new(String::new()),
            digits: 6_u32,
            period: 30_u64,
            algorithm: TotpAlgorithm::SHA1,
        }
    }
}

/// TOTP algorithm types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TotpAlgorithm {
    SHA1,
    SHA256,
    SHA512,
}

impl std::fmt::Display for TotpAlgorithm {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TotpAlgorithm::SHA1 => write!(f, "SHA1"),
            TotpAlgorithm::SHA256 => write!(f, "SHA256"),
            TotpAlgorithm::SHA512 => write!(f, "SHA512"),
        }
    }
}

/// TOTP generator for creating secrets and QR codes
pub struct TotpGenerator {
    config: TotpConfig,
}

impl TotpGenerator {
    /// Create new TOTP generator with default configuration
    pub fn new() -> Self {
        Self {
            config: TotpConfig::default(),
        }
    }

    /// Generate a random TOTP secret (Base32 encoded, 160 bits)
    pub fn generate_secret(&self) -> Result<SecretString> {
        let mut rng = rand::thread_rng();
        let mut secret_bytes = [0u8; 20_usize]; // 160 bits for SHA1
        rng.fill(&mut secret_bytes);

        // Encode to Base32 (RFC 4648)
        let secret_base32 = base32::encode(Alphabet::Rfc4648 { padding: false }, &secret_bytes);

        Ok(SecretString::new(secret_base32))
    }

    /// Generate QR code URI for authenticator apps (otpauth:// format)
    pub fn generate_qr_uri(
        &self,
        secret: &SecretString,
        issuer: &str,
        account_name: &str,
    ) -> Result<String> {
        // Format: otpauth://totp/ISSUER:ACCOUNT?secret=SECRET&issuer=ISSUER&algorithm=SHA1&digits=6&period=30
        let uri = format!(
            "otpauth://totp/{}:{}?secret={}&issuer={}&algorithm={}&digits={}&period={}",
            urlencoding::encode(issuer),
            urlencoding::encode(account_name),
            secret.expose_secret(),
            urlencoding::encode(issuer),
            self.config.algorithm,
            self.config.digits,
            self.config.period
        );

        Ok(uri)
    }

    /// Generate TOTP code for current time (for testing purposes only)
    pub fn generate_code(&self, secret: &str) -> Result<String> {
        let time = u64::try_from(Utc::now().timestamp())
            .map_err(|_| anyhow::anyhow!("Negative timestamp cannot be converted to u64"))?;

        self.generate_code_at_time(secret, time)
    }

    /// Generate TOTP code for specific timestamp
    pub fn generate_code_at_time(&self, secret: &str, time: u64) -> Result<String> {
        let counter = time / self.config.period;
        self.generate_hotp(secret, counter)
    }

    /// Generate HOTP code (RFC 4226)
    fn generate_hotp(&self, secret: &str, counter: u64) -> Result<String> {
        // Validate secret is not empty
        if secret.is_empty() {
            return Err(anyhow::anyhow!("Secret cannot be empty"));
        }

        // Decode Base32 secret
        let secret_bytes = base32::decode(Alphabet::Rfc4648 { padding: false }, secret)
            .ok_or_else(|| anyhow::anyhow!("Invalid Base32 secret"))?;

        // Ensure decoded bytes are not empty
        if secret_bytes.is_empty() {
            return Err(anyhow::anyhow!("Decoded secret cannot be empty"));
        }

        // Create HMAC-SHA1
        let mut mac = HmacSha1::new_from_slice(&secret_bytes)
            .map_err(|e| anyhow::anyhow!("HMAC initialization failed: {}", e))?;

        // Counter as 8-byte big-endian
        let counter_bytes = counter.to_be_bytes();
        mac.update(&counter_bytes);

        // Compute HMAC
        let result = mac.finalize();
        let hmac_result = result.into_bytes();

        // Dynamic truncation (RFC 4226 section 5.3)
        let offset = usize::from(hmac_result[19_usize] & 0x0f);
        let truncated_hash = u32::from_be_bytes([
            hmac_result[offset] & 0x7f,
            hmac_result[offset + 1_usize],
            hmac_result[offset + 2_usize],
            hmac_result[offset + 3_usize],
        ]);

        // Generate code with specified digits
        let code = truncated_hash % 10u32.pow(self.config.digits);
        let code_str = format!(
            "{:0width$}",
            code,
            width = usize::try_from(self.config.digits).unwrap_or(6)
        );

        Ok(code_str)
    }
}

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

/// TOTP verifier for validating codes
pub struct TotpVerifier {
    config: TotpConfig,
}

impl TotpVerifier {
    /// Create new TOTP verifier with default configuration
    pub fn new() -> Self {
        Self {
            config: TotpConfig::default(),
        }
    }

    /// Verify TOTP code with time drift tolerance
    ///
    /// # Arguments
    /// * `secret` - Base32-encoded secret
    ///
    /// * `code` - TOTP code to verify
    /// * `drift_tolerance` - Number of time steps to check before/after current time (0-2 recommended)
    pub fn verify(&self, secret: &str, code: &str, drift_tolerance: u64) -> Result<bool> {
        let current_time = u64::try_from(Utc::now().timestamp())
            .map_err(|_| anyhow::anyhow!("Negative timestamp cannot be converted to u64"))?;

        self.verify_at_time(secret, code, current_time, drift_tolerance)
    }

    /// Verify TOTP code at specific time with drift tolerance
    pub fn verify_at_time(
        &self,
        secret: &str,
        code: &str,
        time: u64,
        drift_tolerance: u64,
    ) -> Result<bool> {
        // Validate code format
        if code.len() != usize::try_from(self.config.digits).unwrap_or(6) {
            return Ok(false);
        }

        if !code.chars().all(|c| c.is_ascii_digit()) {
            return Ok(false);
        }

        let generator = TotpGenerator::new();
        let current_counter = time / self.config.period;

        // Check current time and drift windows
        for offset in 0..=drift_tolerance {
            // Check current + offset
            if offset > 0 {
                let forward_counter = current_counter + offset;
                let forward_code = generator.generate_hotp(secret, forward_counter)?;
                if constant_time_compare(code, &forward_code) {
                    return Ok(true);
                }
            }

            // Check current - offset
            if offset > 0 && current_counter >= offset {
                let backward_counter = current_counter - offset;
                let backward_code = generator.generate_hotp(secret, backward_counter)?;
                if constant_time_compare(code, &backward_code) {
                    return Ok(true);
                }
            }

            // Check exact current counter (offset == 0)
            if offset == 0 {
                let current_code = generator.generate_hotp(secret, current_counter)?;
                if constant_time_compare(code, &current_code) {
                    return Ok(true);
                }
            }
        }

        Ok(false)
    }

    /// Get current time step counter
    pub fn current_counter(&self) -> u64 {
        let current_time = u64::try_from(Utc::now().timestamp()).unwrap_or_else(|_| {
            warn!("Negative timestamp, using 0");
            0
        });
        current_time / self.config.period
    }

    /// Get time remaining in current period (seconds)
    pub fn time_remaining(&self) -> u64 {
        let current_time = u64::try_from(Utc::now().timestamp()).unwrap_or_else(|_| {
            warn!("Negative timestamp, using 0");
            0
        });
        self.config.period - (current_time % self.config.period)
    }
}

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

/// Constant-time string comparison to prevent timing attacks
fn constant_time_compare(a: &str, b: &str) -> bool {
    // Reject empty strings immediately (security: empty codes should never validate)
    if a.is_empty() || b.is_empty() {
        return false;
    }

    if a.len() != b.len() {
        return false;
    }

    let mut result = 0u8;
    for (x, y) in a.bytes().zip(b.bytes()) {
        result |= x ^ y;
    }

    result == 0
}

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

    #[test]
    fn test_generate_secret() {
        let generator = TotpGenerator::new();
        let secret = generator.generate_secret().unwrap();

        // Base32 encoded secret should be non-empty
        assert!(!secret.expose_secret().is_empty());

        // Should be valid Base32
        assert!(
            base32::decode(Alphabet::Rfc4648 { padding: false }, secret.expose_secret()).is_some()
        );
    }

    #[test]
    fn test_generate_qr_uri() {
        let generator = TotpGenerator::new();
        let secret = SecretString::new("JBSWY3DPEHPK3PXP".into());
        let uri = generator
            .generate_qr_uri(&secret, "FoxhuntHFT", "user@example.com")
            .unwrap();

        assert!(uri.starts_with("otpauth://totp/"));
        assert!(uri.contains("secret=JBSWY3DPEHPK3PXP"));
        assert!(uri.contains("issuer=FoxhuntHFT"));
        assert!(uri.contains("digits=6"));
        assert!(uri.contains("period=30"));
    }

    #[test]
    fn test_generate_and_verify_totp() {
        let generator = TotpGenerator::new();
        let verifier = TotpVerifier::new();

        let secret = "JBSWY3DPEHPK3PXP"; // Test secret
        let current_time = 1234567890u64;

        // Generate code
        let code = generator
            .generate_code_at_time(secret, current_time)
            .unwrap();
        assert_eq!(code.len(), 6);

        // Verify code
        assert!(verifier
            .verify_at_time(secret, &code, current_time, 1)
            .unwrap());

        // Verify invalid code
        assert!(!verifier
            .verify_at_time(secret, "000000", current_time, 1)
            .unwrap());
    }

    #[test]
    fn test_totp_drift_tolerance() {
        let generator = TotpGenerator::new();
        let verifier = TotpVerifier::new();

        let secret = "JBSWY3DPEHPK3PXP";
        let current_time = 1234567890u64;
        let period = 30u64;

        // Generate code for current time
        let code = generator
            .generate_code_at_time(secret, current_time)
            .unwrap();

        // Should verify within drift tolerance (±1 period)
        assert!(verifier
            .verify_at_time(secret, &code, current_time + period, 1)
            .unwrap());
        assert!(verifier
            .verify_at_time(secret, &code, current_time - period, 1)
            .unwrap());

        // Should fail outside drift tolerance
        assert!(!verifier
            .verify_at_time(secret, &code, current_time + period * 2_u64, 1)
            .unwrap());
    }

    #[test]
    fn test_constant_time_compare() {
        assert!(constant_time_compare("123456", "123456"));
        assert!(!constant_time_compare("123456", "123457"));
        assert!(!constant_time_compare("123456", "12345"));

        // Security test: Empty strings should NEVER validate
        assert!(!constant_time_compare("", ""));
        assert!(!constant_time_compare("", "123456"));
        assert!(!constant_time_compare("123456", ""));
    }

    #[test]
    fn test_verifier_time_remaining() {
        let verifier = TotpVerifier::new();
        let remaining = verifier.time_remaining();

        // Should be between 0 and 30 seconds
        assert!(remaining > 0 && remaining <= 30);
    }

    #[test]
    fn test_invalid_totp_code_format() {
        let verifier = TotpVerifier::new();
        let secret = "JBSWY3DPEHPK3PXP";
        let current_time = 1234567890u64;

        // Wrong length
        assert!(!verifier
            .verify_at_time(secret, "12345", current_time, 1)
            .unwrap());
        assert!(!verifier
            .verify_at_time(secret, "1234567", current_time, 1)
            .unwrap());

        // Non-numeric
        assert!(!verifier
            .verify_at_time(secret, "12345a", current_time, 1)
            .unwrap());
        assert!(!verifier
            .verify_at_time(secret, "abcdef", current_time, 1)
            .unwrap());
    }
}
