use async_trait::async_trait;
use chrono::{offset::Local, Duration};
use loco_oauth2::models::users::OAuth2UserTrait;
use loco_rs::{auth::jwt, hash, prelude::*};
use passwords::PasswordGenerator;
use serde::{Deserialize, Serialize};
use serde_json::Map;
use totp_rs::{Algorithm, Secret, TOTP};
use uuid::Uuid;

use crate::models::_entities::o_auth2_sessions;
pub use crate::models::_entities::users::{self, ActiveModel, Entity, Model};

pub const MAGIC_LINK_LENGTH: i8 = 32;
pub const MAGIC_LINK_EXPIRATION_MIN: i8 = 5;

/// Minimum gap between verification-email resends for one account, in seconds.
pub const VERIFICATION_RESEND_COOLDOWN_SECS: i64 = 60;

// TODO(security): `users.totp_secret` is stored as a PLAINTEXT base32 string.
// Unlike `password` (a one-way hash) the TOTP secret must be kept in reversible
// form — the server needs the original value to recompute codes — so it is
// effectively password-equivalent: anyone who can read this column can mint
// valid 2FA codes for that user. It is deliberately left in plaintext for now
// and treated like the app's other server-side secrets (e.g. the SMTP password,
// kept out of the DB entirely). When secrets get a proper at-rest story, encrypt
// this column with a key held OUTSIDE the database (env / `pass`), decrypting
// only in memory. The single read/write site is `build_totp` +
// `begin_totp_enrollment` below; `totp_backup_codes` are already hashed and need
// no change. Grep `TODO(security)` to find this.

/// TOTP (Google Authenticator) parameters. These are the values Google
/// Authenticator assumes; it ignores anything else encoded in the otpauth URL,
/// so they must stay SHA1 / 6 digits / 30s or codes won't match.
const TOTP_ISSUER: &str = "Kompress";
const TOTP_DIGITS: usize = 6;
/// Accept codes ±1 time-step (~30s) to tolerate client/server clock drift.
const TOTP_SKEW: u8 = 1;
const TOTP_STEP: u64 = 30;
/// Number of one-time recovery codes generated when 2FA is enabled.
pub const TOTP_BACKUP_CODE_COUNT: usize = 8;

/// Build a [`TOTP`] from a stored base32 secret and the account label (email).
/// Returns `None` if the secret can't be decoded.
fn build_totp(secret_base32: &str, account: &str) -> Option<TOTP> {
    let bytes = Secret::Encoded(secret_base32.to_string()).to_bytes().ok()?;
    TOTP::new(
        Algorithm::SHA1,
        TOTP_DIGITS,
        TOTP_SKEW,
        TOTP_STEP,
        bytes,
        Some(TOTP_ISSUER.to_string()),
        account.to_string(),
    )
    .ok()
}

#[derive(Debug, Deserialize, Serialize)]
pub struct LoginParams {
    pub email: String,
    pub password: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct RegisterParams {
    pub email: String,
    pub password: String,
    pub name: String,
    /// "personal" or "company"; permanent for the account. Optional on the wire
    /// (older/JSON callers omit it) and normalized via [`normalize_account_type`].
    #[serde(default)]
    pub account_type: Option<String>,
}

/// Normalize an account type to one of the two permanent values, defaulting to
/// "personal" for anything missing or unexpected. An account's type is chosen
/// once at registration and never changes.
#[must_use]
pub fn normalize_account_type(value: Option<&str>) -> String {
    match value.map(str::trim) {
        Some("company") => "company".to_string(),
        _ => "personal".to_string(),
    }
}

#[derive(Debug, Validate, Deserialize)]
pub struct Validator {
    #[validate(length(min = 2, message = "Name must be at least 2 characters long."))]
    pub name: String,
    #[validate(email(message = "invalid email"))]
    pub email: String,
}

impl Validatable for ActiveModel {
    fn validator(&self) -> Box<dyn Validate> {
        Box::new(Validator {
            name: self.name.as_ref().to_owned(),
            email: self.email.as_ref().to_owned(),
        })
    }
}

#[async_trait::async_trait]
impl ActiveModelBehavior for crate::models::_entities::users::ActiveModel {
    async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
    where
        C: ConnectionTrait,
    {
        self.validate()?;
        if insert {
            let mut this = self;
            this.pid = ActiveValue::Set(Uuid::new_v4());
            this.api_key = ActiveValue::Set(format!("lo-{}", Uuid::new_v4()));
            Ok(this)
        } else {
            Ok(self)
        }
    }
}

#[async_trait]
impl Authenticable for Model {
    async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
        let user = users::Entity::find()
            .filter(
                model::query::condition()
                    .eq(users::Column::ApiKey, api_key)
                    .build(),
            )
            .one(db)
            .await?;
        user.ok_or_else(|| ModelError::EntityNotFound)
    }

