use argon2::{
    Argon2, PasswordHash,
    password_hash::{PasswordHasher, PasswordVerifier},
};
use db::sqlx;
use model::users::{UserModel, user_pass::UserPasswordModel};
use serde::{Deserialize, Serialize};
use session::Session;

use crate::{AppService, error::AppError};

#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct LoginParams {
    pub username: String,
    pub password: String,
    pub captcha: String,
    pub totp_code: Option<String>,
}

impl AppService {
    pub const TOTP_KEY: &'static str = "totp_key";
    pub async fn auth_login(
        &self,
        params: LoginParams,
        context: Session,
    ) -> Result<(), AppError> {
        self.auth_check_captcha(&context, params.captcha).await?;
        let password = self.auth_rsa_decode(&context, params.password).await?;
        let user = match self.auth_find_user_by_username(&params.username).await
        {
            Ok(user) => user,
            Err(_) => {
                match self.auth_find_user_by_email(&params.username).await {
                    Ok(user) => user,
                    Err(_) => {
                        let _ = Argon2::default()
                            .hash_password(password.as_bytes());
                        return Err(AppError::UserNotFound);
                    }
                }
            }
        };

        let user_password = sqlx::query_as::<_, UserPasswordModel>(
            "SELECT \"user\", hash, salt, is_active, reason, created_at, updated_at \
             FROM user_password WHERE \"user\" = $1 AND is_active = true",
        )
        .bind(user.id)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::UserNotFound)?;

        let password_hash = PasswordHash::new(&user_password.hash)
            .map_err(|_| AppError::UserNotFound)?;
        if Argon2::default()
            .verify_password(password.as_bytes(), &password_hash)
            .is_err()
        {
            tracing::warn!(username = %params.username, ip = ?context.ip_address(), "Login failed: invalid password");
            return Err(AppError::UserNotFound);
        }

        if context
            .get::<String>(Self::TOTP_KEY)
            .ok()
            .flatten()
            .is_some()
        {
            if let Some(ref totp_code) = params.totp_code {
                if !self.auth_2fa_verify_login(&context, totp_code).await? {
                    return Err(AppError::InvalidTwoFactorCode);
                }
            } else {
                return Err(AppError::InvalidTwoFactorCode);
            }
        } else if self.auth_2fa_status_by_uid(user.id).await?.is_enabled {
            let totp_session_key = uuid::Uuid::new_v4().to_string();
            context
                .insert(Self::TOTP_KEY, totp_session_key.clone())
                .map_err(|_| AppError::InternalError)?;
            self.cache
                .set(&totp_session_key, &user.id)
                .await
                .map_err(|e| AppError::InternalServerError(e.to_string()))?;
            tracing::info!(username = %params.username, ip = ?context.ip_address(), "Login 2FA triggered");
            return Err(AppError::TwoFactorRequired);
        }

        sqlx::query("UPDATE \"user\" SET last_sign_in_at = $1, updated_at = $1 WHERE id = $2")
            .bind(chrono::Utc::now())
            .bind(user.id)
            .execute(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        context.renew();
        context.set_user(user.id);
        context.remove(Self::RSA_PRIVATE_KEY);
        context.remove(Self::RSA_PUBLIC_KEY);
        tracing::info!(user_uid = %user.id, username = %user.username, ip = ?context.ip_address(), "User logged in successfully");
        Ok(())
    }

    pub async fn auth_find_user_by_username(
        &self,
        username: &str,
    ) -> Result<UserModel, AppError> {
        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(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::UserNotFound)
    }

    pub async fn auth_find_user_by_email(
        &self,
        email: &str,
    ) -> Result<UserModel, AppError> {
        sqlx::query_as::<_, UserModel>(
            "SELECT u.id, u.username, u.display_name, u.avatar_url, u.website_url, u.allow_use, \
                    u.can_search, u.last_sign_in_at, u.created_at, u.updated_at \
             FROM \"user\" u \
             INNER JOIN user_email e ON e.\"user\" = u.id \
             WHERE e.email = $1 AND e.active = true",
        )
        .bind(email)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::UserNotFound)
    }

    pub async fn auth_find_user_by_uid(
        &self,
        uid: uuid::Uuid,
    ) -> Result<UserModel, AppError> {
        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 id = $1",
        )
        .bind(uid)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::UserNotFound)
    }

    pub fn validate_password_strength(password: &str) -> Result<(), AppError> {
        if password.len() < 8 {
            return Err(AppError::PasswordTooWeak);
        }

        let has_uppercase = password.chars().any(|c| c.is_uppercase());
        let has_lowercase = password.chars().any(|c| c.is_lowercase());
        let has_digit = password.chars().any(|c| c.is_numeric());

        if !has_uppercase || !has_lowercase || !has_digit {
            return Err(AppError::PasswordTooWeak);
        }
        Ok(())
    }
}
