use argon2::{Argon2, PasswordHasher};
use chrono::{Duration, Utc};
use db::sqlx;
use email::EmailMessage;
use rand::{RngExt, distr::Alphanumeric};
use serde::{Deserialize, Serialize};
use session::Session;

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

#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)]
pub struct ResetPasswordRequest {
    pub email: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)]
pub struct ResetPasswordVerifyParams {
    pub token: String,
    pub password: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct PendingResetPassword {
    user_uid: uuid::Uuid,
    created_at: chrono::DateTime<Utc>,
}

impl AppService {
    const RESET_PASS_PREFIX: &'static str = "auth:reset_pass:";
    const RESET_PASS_EXPIRY_HOURS: i64 = 1;
    pub async fn auth_reset_password_request(
        &self,
        params: ResetPasswordRequest,
    ) -> Result<(), AppError> {
        let user = self.auth_find_user_by_email(&params.email).await.ok();

        if let Some(user) = user {
            let token = Self::generate_reset_token();
            let cache_key = format!("{}{}", Self::RESET_PASS_PREFIX, token);
            let now = chrono::Utc::now();

            if let Err(e) = self
                .cache
                .set(
                    &cache_key,
                    &PendingResetPassword {
                        user_uid: user.id,
                        created_at: now,
                    },
                )
                .await
            {
                tracing::error!(error = %e, user_uid = %user.id, "Failed to cache reset token");
                return Ok(());
            }

            let domain = match self.config.main_domain() {
                Ok(d) => d,
                Err(e) => {
                    tracing::error!(error = %e, "Domain not configured for password reset");
                    return Ok(());
                }
            };
            let reset_link =
                format!("{}/auth/reset-password?token={}", domain, token);

            if let Err(e) = self
                .email
                .send(EmailMessage {
                    to: params.email.clone(),
                    subject: "Reset Your Password".to_string(),
                    body: format!(
                        "You requested to reset your GitDataAI password.\n\n\
                         Reset your password here:\n\n{}\n\n\
                         If you did not request this, ignore this email.",
                        reset_link
                    ),
                })
                .await
            {
                tracing::error!(error = %e, email = %params.email, "Failed to queue password reset email");
            }

            tracing::info!(email = %params.email, user_uid = %user.id, "Password reset email queued");
        }

        Ok(())
    }

    pub async fn auth_reset_password_verify(
        &self,
        context: &Session,
        params: ResetPasswordVerifyParams,
    ) -> Result<(), AppError> {
        if params.token.is_empty() {
            return Err(AppError::InvalidResetToken);
        }

        let cache_key = format!("{}{}", Self::RESET_PASS_PREFIX, params.token);
        let pending = self
            .cache
            .get::<PendingResetPassword>(&cache_key)
            .await
            .map_err(|e| AppError::InternalServerError(e.to_string()))?
            .ok_or(AppError::InvalidResetToken)?;

        if Utc::now() - pending.created_at
            > Duration::hours(Self::RESET_PASS_EXPIRY_HOURS)
        {
            let _ = self.cache.remove(&cache_key).await;
            return Err(AppError::ResetTokenExpired);
        }

        self.cache
            .remove(&cache_key)
            .await
            .map_err(|e| AppError::InternalServerError(e.to_string()))?;

        let password = self.auth_rsa_decode(context, params.password).await?;
        Self::validate_password_strength(&password)?;

        let password_hash = Argon2::default()
            .hash_password(password.as_bytes())
            .map_err(|e| AppError::PasswordHashError(e.to_string()))?
            .to_string();

        let now = chrono::Utc::now();
        let result = sqlx::query(
            "UPDATE user_password SET hash = $1, updated_at = $2 WHERE \"user\" = $3 AND is_active = true",
        )
        .bind(&password_hash)
        .bind(now)
        .bind(pending.user_uid)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        if result.rows_affected() == 0 {
            return Err(AppError::InvalidResetToken);
        }

        tracing::info!(user_uid = %pending.user_uid, "Password reset successfully");
        Ok(())
    }

    fn generate_reset_token() -> String {
        #[allow(deprecated)]
        let mut rng = rand::rng();
        let token: String =
            (0..64).map(|_| rng.sample(Alphanumeric) as char).collect();
        format!("rst_{}", token)
    }
}
