use actix_web::{Error, HttpRequest};
use argon2::{
    Argon2,
    password_hash::{PasswordHash, PasswordVerifier},
};
use db::database::AppDatabase;
use model::{
    repos::RepoModel,
    users::{user::UserModel, user_token::UserTokenModel},
};

use crate::{
    http::utils::extract_basic_credentials, ssh::authz::SshAuthService,
};

pub async fn verify_access_token(
    db: &AppDatabase,
    username: &str,
    access_key: &str,
) -> Result<UserModel, Error> {
    let user = sqlx::query_as::<_, UserModel>(
        "SELECT id, username, display_name, avatar_url, website_url, allow_use, can_search, \
                last_sign_in_at, created_at, updated_at \
         FROM \"user\" \
         WHERE username = $1",
    )
    .bind(username)
    .fetch_optional(db.reader())
    .await
    .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid username or access key"))?
    .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid username or access key"))?;

    let tokens: Vec<UserTokenModel> = sqlx::query_as::<_, UserTokenModel>(
        "SELECT id, \"user\", name, token_hash, scopes, expires_at, is_revoked, created_at, updated_at \
         FROM user_token \
         WHERE \"user\" = $1 AND is_revoked = false",
    )
    .bind(user.id)
    .fetch_all(db.reader())
    .await
    .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid username or access key"))?
    .into_iter()
    .filter(|token| {
        token
            .expires_at
            .map(|expires_at| expires_at >= chrono::Utc::now())
            .unwrap_or(true)
    })
    .collect();

    for token in tokens {
        let Ok(hash) = PasswordHash::new(&token.token_hash) else {
            tracing::warn!(
                token_id = token.id,
                "invalid stored access key hash"
            );
            continue;
        };
        if Argon2::default()
            .verify_password(access_key.as_bytes(), &hash)
            .is_ok()
        {
            return Ok(user);
        }
    }

    Err(actix_web::error::ErrorUnauthorized(
        "Invalid username or access key",
    ))
}

pub async fn authorize_repo_access(
    req: &HttpRequest,
    db: &AppDatabase,
    repo: &RepoModel,
    is_write: bool,
) -> Result<(), Error> {
    if !is_write && repo.visibility == "public" {
        return Ok(());
    }

    let (username, access_key) = extract_basic_credentials(req)?;
    let user = verify_access_token(db, &username, &access_key).await?;
    let authz = SshAuthService::new(db.clone());

    let can_access = authz.check_repo_permission(&user, repo, is_write).await;
    if !can_access {
        return Err(actix_web::error::ErrorForbidden(
            "No permission for repository",
        ));
    }

    Ok(())
}
