//! Shared password utilities for hashing, verification, validation and
//! session-cookie construction.
//!
//! Lives in `better-auth-core` so that any crate in the workspace (plugins,
//! integrations, etc.) can reuse these primitives without duplicating logic.

use std::sync::Arc;

use argon2::password_hash::{PasswordHash, SaltString, rand_core::OsRng};
use argon2::{
    Algorithm, Argon2, Params, PasswordHasher as Argon2PasswordHasher, PasswordVerifier, Version,
};
use async_trait::async_trait;
use serde::Serialize;

use crate::adapters::DatabaseAdapter;
use crate::config::Argon2Config;
use crate::error::{AuthError, AuthResult};
use crate::plugin::AuthContext;
use crate::types::UpdateUser;

// ---------------------------------------------------------------------------
// PasswordHasher trait
// ---------------------------------------------------------------------------

/// Custom password hasher trait for pluggable password hashing strategies.
///
/// When provided in plugin configs, this overrides the default Argon2-based
/// password hashing.
#[async_trait]
pub trait PasswordHasher: Send + Sync {
    /// Hash a plaintext password and return the hash string.
    async fn hash(&self, password: &str) -> AuthResult<String>;
    /// Verify a password against a hash string. Returns `true` if the password matches.
    async fn verify(&self, hash: &str, password: &str) -> AuthResult<bool>;
}

// ---------------------------------------------------------------------------
// hash / verify helpers
// ---------------------------------------------------------------------------

/// Hash `password` using the custom `hasher` (if provided) or the default
/// Argon2 profile configured by Athena Auth defaults.
pub async fn hash_password(
    hasher: Option<&Arc<dyn PasswordHasher>>,
    password: &str,
) -> AuthResult<String> {
    hash_password_with_config(hasher, None, password).await
}

/// Hash `password` using the custom `hasher` (if provided) or the configured
/// Argon2 settings when no custom hasher is installed.
pub async fn hash_password_with_config(
    hasher: Option<&Arc<dyn PasswordHasher>>,
    argon2_config: Option<&Argon2Config>,
    password: &str,
) -> AuthResult<String> {
    if let Some(hasher) = hasher {
        return hasher.hash(password).await;
    }

    let salt: SaltString = SaltString::generate(&mut OsRng);
    let argon2 = build_argon2(argon2_config)?;

    let password_hash: PasswordHash<'_> = argon2
        .hash_password(password.as_bytes(), &salt)
        .map_err(|e| AuthError::PasswordHash(format!("Failed to hash password: {}", e)))?;

    Ok(password_hash.to_string())
}

/// Verify `password` against `hash` using the custom `hasher` (if provided) or
/// the Athena Auth default Argon2 profile. Returns `Ok(())` on match, or
/// `Err(AuthError::InvalidCredentials)` on mismatch.
pub async fn verify_password(
    hasher: Option<&Arc<dyn PasswordHasher>>,
    password: &str,
    hash: &str,
) -> AuthResult<()> {
    verify_password_with_config(hasher, None, password, hash).await
}

/// Verify `password` against `hash` using the custom `hasher` (if provided) or
/// the configured Argon2 settings.
///
/// Note: Argon2 verification uses the parameters embedded in the parsed PHC
/// hash, so `argon2_config` primarily affects newly generated hashes.
pub async fn verify_password_with_config(
    hasher: Option<&Arc<dyn PasswordHasher>>,
    argon2_config: Option<&Argon2Config>,
    password: &str,
    hash: &str,
) -> AuthResult<()> {
    if let Some(hasher) = hasher {
        return hasher.verify(hash, password).await.and_then(|valid| {
            if valid {
                Ok(())
            } else {
                Err(AuthError::InvalidCredentials)
            }
        });
    }

    let parsed_hash: PasswordHash<'_> = PasswordHash::new(hash)
        .map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;

    let argon2 = build_argon2(argon2_config)?;
    argon2
        .verify_password(password.as_bytes(), &parsed_hash)
        .map_err(|_| AuthError::InvalidCredentials)?;

    Ok(())
}

