//! Login attempt tracking and rate limiting.
//!
//! Tracks failed login attempts per identifier (email or IP) and enforces
//! lockout after configurable thresholds.

use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};

use once_cell::sync::Lazy;

/// Global login rate limiter with defaults: 5 attempts, 1-minute window, 15-minute lockout.
pub static LOGIN_LIMITER: Lazy<LoginAttemptTracker> =
    Lazy::new(|| LoginAttemptTracker::new(5, 60, 900));

/// Tracks failed login attempts per identifier and enforces lockout.
pub struct LoginAttemptTracker {
    attempts: Mutex<HashMap<String, Vec<Instant>>>,
    max_attempts: u32,
    window: Duration,
    lockout: Duration,
    locked: Mutex<HashMap<String, Instant>>,
}

impl LoginAttemptTracker {
    /// Create a new tracker.
    ///
    /// * `max_attempts` — failed attempts before lockout
    /// * `window_secs` — sliding window in seconds
    /// * `lockout_secs` — lockout duration in seconds
    pub fn new(max_attempts: u32, window_secs: u64, lockout_secs: u64) -> Self {
        Self {
            attempts: Mutex::new(HashMap::new()),
            max_attempts,
            window: Duration::from_secs(window_secs),
            lockout: Duration::from_secs(lockout_secs),
            locked: Mutex::new(HashMap::new()),
        }
    }

    /// Record a failed attempt for `key`.
    ///
    /// Returns `true` if the key is now locked out after this attempt.
    pub fn record_failure(&self, key: &str) -> bool {
        let now = Instant::now();
        let mut attempts = self.attempts.lock().unwrap();
        let timestamps = attempts.entry(key.to_string()).or_default();
        timestamps.retain(|t| now.duration_since(*t) < self.window);
        timestamps.push(now);

        if timestamps.len() > self.max_attempts as usize {
            let mut locked = self.locked.lock().unwrap();
            locked.insert(key.to_string(), now);
            true
        } else {
            false
        }
    }

    /// Check if `key` is currently locked out.
    pub fn is_locked(&self, key: &str) -> bool {
        let mut locked = self.locked.lock().unwrap();
        if let Some(until) = locked.get(key) {
            if until.elapsed() < self.lockout {
                return true;
            }
            locked.remove(key);
        }
        false
    }

    /// Clear all recorded data for `key` (e.g. after successful login).
    pub fn clear(&self, key: &str) {
        self.attempts.lock().unwrap().remove(key);
        self.locked.lock().unwrap().remove(key);
    }

    /// Remaining attempts before the key is locked out.
    pub fn remaining(&self, key: &str) -> u32 {
        let now = Instant::now();
        let attempts = self.attempts.lock().unwrap();
        let count = attempts
            .get(key)
            .map(|v| {
                v.iter()
                    .filter(|t| now.duration_since(**t) < self.window)
                    .count()
            })
            .unwrap_or(0);
        self.max_attempts.saturating_sub(count as u32)
    }
}
