use session::Session;
use utoipa::{IntoParams, ToSchema};

use crate::{
    AppService, auth::rsa::RsaResponse, constant_time_eq, error::AppError,
};

#[derive(
    serde::Deserialize, serde::Serialize, Clone, Debug, ToSchema, IntoParams,
)]
pub struct CaptchaQuery {
    pub w: u32,
    pub h: u32,
    pub dark: bool,
    pub rsa: bool,
}

#[derive(serde::Serialize, ToSchema)]
pub struct CaptchaResponse {
    pub base64: String,
    pub rsa: Option<RsaResponse>,
    pub req: CaptchaQuery,
}

impl AppService {
    const CAPTCHA_KEY: &'static str = "captcha";
    const CAPTCHA_LENGTH: usize = 4;
    pub async fn auth_captcha(
        &self,
        context: &Session,
        query: CaptchaQuery,
    ) -> Result<CaptchaResponse, AppError> {
        let CaptchaQuery { w, h, dark, rsa } = query;
        let captcha = captcha_rs::CaptchaBuilder::new()
            .width(w)
            .height(h)
            .dark_mode(dark)
            .length(Self::CAPTCHA_LENGTH)
            .build();

        let base64 = captcha.to_base64();

        let text = captcha.text;
        context.insert(Self::CAPTCHA_KEY, text).ok();
        Ok(CaptchaResponse {
            base64,
            rsa: if rsa {
                Some(self.auth_rsa(context).await?)
            } else {
                None
            },
            req: CaptchaQuery { w, h, dark, rsa },
        })
    }
    pub async fn auth_check_captcha(
        &self,
        context: &Session,
        captcha: String,
    ) -> Result<(), AppError> {
        let text = context
            .get::<String>(Self::CAPTCHA_KEY)
            .map_err(|_| AppError::CaptchaError)?
            .ok_or(AppError::CaptchaError)?;
        if !constant_time_eq(&text.to_lowercase(), &captcha.to_lowercase()) {
            context.remove(Self::CAPTCHA_KEY);
            tracing::warn!(ip = ?context.ip_address(), "Captcha verification failed");
            return Err(AppError::CaptchaError);
        }
        context.remove(Self::CAPTCHA_KEY);
        Ok(())
    }
}
