use crate::AppService;
use crate::error::AppError;
use argon2::password_hash::{Salt, SaltString};
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use models::users::{user_activity_log, user_password, user_password_reset};
use rand::RngExt;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use uuid::Uuid;

#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct ChangePasswordParams {
    pub old_password: String,
    pub new_password: String,
}

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

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

impl AppService {
    pub async fn auth_change_password(
        &self,
        context: &Session,
        params: ChangePasswordParams,
    ) -> Result<(), AppError> {
        let user_uid = context.user().ok_or(AppError::Unauthorized)?;

        let old_password = self.auth_rsa_decode(context, params.old_password).await?;
        let new_password = self.auth_rsa_decode(context, params.new_password).await?;

        Self::validate_password_strength(&new_password)?;

        let user_password = user_password::Entity::find()
            .filter(user_password::Column::User.eq(user_uid))
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .ok_or(AppError::UserNotFound)?;

        let password_hash =
            PasswordHash::new(&user_password.password_hash).map_err(|_| AppError::UserNotFound)?;

        Argon2::default()
            .verify_password(old_password.as_bytes(), &password_hash)
            .map_err(|_| AppError::UserNotFound)?;

        let salt = SaltString::generate(&mut rsa::rand_core::OsRng::default());
        let new_password_hash = Argon2::default()
            .hash_password(new_password.as_bytes(), Salt::from_b64(&*salt.to_string())?)
            .map_err(|_| AppError::UserNotFound)?
            .to_string();

        let mut active_password: user_password::ActiveModel = user_password.into();
        active_password.password_hash = Set(new_password_hash);
        active_password.password_salt = Set(Some(salt.to_string()));

        active_password
            .update(&self.db)
            .await
            .map_err(|_| AppError::UserNotFound)?;

        slog::info!(self.logs, "Password changed"; "user_uid" => %user_uid, "ip" => context.ip_address());
        let _ = user_activity_log::ActiveModel {
            user_uid: Set(Option::from(user_uid)),
            action: Set("password_change".to_string()),
            ip_address: Set(context.ip_address()),
            user_agent: Set(context.user_agent()),
            details: Set(serde_json::json!({
                "method": "change_password"
            })),
            created_at: Set(chrono::Utc::now()),
            ..Default::default()
        }
        .insert(&self.db)
        .await;

        Ok(())
    }

    pub async fn auth_request_password_reset(
        &self,
        params: ResetPasswordParams,
    ) -> Result<(), AppError> {
        let user = self.utils_find_user_by_email(params.email.clone()).await?;

        let token = self.generate_reset_token();

        let expires_at = chrono::Utc::now() + chrono::Duration::hours(1);

        let _ = user_password_reset::Entity::delete_many()
            .filter(user_password_reset::Column::UserUid.eq(user.uid))
            .filter(user_password_reset::Column::Used.eq(false))
            .exec(&self.db)
            .await;

        let reset_token = user_password_reset::ActiveModel {
            token: Set(token.clone()),
            user_uid: Set(user.uid),
            expires_at: Set(expires_at),
            used: Set(false),
            created_at: Set(chrono::Utc::now()),
        };

        reset_token
            .insert(&self.db)
            .await
            .map_err(|_| AppError::UserNotFound)?;

        let domain = self
            .config
            .main_domain()
            .map_err(|_| AppError::DoMainNotSet)?;

        let email_address = params.email.clone();
        let reset_link = format!("https://{}/auth/reset-password?token={}", domain, token);

        let envelope = queue::EmailEnvelope {
            id: Uuid::new_v4(),
            to: email_address.clone(),
            subject: "Password Reset Request".to_string(),
            body: format!(
                "Hello {},\n\n\
                You have requested to reset your password. Please click the link below to reset your password:\n\n\
                {}\n\n\
                This link will expire in 1 hour.\n\n\
                If you did not request this password reset, please ignore this email.\n\n\
                Best regards,\n\
                GitDataAI Team",
                user.username, reset_link
            ),
            created_at: chrono::Utc::now(),
        };

        self.queue_producer
            .publish_email(envelope)
            .await
            .map_err(|_| AppError::UserNotFound)?;

        slog::info!(self.logs, "Password reset email queued"; "email" => email_address);
        Ok(())
    }
    pub fn validate_password_strength(password: &str) -> Result<(), AppError> {
        if password.len() < 8 {
            return Err(AppError::UserNotFound);
        }

        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::UserNotFound);
        }

        Ok(())
    }

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

    pub async fn auth_cleanup_expired_reset_tokens(&self) -> Result<u64, AppError> {
        let now = chrono::Local::now().naive_local();

        let result = user_password_reset::Entity::delete_many()
            .filter(user_password_reset::Column::ExpiresAt.lt(now))
            .exec(&self.db)
            .await
            .map_err(|_| AppError::UserNotFound)?;

        slog::info!(self.logs, "Expired password reset tokens cleaned up"; "count" => result.rows_affected);
        Ok(result.rows_affected)
    }
}
