//! OTP token storage abstractions.
//!
//! Provides an async trait for OTP entry lifecycle and two implementations:
//! - `MemoryOtpStore`: DashMap-backed, with a background cleanup task.
//! - `DbOtpStore`: database-backed (requires `auth` feature + `otp_tokens`
//!   table).

use std::sync::Arc;
use std::time::{Duration, Instant};

use async_trait::async_trait;
use dashmap::DashMap;
use rustelo_core_lib::auth::AuthError;

/// A single OTP entry stored in the backing store.
#[derive(Clone)]
pub struct OtpEntry {
    pub code_hash: String,
    /// Wall-clock deadline; store impls filter expired entries via this field.
    pub expires_at: Instant,
    /// Number of verification attempts made so far.
    pub attempts: u8,
    /// Whether the token has been consumed (invalidates it for further use).
    pub used: bool,
}

/// Backing store for OTP tokens.
///
/// All methods are infallible in the memory implementation; the `Result`
/// wrapper is present to allow database implementations to propagate I/O
/// errors.
#[async_trait]
pub trait OtpStore: Send + Sync {
    /// Insert (or overwrite) an OTP entry for `email`.
    async fn insert(
        &self,
        email: &str,
        code_hash: &str,
        expires_at: Instant,
    ) -> Result<(), AuthError>;

    /// Retrieve the entry for `email`, returning `None` if it is missing,
    /// expired, or already consumed.
    async fn get(&self, email: &str) -> Result<Option<OtpEntry>, AuthError>;

    /// Atomically increment the attempt counter and return the new value.
    async fn increment_attempts(&self, email: &str) -> Result<u8, AuthError>;

    /// Mark the entry as consumed.  Subsequent `get` calls return `None`.
    async fn mark_used(&self, email: &str) -> Result<(), AuthError>;
}

// ─── In-memory implementation
// ─────────────────────────────────────────────────

pub struct MemoryOtpStore {
    inner: Arc<DashMap<String, OtpEntry>>,
}

impl MemoryOtpStore {
    /// Create a new store wrapped in an `Arc` and spawn a background cleanup
    /// task that evicts expired / consumed entries every 60 seconds.
    pub fn new_arc() -> Arc<Self> {
        let inner: Arc<DashMap<String, OtpEntry>> = Arc::new(DashMap::new());
        let cleanup = Arc::clone(&inner);
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(Duration::from_secs(60));
            loop {
                interval.tick().await;
                cleanup.retain(|_, e| !e.used && e.expires_at > Instant::now());
            }
        });
        Arc::new(Self { inner })
    }
}

#[async_trait]
impl OtpStore for MemoryOtpStore {
    async fn insert(
        &self,
        email: &str,
        code_hash: &str,
        expires_at: Instant,
    ) -> Result<(), AuthError> {
        self.inner.insert(
            email.to_string(),
            OtpEntry {
                code_hash: code_hash.to_string(),
                expires_at,
                attempts: 0,
                used: false,
            },
        );
        Ok(())
    }

    async fn get(&self, email: &str) -> Result<Option<OtpEntry>, AuthError> {
        let entry = self
            .inner
            .get(email)
            .filter(|e| !e.used && e.expires_at > Instant::now())
            .map(|e| e.clone());
        Ok(entry)
    }

    async fn increment_attempts(&self, email: &str) -> Result<u8, AuthError> {
        let mut entry = self.inner.get_mut(email).ok_or(AuthError::InvalidToken)?;
        entry.attempts += 1;
        Ok(entry.attempts)
    }

    async fn mark_used(&self, email: &str) -> Result<(), AuthError> {
        if let Some(mut entry) = self.inner.get_mut(email) {
            entry.used = true;
        }
        Ok(())
    }
}