    async fn find_by_claims_key(db: &DatabaseConnection, claims_key: &str) -> ModelResult<Self> {
        Self::find_by_pid(db, claims_key).await
    }
}

impl Model {
    /// finds a user by the provided email
    ///
    /// # Errors
    ///
    /// When could not find user by the given token or DB query error
    pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
        let user = users::Entity::find()
            .filter(
                model::query::condition()
                    .eq(users::Column::Email, email)
                    .build(),
            )
            .one(db)
            .await?;
        user.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// finds a user by the provided verification token
    ///
    /// # Errors
    ///
    /// When could not find user by the given token or DB query error
    pub async fn find_by_verification_token(
        db: &DatabaseConnection,
        token: &str,
    ) -> ModelResult<Self> {
        let user = users::Entity::find()
            .filter(
                model::query::condition()
                    .eq(users::Column::EmailVerificationToken, token)
                    .build(),
            )
            .one(db)
            .await?;
        user.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// finds a user by the magic token and verify and token expiration
    ///
    /// # Errors
    ///
    /// When could not find user by the given token or DB query error ot token expired
    pub async fn find_by_magic_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {
        let user = users::Entity::find()
            .filter(
                query::condition()
                    .eq(users::Column::MagicLinkToken, token)
                    .build(),
            )
            .one(db)
            .await?;

        let user = user.ok_or_else(|| ModelError::EntityNotFound)?;
        if let Some(expired_at) = user.magic_link_expiration {
            if expired_at >= Local::now() {
                Ok(user)
            } else {
                tracing::debug!(
                    user_pid = user.pid.to_string(),
                    token_expiration = expired_at.to_string(),
                    "magic token expired for the user."
                );
                Err(ModelError::msg("magic token expired"))
            }
        } else {
            tracing::error!(
                user_pid = user.pid.to_string(),
                "magic link expiration time not exists"
            );
            Err(ModelError::msg("expiration token not exists"))
        }
    }

    /// finds a user by the provided reset token
    ///
    /// # Errors
    ///
    /// When could not find user by the given token or DB query error
    pub async fn find_by_reset_token(db: &DatabaseConnection, token: &str) -> ModelResult<Self> {
        let user = users::Entity::find()
            .filter(
                model::query::condition()
                    .eq(users::Column::ResetToken, token)
                    .build(),
            )
            .one(db)
            .await?;
        user.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// finds a user by the provided pid
    ///
    /// # Errors
    ///
    /// When could not find user  or DB query error
    pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> {
        let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Any(e.into()))?;
        let user = users::Entity::find()
            .filter(
                model::query::condition()
                    .eq(users::Column::Pid, parse_uuid)
                    .build(),
            )
            .one(db)
            .await?;
        user.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// finds a user by the provided api key
    ///
    /// # Errors
    ///
    /// When could not find user by the given token or DB query error
    pub async fn find_by_api_key(db: &DatabaseConnection, api_key: &str) -> ModelResult<Self> {
        let user = users::Entity::find()
            .filter(
                model::query::condition()
                    .eq(users::Column::ApiKey, api_key)
                    .build(),
            )
            .one(db)
            .await?;
        user.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// Verifies whether the provided plain password matches the hashed password
    ///
    /// # Errors
    ///
    /// when could not verify password
    #[must_use]
    pub fn verify_password(&self, password: &str) -> bool {
        hash::verify_password(password, &self.password)
    }

    /// Whether this is a company account (vs a personal one). Fixed at
    /// registration.
    #[must_use]
    pub fn is_company(&self) -> bool {
        self.account_type == "company"
    }

    /// Whether two-factor auth is active for this account. This is the single
    /// source of truth used by the login flow: a secret may be present during a
    /// half-finished enrollment, but 2FA only gates login once it is *confirmed*
    /// (which is what sets `totp_enabled_at`).
    #[must_use]
    pub fn totp_enabled(&self) -> bool {
        self.totp_enabled_at.is_some()
    }

    /// Build the [`TOTP`] for this user from its stored secret, if any.
    fn totp(&self) -> Option<TOTP> {
        let secret = self.totp_secret.as_deref()?;
        build_totp(secret, &self.email)
    }

    /// A `data:image/png;base64,...` QR for the *pending* secret plus the secret
    /// itself (shown as a manual-entry fallback). Used on the enrollment page.
    /// Returns `None` if no secret is staged or QR rendering fails.
    #[must_use]
    pub fn totp_provisioning(&self) -> Option<(String, String)> {
        let totp = self.totp()?;
        let qr = totp.get_qr_base64().ok()?;
        Some((
            format!("data:image/png;base64,{qr}"),
            self.totp_secret.clone()?,
        ))
    }

    /// Verify a 6-digit authenticator code against the stored secret. Returns
    /// false if no secret is staged or the code is wrong. Works both during
    /// enrollment confirmation and at login.
    #[must_use]
    pub fn verify_totp_code(&self, code: &str) -> bool {
        let code = code.trim().replace(' ', "");
        self.totp()
            .and_then(|t| t.check_current(&code).ok())
            .unwrap_or(false)
    }

    /// Whether `code` matches one of the still-unused backup codes.
    #[must_use]
    pub fn matches_backup_code(&self, code: &str) -> bool {
        let code = code.trim().replace([' ', '-'], "");
        self.backup_code_hashes()
            .iter()
            .any(|h| hash::verify_password(&code, h))
    }

    /// The stored hashed backup codes (empty if none).
    fn backup_code_hashes(&self) -> Vec<String> {
        self.totp_backup_codes
            .as_deref()
            .and_then(|s| serde_json::from_str::<Vec<String>>(s).ok())
            .unwrap_or_default()
    }

    /// How many unused backup codes remain.
    #[must_use]
    pub fn backup_codes_remaining(&self) -> usize {
        self.backup_code_hashes().len()
    }

    /// Seconds the user must still wait before another verification email may be
    /// sent — 0 means a resend is allowed now. Throttling resends off the last
    /// `email_verification_sent_at` keeps the endpoint from being an easy way to
    /// spam someone's inbox.
    #[must_use]
    pub fn verification_resend_wait_secs(&self) -> i64 {
        match self.email_verification_sent_at {
            Some(sent) => {
                let elapsed =
                    (chrono::Utc::now() - sent.with_timezone(&chrono::Utc)).num_seconds();
                (VERIFICATION_RESEND_COOLDOWN_SECS - elapsed).max(0)
            }
            None => 0,
        }
    }

    /// Asynchronously creates a user with a password and saves it to the
    /// database.
    ///
    /// # Errors
    ///
    /// When could not save the user into the DB
    pub async fn create_with_password(
        db: &DatabaseConnection,
        params: &RegisterParams,
    ) -> ModelResult<Self> {
        let txn = db.begin().await?;

        if users::Entity::find()
            .filter(
                model::query::condition()
                    .eq(users::Column::Email, &params.email)
                    .build(),
            )
            .one(&txn)
            .await?
            .is_some()
        {
            return Err(ModelError::EntityAlreadyExists {});
        }

        let password_hash =
            hash::hash_password(&params.password).map_err(|e| ModelError::Any(e.into()))?;
        let user = users::ActiveModel {
            email: ActiveValue::set(params.email.to_string()),
            password: ActiveValue::set(password_hash),
            name: ActiveValue::set(params.name.to_string()),
            account_type: ActiveValue::set(normalize_account_type(params.account_type.as_deref())),
            ..Default::default()
        }
        .insert(&txn)
        .await?;

        txn.commit().await?;

        Ok(user)
    }

    /// Creates an account on behalf of a checkout guest. The user never picks a
    /// password here (a strong random one satisfies the NOT NULL column, as in
    /// the OAuth path); they receive a "set your password" link by email. Errors
    /// with [`ModelError::EntityAlreadyExists`] if the email is already taken.
    ///
    /// # Errors
    ///
    /// When the email already exists or the insert fails.
    pub async fn create_guest_account(
        db: &DatabaseConnection,
        email: &str,
        name: &str,
        account_type: &str,
    ) -> ModelResult<Self> {
        let password = PasswordGenerator::new()
            .length(16)
            .numbers(true)
            .lowercase_letters(true)
            .uppercase_letters(true)
            .symbols(true)
            .strict(true)
            .generate_one()
            .map_err(|e| ModelError::Any(e.into()))?;
        Self::create_with_password(
            db,
            &RegisterParams {
                email: email.to_string(),
                password,
                name: name.to_string(),
                account_type: Some(account_type.to_string()),
            },
        )
        .await
    }

    /// Creates a JWT
    ///
    /// # Errors
    ///
    /// when could not convert user claims to jwt token
    pub fn generate_jwt(&self, secret: &str, expiration: u64) -> ModelResult<String> {
        jwt::JWT::new(secret)
            .generate_token(expiration, self.pid.to_string(), Map::new())
            .map_err(ModelError::from)
    }
}

impl ActiveModel {
    /// Sets the email verification information for the user and
    /// updates it in the database.
    ///
    /// This method is used to record the timestamp when the email verification
    /// was sent and generate a unique verification token for the user.
    ///
    /// # Errors
    ///
    /// when has DB query error
    pub async fn set_email_verification_sent(
        mut self,
        db: &DatabaseConnection,
    ) -> ModelResult<Model> {
        self.email_verification_sent_at = ActiveValue::set(Some(Local::now().into()));
        self.email_verification_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));
        self.update(db).await.map_err(ModelError::from)
    }

    /// Sets the information for a reset password request,
    /// generates a unique reset password token, and updates it in the
    /// database.
    ///
    /// This method records the timestamp when the reset password token is sent
    /// and generates a unique token for the user.
    ///
    /// # Arguments
    ///
    /// # Errors
    ///
    /// when has DB query error
    pub async fn set_forgot_password_sent(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
        self.reset_sent_at = ActiveValue::set(Some(Local::now().into()));
        self.reset_token = ActiveValue::Set(Some(Uuid::new_v4().to_string()));
        self.update(db).await.map_err(ModelError::from)
    }

    /// Records the verification time when a user verifies their
    /// email and updates it in the database.
    ///
    /// This method sets the timestamp when the user successfully verifies their
    /// email.
    ///
    /// # Errors
    ///
    /// when has DB query error
    pub async fn verified(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
        self.email_verified_at = ActiveValue::set(Some(Local::now().into()));
        self.update(db).await.map_err(ModelError::from)
    }

    /// Resets the current user password with a new password and
    /// updates it in the database.
    ///
    /// This method hashes the provided password and sets it as the new password
    /// for the user.
    ///
    /// # Errors
    ///
    /// when has DB query error or could not hashed the given password
    pub async fn reset_password(
        mut self,
        db: &DatabaseConnection,
        password: &str,
    ) -> ModelResult<Model> {
        self.password =
            ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?);
        self.reset_token = ActiveValue::Set(None);
        self.reset_sent_at = ActiveValue::Set(None);
        self.update(db).await.map_err(ModelError::from)
    }

