//! TOTP 2FA — RFC 6238 Time-based One-Time Passwords (HMAC-SHA1, 6 digits, 30-second step).
//!
//! Compatible with standard authenticator apps (Google Authenticator, Authy, etc.).
//!
//! # Flow
//!
//! 1. **Setup** — generate a secret, store it in `users.totp_secret`.
//!    Return `TotpSetup` to the client (contains the QR URI).
//! 2. **Confirm** — user scans QR and submits a code: call [`verify`].
//!    On success, set `users.totp_enabled = true`.
//! 3. **Login** — after credential check: if `totp_enabled`, return a short-lived
//!    "pending" JWT with role `totp_pending`.  Client hits `POST /auth/login/totp`
//!    with the pending token + TOTP code; server issues the real token pair.
//!
//! # Database
//!
//! See `migrations/005_totp.sql`.

use hmac::{Hmac, Mac};
use sha1::Sha1;

type HmacSha1 = Hmac<Sha1>;
const STEP: u64 = 30;
const DIGITS: u32 = 6;
const WINDOW: u64 = 1; // ±1 step tolerance (~30 s clock drift)

/// Details returned to the client during TOTP setup.
pub struct TotpSetup {
    /// Base32-encoded secret — stored in DB, displayed as QR or plaintext.
    pub secret: String,
    /// `otpauth://` URI ready for a QR code library.
    pub uri: String,
}

/// Generate a new TOTP secret and build the `otpauth://` provisioning URI.
pub fn setup(email: &str, issuer: &str) -> TotpSetup {
    let secret = new_secret();
    let uri = build_uri(&secret, email, issuer);
    TotpSetup { secret, uri }
}

/// Generate a cryptographically random 20-byte secret, base32-encoded.
pub fn new_secret() -> String {
    use rand::RngCore;
    let mut bytes = [0u8; 20];
    rand::thread_rng().fill_bytes(&mut bytes);
    base32_encode(&bytes)
}

/// Verify a 6-digit `code` against `secret_b32` for `timestamp_secs`.
///
/// Accepts codes from one step before and after the current window to allow
/// for clock drift.
pub fn verify(secret_b32: &str, code: u32, timestamp_secs: u64) -> bool {
    let counter = timestamp_secs / STEP;
    for window in counter.saturating_sub(WINDOW)..=counter + WINDOW {
        if hotp_code(secret_b32, window) == Some(code) {
            return true;
        }
    }
    false
}

/// Compute the current TOTP code for debugging / pre-seeding tests.
pub fn current_code(secret_b32: &str) -> Option<u32> {
    use std::time::{SystemTime, UNIX_EPOCH};
    let ts = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    hotp_code(secret_b32, ts / STEP)
}

/// `otpauth://totp/<issuer>:<email>?secret=...&issuer=...`
pub fn build_uri(secret_b32: &str, email: &str, issuer: &str) -> String {
    let label = format!("{issuer}:{email}");
    let label_enc = url_encode(&label);
    let issuer_enc = url_encode(issuer);
    format!(
        "otpauth://totp/{label_enc}?secret={secret_b32}&issuer={issuer_enc}&algorithm=SHA1&digits=6&period=30"
    )
}

// ── HOTP/TOTP internals ───────────────────────────────────────────────────────

fn hotp_code(secret_b32: &str, counter: u64) -> Option<u32> {
    let secret = base32_decode(secret_b32)?;
    let mut mac = HmacSha1::new_from_slice(&secret).expect("HMAC-SHA1 accepts any key length");
    mac.update(&counter.to_be_bytes());
    let result = mac.finalize().into_bytes();

    // Dynamic truncation (RFC 4226 §5.4)
    let offset = (result[19] & 0x0f) as usize;
    let code = ((result[offset] & 0x7f) as u32) << 24
        | (result[offset + 1] as u32) << 16
        | (result[offset + 2] as u32) << 8
        | result[offset + 3] as u32;
    Some(code % 10_u32.pow(DIGITS))
}

// ── Base32 encode/decode (RFC 4648, no padding) ───────────────────────────────

const BASE32_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

