Skip to content
use actix_identity::Identity;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use rand::rngs::OsRng;
use uuid::Uuid;

use crate::db::Database;
use crate::models::User;

pub const SESSION_COOKIE_NAME: &str = "pear_session";

pub fn hash_password(password: &str) -> Result<String, String> {
    let argon2 = Argon2::default();
    let salt = SaltString::generate(&mut OsRng);
    argon2
        .hash_password(password.as_bytes(), &salt)
        .map(|hash| hash.to_string())
        .map_err(|e| e.to_string())
}

pub fn verify_password(password: &str, hash: &str) -> Result<bool, String> {
    let argon2 = Argon2::default();
    let parsed_hash = PasswordHash::new(hash).map_err(|e| e.to_string())?;
    Ok(argon2
        .verify_password(password.as_bytes(), &parsed_hash)
        .is_ok())
}

pub fn create_user(username: &str, email: &str, password: &str) -> Result<User, String> {
    let email = email.trim();
    Ok(User {
        id: Uuid::new_v4().to_string(),
        username: username.trim().to_string(),
        email: (!email.is_empty()).then(|| email.to_string()),
        password_hash: hash_password(password)?,
        created_at: chrono::Utc::now().to_rfc3339(),
    })
}

/// Resolves the logged-in user from the Actix identity, if present.
pub async fn current_user(identity: Option<Identity>, db: &Database) -> Option<User> {
    let user_id = identity?.id().ok()?;
    db.get_user_by_id(&user_id).await.ok().flatten()
}

pub fn validate_username(username: &str) -> Result<(), String> {
    let username = username.trim();
    if username.len() < 3 {
        return Err("Username must be at least 3 characters".to_string());
    }
    if username.len() > 32 {
        return Err("Username must be at most 32 characters".to_string());
    }
    if !username
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
    {
        return Err("Username may only contain letters, numbers, '-' and '_'".to_string());
    }
    Ok(())
}

pub fn validate_email(email: &str) -> Result<(), String> {
    let email = email.trim();
    if email.is_empty() || !email.contains('@') {
        return Err("Please enter a valid email address".to_string());
    }
    Ok(())
}

pub fn validate_password(password: &str) -> Result<(), String> {
    if password.len() < 8 {
        return Err("Password must be at least 8 characters".to_string());
    }
    Ok(())
}

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

    #[test]
    fn test_hash_and_verify_password() {
        let hash = hash_password("correct horse").unwrap();
        assert!(verify_password("correct horse", &hash).unwrap());
        assert!(!verify_password("wrong", &hash).unwrap());
    }

    #[test]
    fn test_create_user_hashes_password_and_drops_blank_email() {
        let user = create_user("alice", "  ", "password123").unwrap();
        assert_eq!(user.username, "alice");
        assert_eq!(user.email, None);
        assert!(verify_password("password123", &user.password_hash).unwrap());

        let with_email = create_user("bob", "bob@example.com", "password123").unwrap();
        assert_eq!(with_email.email, Some("bob@example.com".to_string()));
    }

    #[test]
    fn test_validate_username() {
        assert!(validate_username("alice").is_ok());
        assert!(validate_username("a").is_err());
        assert!(validate_username("al ice").is_err());
    }
}