    /// Creates a magic link token for passwordless authentication.
    ///
    /// Generates a random token with a specified length and sets an expiration time
    /// for the magic link. This method is used to initiate the magic link authentication flow.
    ///
    /// # Errors
    /// - Returns an error if database update fails
    pub async fn create_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
        let random_str = hash::random_string(MAGIC_LINK_LENGTH as usize);
        let expired = Local::now() + Duration::minutes(MAGIC_LINK_EXPIRATION_MIN.into());

        self.magic_link_token = ActiveValue::set(Some(random_str));
        self.magic_link_expiration = ActiveValue::set(Some(expired.into()));
        self.update(db).await.map_err(ModelError::from)
    }

    /// Verifies and invalidates the magic link after successful authentication.
    ///
    /// Clears the magic link token and expiration time after the user has
    /// successfully authenticated using the magic link.
    ///
    /// # Errors
    /// - Returns an error if database update fails
    pub async fn clear_magic_link(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
        self.magic_link_token = ActiveValue::set(None);
        self.magic_link_expiration = ActiveValue::set(None);
        self.update(db).await.map_err(ModelError::from)
    }

    /// Stage a fresh TOTP secret for enrollment. This does **not** turn 2FA on —
    /// `totp_enabled_at` stays null until the user proves they scanned it by
    /// confirming a code (see [`Self::enable_totp`]). Any previously staged
    /// secret/backup codes are discarded so re-enrolling always starts clean.
    pub async fn begin_totp_enrollment(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
        let secret = match Secret::generate_secret().to_encoded() {
            Secret::Encoded(s) => s,
            // generate_secret() always yields raw bytes that encode cleanly.
            Secret::Raw(_) => unreachable!("to_encoded() returns Encoded"),
        };
        self.totp_secret = ActiveValue::set(Some(secret));
        self.totp_enabled_at = ActiveValue::set(None);
        self.totp_backup_codes = ActiveValue::set(None);
        self.update(db).await.map_err(ModelError::from)
    }

    /// Confirm enrollment and switch 2FA on. The caller must have already
    /// verified a code against the staged secret. Generates and stores hashed
    /// one-time backup codes and returns the plaintext codes to display **once**.
    pub async fn enable_totp(
        mut self,
        db: &DatabaseConnection,
    ) -> ModelResult<(Model, Vec<String>)> {
        let (plain, hashes) = generate_backup_codes()?;
        let encoded = serde_json::to_string(&hashes).map_err(|e| ModelError::Any(e.into()))?;
        self.totp_enabled_at = ActiveValue::set(Some(Local::now().into()));
        self.totp_backup_codes = ActiveValue::set(Some(encoded));
        let model = self.update(db).await.map_err(ModelError::from)?;
        Ok((model, plain))
    }

    /// Turn 2FA off and wipe all TOTP state. Callers gate this behind a fresh
    /// confirmation (password or a current code).
    pub async fn disable_totp(mut self, db: &DatabaseConnection) -> ModelResult<Model> {
        self.totp_secret = ActiveValue::set(None);
        self.totp_enabled_at = ActiveValue::set(None);
        self.totp_backup_codes = ActiveValue::set(None);
        self.update(db).await.map_err(ModelError::from)
    }

    /// Remove a used backup code from the stored set so it can't be reused.
    /// `code` is matched against the remaining hashes; a no-op if it doesn't
    /// match (the caller decides whether a match was required).
    pub async fn consume_backup_code(
        mut self,
        db: &DatabaseConnection,
        code: &str,
    ) -> ModelResult<Model> {
        let code = code.trim().replace([' ', '-'], "");
        let current: Vec<String> = match self.totp_backup_codes.as_ref() {
            Some(s) => serde_json::from_str(s.as_str()).unwrap_or_default(),
            None => Vec::new(),
        };
        let remaining: Vec<String> = current
            .into_iter()
            .filter(|h| !hash::verify_password(&code, h))
            .collect();
        let encoded = serde_json::to_string(&remaining).map_err(|e| ModelError::Any(e.into()))?;
        self.totp_backup_codes = ActiveValue::set(Some(encoded));
        self.update(db).await.map_err(ModelError::from)
    }

    /// Replace the backup codes with a fresh set (e.g. after the user used some).
    /// Only meaningful while 2FA is enabled; returns the new plaintext codes.
    pub async fn regenerate_backup_codes(
        mut self,
        db: &DatabaseConnection,
    ) -> ModelResult<(Model, Vec<String>)> {
        let (plain, hashes) = generate_backup_codes()?;
        let encoded = serde_json::to_string(&hashes).map_err(|e| ModelError::Any(e.into()))?;
        self.totp_backup_codes = ActiveValue::set(Some(encoded));
        let model = self.update(db).await.map_err(ModelError::from)?;
        Ok((model, plain))
    }
}

