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

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

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

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

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct EmailResponse {
    pub email: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
struct PendingEmailChange {
    user_uid: uuid::Uuid,
    new_email: String,
}

impl AppService {
    const EMAIL_CHANGE_PREFIX: &'static str = "auth:email_change:";

    pub async fn auth_get_email(
        &self,
        ctx: &Session,
    ) -> Result<EmailResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let email = sqlx::query_as::<_, UserEmailModel>(
            "SELECT \"user\", email, created_at, active, last_use_login, updated_at \
             FROM user_email WHERE \"user\" = $1 AND active = true",
        )
        .bind(user_uid)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(EmailResponse {
            email: email.map(|e| e.email),
        })
    }

    pub async fn auth_email_change_request(
        &self,
        ctx: &Session,
        params: EmailChangeRequest,
    ) -> Result<(), AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let password = self.auth_rsa_decode(ctx, params.password).await?;

        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_uid)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::UserNotFound)?;

        let hash = PasswordHash::new(&user_password.hash)
            .map_err(|_| AppError::UserNotFound)?;
        Argon2::default()
            .verify_password(password.as_bytes(), &hash)
            .map_err(|_| AppError::InvalidPassword)?;

        let existing = sqlx::query_as::<_, UserEmailModel>(
            "SELECT \"user\", email, created_at, active, last_use_login, updated_at \
             FROM user_email WHERE email = $1 AND active = true",
        )
        .bind(&params.new_email)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        if existing.is_some() {
            return Err(AppError::EmailExists);
        }

        let token = Self::generate_email_change_token();
        let cache_key = format!("{}{}", Self::EMAIL_CHANGE_PREFIX, token);
        self.cache
            .set(
                &cache_key,
                &PendingEmailChange {
                    user_uid,
                    new_email: params.new_email.clone(),
                },
            )
            .await
            .map_err(|e| AppError::InternalServerError(e.to_string()))?;

        let domain = self
            .config
            .main_domain()
            .map_err(|_| AppError::DoMainNotSet)?;
        let verify_link =
            format!("{}/auth/verify-email?token={}", domain, token);

        self.email
            .send(EmailMessage {
                to: params.new_email.clone(),
                subject: "Confirm Email Change".to_string(),
                body: format!(
                    "You requested to change your GitDataAI email address.\n\n\
                     Confirm the change here:\n\n{}\n\n\
                     If you did not request this change, ignore this email.",
                    verify_link
                ),
            })
            .await
            .map_err(|e| {
                tracing::error!(error = %e, new_email = %params.new_email, "Failed to queue email change verification");
                AppError::InternalServerError(e.to_string())
            })?;

        tracing::info!(new_email = %params.new_email, user_uid = %user_uid, "Email change verification queued");
        Ok(())
    }

    pub async fn auth_email_verify(
        &self,
        params: EmailVerifyRequest,
    ) -> Result<(), AppError> {
        if params.token.is_empty() {
            return Err(AppError::BadRequest(
                "missing email verification token".to_string(),
            ));
        }
        let cache_key =
            format!("{}{}", Self::EMAIL_CHANGE_PREFIX, params.token);
        let pending = self
            .cache
            .get::<PendingEmailChange>(&cache_key)
            .await
            .map_err(|e| AppError::InternalServerError(e.to_string()))?
            .ok_or(AppError::NotFound(
                "invalid or expired email verification token".to_string(),
            ))?;

        let existing = sqlx::query_as::<_, UserEmailModel>(
            "SELECT \"user\", email, created_at, active, last_use_login, updated_at \
             FROM user_email WHERE email = $1 AND active = true",
        )
        .bind(&pending.new_email)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        if existing.is_some() {
            return Err(AppError::EmailExists);
        }

        let now = chrono::Utc::now();
        let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?;
        sqlx::query("UPDATE user_email SET active = false, updated_at = $1 WHERE \"user\" = $2")
            .bind(now)
            .bind(pending.user_uid)
            .execute(&mut **txn.inner_mut())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        sqlx::query(
            "INSERT INTO user_email (\"user\", email, created_at, active, last_use_login, updated_at) \
             VALUES ($1, $2, $3, true, NULL, $3)",
        )
        .bind(pending.user_uid)
        .bind(&pending.new_email)
        .bind(now)
        .execute(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        txn.commit().await.map_err(|_| AppError::TxnError)?;

        let _ = self.cache.remove(&cache_key).await;
        tracing::info!(new_email = %pending.new_email, user_uid = %pending.user_uid, "Email changed successfully");
        Ok(())
    }

    fn generate_email_change_token() -> String {
        use rand::{RngExt, distr::Alphanumeric};

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