fn base32_encode(input: &[u8]) -> String {
    let mut out = String::new();
    let mut buf: u64 = 0;
    let mut bits: u32 = 0;
    for &byte in input {
        buf = (buf << 8) | byte as u64;
        bits += 8;
        while bits >= 5 {
            bits -= 5;
            out.push(BASE32_ALPHABET[((buf >> bits) & 0x1f) as usize] as char);
        }
    }
    if bits > 0 {
        out.push(BASE32_ALPHABET[((buf << (5 - bits)) & 0x1f) as usize] as char);
    }
    out
}

fn base32_decode(input: &str) -> Option<Vec<u8>> {
    let mut out = Vec::new();
    let mut buf: u64 = 0;
    let mut bits: u32 = 0;
    for c in input.chars() {
        if c == '=' {
            break;
        } // strip optional padding
        let val = BASE32_ALPHABET
            .iter()
            .position(|&a| a == c.to_ascii_uppercase() as u8)? as u64;
        buf = (buf << 5) | val;
        bits += 5;
        if bits >= 8 {
            bits -= 8;
            out.push((buf >> bits) as u8);
        }
    }
    Some(out)
}

// ── minimal URL encoder ───────────────────────────────────────────────────────

fn url_encode(s: &str) -> String {
    s.chars()
        .flat_map(|c| match c {
            'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
                vec![c]
            }
            _ => {
                let mut buf = [0u8; 4];
                c.encode_utf8(&mut buf)
                    .bytes()
                    .flat_map(|b| {
                        let hex = format!("%{:02X}", b);
                        hex.chars().collect::<Vec<_>>()
                    })
                    .collect()
            }
        })
        .collect()
}

// ── DB helpers ────────────────────────────────────────────────────────────────

use sqlx::PgPool;

/// Store the TOTP secret for a user (setup step, not yet enabled).
pub async fn store_secret(pool: &PgPool, user_id: i64, secret: &str) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE users SET totp_secret = $1, totp_enabled = FALSE WHERE id = $2")
        .bind(secret)
        .bind(user_id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Activate TOTP for a user — call after successful first verification.
pub async fn enable(pool: &PgPool, user_id: i64) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE users SET totp_enabled = TRUE WHERE id = $1")
        .bind(user_id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Disable TOTP for a user.
pub async fn disable(pool: &PgPool, user_id: i64) -> Result<(), sqlx::Error> {
    sqlx::query("UPDATE users SET totp_secret = NULL, totp_enabled = FALSE WHERE id = $1")
        .bind(user_id)
        .execute(pool)
        .await?;
    Ok(())
}

/// Fetch the TOTP secret and enabled flag for a user.
pub async fn fetch(pool: &PgPool, user_id: i64) -> Result<Option<(String, bool)>, sqlx::Error> {
    sqlx::query_as::<_, (Option<String>, bool)>(
        "SELECT totp_secret, totp_enabled FROM users WHERE id = $1",
    )
    .bind(user_id)
    .fetch_optional(pool)
    .await
    .map(|opt| opt.and_then(|(secret, enabled)| secret.map(|s| (s, enabled))))
}

// ── tests ─────────────────────────────────────────────────────────────────────

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

    #[test]
    fn round_trip_base32() {
        let original = b"Hello, TOTP!";
        let encoded = base32_encode(original);
        let decoded = base32_decode(&encoded).unwrap();
        assert_eq!(decoded, original);
    }

    #[test]
    fn verify_accepts_current_code() {
        let secret = new_secret();
        let ts = 1_700_000_000u64; // arbitrary fixed timestamp
        let code = hotp_code(&secret, ts / STEP).unwrap();
        assert!(verify(&secret, code, ts));
    }

    #[test]
    fn verify_rejects_wrong_code() {
        let secret = new_secret();
        let ts = 1_700_000_000u64;
        let code = hotp_code(&secret, ts / STEP).unwrap();
        assert!(!verify(&secret, (code + 1) % 1_000_000, ts));
    }

    #[test]
    fn build_uri_contains_secret_and_issuer() {
        let uri = build_uri("SECRETKEY", "user@example.com", "MyApp");
        assert!(uri.starts_with("otpauth://totp/"));
        assert!(uri.contains("SECRETKEY"));
        assert!(uri.contains("MyApp"));
    }
}