/// Generate `TOTP_BACKUP_CODE_COUNT` recovery codes, returning
/// `(plaintext, hashes)`. Only the hashes are persisted; the plaintext is shown
/// to the user once and never stored.
fn generate_backup_codes() -> ModelResult<(Vec<String>, Vec<String>)> {
    let mut plain = Vec::with_capacity(TOTP_BACKUP_CODE_COUNT);
    let mut hashes = Vec::with_capacity(TOTP_BACKUP_CODE_COUNT);
    for _ in 0..TOTP_BACKUP_CODE_COUNT {
        let code = hash::random_string(10).to_lowercase();
        let hashed = hash::hash_password(&code).map_err(|e| ModelError::Any(e.into()))?;
        plain.push(code);
        hashes.push(hashed);
    }
    Ok((plain, hashes))
}

/// Google OpenID Connect user profile (the fields our scopes request).
/// <https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo>
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct OAuth2UserProfile {
    pub email: String,
    pub name: String,
    pub sub: String,
    pub email_verified: bool,
    pub given_name: Option<String>,
    pub family_name: Option<String>,
    pub picture: Option<String>,
    pub locale: Option<String>,
}

#[async_trait]
impl OAuth2UserTrait<OAuth2UserProfile> for Model {
    /// Resolve the user behind an active OAuth2 session id.
    async fn find_by_oauth2_session_id(
        db: &DatabaseConnection,
        session_id: &str,
    ) -> ModelResult<Self> {
        let session = o_auth2_sessions::Entity::find()
            .filter(o_auth2_sessions::Column::SessionId.eq(session_id))
            .one(db)
            .await?
            .ok_or_else(|| ModelError::EntityNotFound)?;
        users::Entity::find_by_id(session.user_id)
            .one(db)
            .await?
            .ok_or_else(|| ModelError::EntityNotFound)
    }

