//! OTP (passwordless) authentication service.

#[cfg(feature = "email")]
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};

use rustelo_core_lib::auth::{AuthError, AuthResponse, User};
#[cfg(feature = "email")]
use serde_json::Value;

use super::{
    jwt::JwtService,
    otp_store::OtpStore,
    password::PasswordService,
    repository::{AuthRepository, AuthRepositoryTrait},
};
#[cfg(feature = "email")]
use crate::email::EmailService;

/// Outcome of `verify_otp` — distinguishes returning users from new ones
/// that have not yet accepted GDPR consent.
pub enum OtpVerifyOutcome {
    /// Email belongs to an existing account — session is ready.
    Authenticated(AuthResponse),
    /// Email is new — caller must obtain GDPR consent before creating the
    /// account.
    NeedsConsent { pending_token: String },
}

pub struct OtpService {
    pub store: Arc<dyn OtpStore>,
    pub password_svc: Arc<PasswordService>,
    #[cfg(feature = "email")]
    pub email_svc: Arc<EmailService>,
    pub auth_repo: Arc<AuthRepository>,
    pub jwt_svc: Arc<JwtService>,
}

impl OtpService {
    /// Generate and send a one-time code to `email`.
    ///
    /// Rate-limits to one code per minute: if a valid entry was issued less
    /// than `otp_expiry_minutes - 1` minutes ago it is still "fresh" and we
    /// reject.
    pub async fn request_otp(
        &self,
        email: &str,
        code_len: u8,
        expiry_minutes: u64,
        lang: &str,
    ) -> Result<(), AuthError> {
        // Rate-limit: reject if a fresh (< 1 min old) valid entry already exists.
        if let Some(existing) = self.store.get(email).await? {
            let fresh_threshold =
                Instant::now() + Duration::from_secs((expiry_minutes.saturating_sub(1)) * 60);
            if existing.expires_at > fresh_threshold {
                return Err(AuthError::RateLimitExceeded);
            }
        }

        // Generate a zero-padded N-digit code.
        let max = 10u32.pow(u32::from(code_len));
        let code_num: u32 = rand::random::<u32>() % max;
        let code = format!("{:0>width$}", code_num, width = code_len as usize);

        // Hash via Argon2 (sync).
        let code_hash = self
            .password_svc
            .hash_password(&code)
            .map_err(|_| AuthError::InternalError)?;

        let expires_at = Instant::now() + Duration::from_secs(expiry_minutes * 60);
        self.store.insert(email, &code_hash, expires_at).await?;

        // Send email.
        #[cfg(feature = "email")]
        {
            use rustelo_core_lib::t_for_language;
            let subject = t_for_language(lang, "auth-email-otp-subject");
            let expire_msg = t_for_language(lang, "auth-email-otp-expire-msg")
                .replace("%minutes%", &expiry_minutes.to_string());

            let mut data: HashMap<String, Value> = HashMap::new();
            data.insert("code".to_string(), Value::String(code));
            data.insert(
                "title".to_string(),
                Value::String(t_for_language(lang, "auth-email-otp-title")),
            );
            data.insert(
                "subtitle".to_string(),
                Value::String(t_for_language(lang, "auth-email-otp-subtitle")),
            );
            data.insert("expire_msg".to_string(), Value::String(expire_msg));
            data.insert(
                "ignore_msg".to_string(),
                Value::String(t_for_language(lang, "auth-email-otp-ignore-msg")),
            );

            self.email_svc
                .send_templated_email_with_language(email, &subject, "otp_code", data, lang)
                .await
                .map_err(|e| {
                    tracing::error!("OTP email send failed for {}: {}", email, e);
                    AuthError::InternalError
                })?;
        }

        #[cfg(not(feature = "email"))]
        {
            let _ = lang; // only used when email feature is enabled
            tracing::warn!("OTP requested for {} but email feature is disabled", email);
        }

        Ok(())
    }