/// Return `true` when a stored Argon2 PHC hash does not match the configured
/// cost parameters and should be regenerated on the next successful sign-in.
pub fn password_hash_needs_rehash(hash: &str, argon2_config: &Argon2Config) -> AuthResult<bool> {
    let parsed_hash: PasswordHash<'_> = PasswordHash::new(hash)
        .map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;

    let memory_cost = parsed_hash.params.get_decimal("m").ok_or_else(|| {
        AuthError::PasswordHash("Password hash is missing Argon2 memory cost".into())
    })?;
    let time_cost = parsed_hash.params.get_decimal("t").ok_or_else(|| {
        AuthError::PasswordHash("Password hash is missing Argon2 time cost".into())
    })?;
    let parallelism = parsed_hash.params.get_decimal("p").ok_or_else(|| {
        AuthError::PasswordHash("Password hash is missing Argon2 parallelism".into())
    })?;

    Ok(memory_cost != argon2_config.memory_cost
        || time_cost != argon2_config.time_cost
        || parallelism != argon2_config.parallelism)
}

fn build_argon2(argon2_config: Option<&Argon2Config>) -> AuthResult<Argon2<'static>> {
    let argon2_config = argon2_config.cloned().unwrap_or_default();

    let params = Params::new(
        argon2_config.memory_cost,
        argon2_config.time_cost,
        argon2_config.parallelism,
        None,
    )
    .map_err(|error| AuthError::PasswordHash(format!("Invalid Argon2 configuration: {error}")))?;

    Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
}

// ---------------------------------------------------------------------------
// Password validation
// ---------------------------------------------------------------------------

/// Validate `password` against both the plugin-level length limits and the
/// global `PasswordConfig` strength rules.  Performs min-length, max-length,
/// uppercase, lowercase, digit and special-character checks.
pub fn validate_password<DB: DatabaseAdapter>(
    password: &str,
    min_length: usize,
    max_length: usize,
    ctx: &AuthContext<DB>,
) -> AuthResult<()> {
    let config: &crate::PasswordConfig = &ctx.config.password;

    if password.len() < min_length {
        return Err(AuthError::bad_request(format!(
            "Password must be at least {} characters long",
            config.min_length
        )));
    }

    if password.len() > max_length {
        return Err(AuthError::bad_request(format!(
            "Password must be at most {} characters long",
            max_length
        )));
    }

    if config.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
        return Err(AuthError::bad_request(
            "Password must contain at least one uppercase letter",
        ));
    }

    if config.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
        return Err(AuthError::bad_request(
            "Password must contain at least one lowercase letter",
        ));
    }

    if config.require_numbers && !password.chars().any(|c| c.is_ascii_digit()) {
        return Err(AuthError::bad_request(
            "Password must contain at least one number",
        ));
    }

    if config.require_special
        && !password
            .chars()
            .any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c))
    {
        return Err(AuthError::bad_request(
            "Password must contain at least one special character",
        ));
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Serialisation helper
// ---------------------------------------------------------------------------

/// Serialize any `Serialize`-able value to `serde_json::Value`, converting
/// errors to `AuthError::internal`.
pub fn serialize_to_value(value: &impl Serialize) -> AuthResult<serde_json::Value> {
    serde_json::to_value(value)
        .map_err(|e| AuthError::internal(format!("Failed to serialize value: {}", e)))
}

// ---------------------------------------------------------------------------
// UpdateUser helper
// ---------------------------------------------------------------------------

/// Build an `UpdateUser` that only changes the `metadata` field.
pub fn update_user_metadata(metadata: serde_json::Value) -> UpdateUser {
    UpdateUser {
        metadata: Some(metadata),
        ..Default::default()
    }
}

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

    #[tokio::test]
    async fn default_hash_password_uses_repo_argon2_profile() {
        let hash = hash_password(None, "Password123!")
            .await
            .expect("hash password with default config");
        let parsed_hash = PasswordHash::new(&hash).expect("parse generated password hash");
        let default_config = Argon2Config::default();

        assert_eq!(
            parsed_hash.params.get_decimal("m").unwrap(),
            default_config.memory_cost
        );
        assert_eq!(
            parsed_hash.params.get_decimal("t").unwrap(),
            default_config.time_cost
        );
        assert_eq!(
            parsed_hash.params.get_decimal("p").unwrap(),
            default_config.parallelism
        );
    }

    #[tokio::test]
    async fn default_verify_password_matches_default_hash_profile() {
        let hash = hash_password(None, "Password123!")
            .await
            .expect("hash password with default config");

        verify_password(None, "Password123!", &hash)
            .await
            .expect("verify password hashed with default config");
        assert!(
            !password_hash_needs_rehash(&hash, &Argon2Config::default())
                .expect("inspect generated hash profile"),
            "hash generated with default helper should already match the default profile"
        );
    }
}