    /// Find-or-create the local user for a verified OAuth2 profile.
    ///
    /// Per security advisory LOC-2025-04, OAuth2-created accounts get a strong
    /// RANDOM password (never the provider `sub`) — they sign in via the
    /// provider, and the random secret just satisfies the NOT NULL column.
    /// Google has already verified the email, so we mark it verified.
    async fn upsert_with_oauth(
        db: &DatabaseConnection,
        profile: &OAuth2UserProfile,
    ) -> ModelResult<Self> {
        let txn = db.begin().await?;
        let user = match users::Entity::find()
            .filter(users::Column::Email.eq(&profile.email))
            .one(&txn)
            .await?
        {
            Some(user) => user,
            None => {
                let password = PasswordGenerator::new()
                    .length(16)
                    .numbers(true)
                    .lowercase_letters(true)
                    .uppercase_letters(true)
                    .symbols(true)
                    .exclude_similar_characters(true)
                    .strict(true)
                    .generate_one()
                    .map_err(|e| ModelError::Any(e.into()))?;
                let password_hash =
                    hash::hash_password(&password).map_err(|e| ModelError::Any(e.into()))?;
                users::ActiveModel {
                    email: ActiveValue::set(profile.email.to_string()),
                    name: ActiveValue::set(profile.name.to_string()),
                    email_verified_at: ActiveValue::set(Some(Local::now().into())),
                    password: ActiveValue::set(password_hash),
                    ..Default::default()
                }
                .insert(&txn)
                .await
                .map_err(|e| {
                    tracing::error!(error = %e, "failed to create OAuth2 user");
                    ModelError::Any(e.into())
                })?
            }
        };
        txn.commit().await?;
        Ok(user)
    }

    /// Required by the trait; mirrors the inherent [`Model::generate_jwt`]
    /// (inlined to avoid the inherent/trait name clash).
    fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult<String> {
        jwt::JWT::new(secret)
            .generate_token(*expiration, self.pid.to_string(), Map::new())
            .map_err(ModelError::from)
    }
}