    /// Verify the submitted code.
    ///
    /// Returns `OtpVerifyOutcome::Authenticated` if the email belongs to an
    /// existing user, or `OtpVerifyOutcome::NeedsConsent` if the email is new
    /// and requires GDPR acceptance before account creation.
    pub async fn verify_otp(
        &self,
        email: &str,
        code: &str,
        max_attempts: u8,
    ) -> Result<OtpVerifyOutcome, AuthError> {
        let entry = self
            .store
            .get(email)
            .await?
            .ok_or(AuthError::InvalidToken)?;

        if entry.expires_at <= Instant::now() {
            return Err(AuthError::TokenExpired);
        }

        let attempts = self.store.increment_attempts(email).await?;
        if attempts > max_attempts {
            self.store.mark_used(email).await?;
            return Err(AuthError::TooMany2FAAttempts);
        }

        let valid = self
            .password_svc
            .verify_password(code, &entry.code_hash)
            .map_err(|_| AuthError::InternalError)?;

        if !valid {
            return Err(AuthError::InvalidCredentials);
        }

        self.store.mark_used(email).await?;

        // Check whether the account already exists.
        let existing = self
            .auth_repo
            .find_user_by_email(email)
            .await
            .map_err(|_| AuthError::DatabaseError)?;

        if let Some(db_user) = existing {
            // Existing user: authenticate immediately.
            let user: User = db_user.into();
            let token_pair = self
                .jwt_svc
                .generate_token_pair(&user)
                .map_err(|_| AuthError::InternalError)?;

            Ok(OtpVerifyOutcome::Authenticated(AuthResponse {
                user,
                access_token: token_pair.access_token,
                refresh_token: Some(token_pair.refresh_token),
                expires_in: token_pair.expires_in,
                token_type: token_pair.token_type,
                requires_2fa: false,
            }))
        } else {
            // New email: issue a short-lived pending token for the GDPR step.
            let pending_token = self
                .jwt_svc
                .generate_pending_gdpr_token(email)
                .map_err(|_| AuthError::InternalError)?;

            Ok(OtpVerifyOutcome::NeedsConsent { pending_token })
        }
    }

    /// Accept GDPR consent for a pending registration.
    ///
    /// Validates the `pending_token` issued by `verify_otp`, creates the user
    /// account, records the consent timestamp, and returns auth tokens.
    pub async fn accept_gdpr(&self, pending_token: &str) -> Result<AuthResponse, AuthError> {
        let claims = self
            .jwt_svc
            .verify_pending_gdpr_token(pending_token)
            .map_err(|_| AuthError::InvalidToken)?;

        let email = &claims.sub;

        // Guard against replay if the user already exists.
        if self
            .auth_repo
            .find_user_by_email(email)
            .await
            .map_err(|_| AuthError::DatabaseError)?
            .is_some()
        {
            return Err(AuthError::ValidationError(
                "Account already exists for this email".to_string(),
            ));
        }

        use crate::database::auth::CreateUserRequest;
        let req = CreateUserRequest {
            email: email.to_string(),
            username: None,
            password_hash: String::new(),
            display_name: None,
            is_verified: true,
            is_active: true,
        };

        let db_user = self
            .auth_repo
            .create_user(&req)
            .await
            .map_err(|_| AuthError::DatabaseError)?;

        // Record GDPR consent timestamp (best-effort — does not abort on error).
        if let Err(e) = self.auth_repo.update_gdpr_accepted_at(db_user.id).await {
            tracing::warn!("Could not store gdpr_accepted_at for {}: {}", email, e);
        }

        let user: User = db_user.into();
        let token_pair = self
            .jwt_svc
            .generate_token_pair(&user)
            .map_err(|_| AuthError::InternalError)?;

        Ok(AuthResponse {
            user,
            access_token: token_pair.access_token,
            refresh_token: Some(token_pair.refresh_token),
            expires_in: token_pair.expires_in,
            token_type: token_pair.token_type,
            requires_2fa: false,
        })
    }

    /// Invalidate the session identified by `session_token`.
    pub async fn logout_session(&self, session_token: &str) -> Result<(), AuthError> {
        self.auth_repo
            .delete_session(session_token)
            .await
            .map_err(|_| AuthError::DatabaseError)
    }
}
