use crate::app_config::AppConfig;
use crate::controllers::{HttpFailure, HttpResult};
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
use crate::extractors::money_session::MoneySession;
use crate::services::{tokens_service, users_service};
use actix_remote_ip::RemoteIP;
use actix_web::{HttpResponse, web};
use light_openid::primitives::OpenIDConfig;

#[derive(serde::Serialize)]
struct StartOIDCResponse {
    url: String,
}

/// Start OIDC authentication
pub async fn start_oidc(session: MoneySession, remote_ip: RemoteIP) -> HttpResult {
    let prov = AppConfig::get().openid_provider();

    let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await {
        Ok(c) => c,
        Err(e) => {
            log::error!("Failed to fetch OpenID provider configuration! {e}");
            return Ok(HttpResponse::InternalServerError()
                .json("Failed to fetch OpenID provider configuration!"));
        }
    };

    let state = match session.gen_oidc_state(remote_ip.0) {
        Ok(s) => s,
        Err(e) => {
            log::error!("Failed to generate auth state! {e}");
            return Ok(HttpResponse::InternalServerError().json("Failed to generate auth state!"));
        }
    };

    Ok(HttpResponse::Ok().json(StartOIDCResponse {
        url: conf.gen_authorization_url(
            prov.client_id,
            &state,
            &AppConfig::get().oidc_redirect_url(),
        ),
    }))
}

#[derive(serde::Deserialize)]
pub struct FinishOpenIDLoginQuery {
    code: String,
    state: String,
}

/// Finish OIDC authentication
pub async fn finish_oidc(
    session: MoneySession,
    remote_ip: RemoteIP,
    req: web::Json<FinishOpenIDLoginQuery>,
) -> HttpResult {
    if let Err(e) = session.validate_state(&req.state, remote_ip.0) {
        log::error!("Failed to validate OIDC CB state! {e}");
        return Ok(HttpResponse::BadRequest().json("Invalid state!"));
    }

    let prov = AppConfig::get().openid_provider();

    let conf = OpenIDConfig::load_from_url(prov.configuration_url)
        .await
        .map_err(HttpFailure::OpenID)?;

    let (token, _) = conf
        .request_token(
            prov.client_id,
            prov.client_secret,
            &req.code,
            &AppConfig::get().oidc_redirect_url(),
        )
        .await
        .map_err(HttpFailure::OpenID)?;
    let (user_info, _) = conf
        .request_user_info(&token)
        .await
        .map_err(HttpFailure::OpenID)?;

    if user_info.email_verified != Some(true) {
        log::error!("Email is not verified!");
        return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
    }

    let mail = match user_info.email {
        Some(m) => m,
        None => {
            return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
        }
    };

    let user_name = user_info.name.unwrap_or_else(|| {
        format!(
            "{} {}",
            user_info.given_name.as_deref().unwrap_or(""),
            user_info.family_name.as_deref().unwrap_or("")
        )
    });

    let user = users_service::create_or_update_user(&mail, &user_name).await?;

    session.set_user(&user)?;

    Ok(HttpResponse::Ok().finish())
}

/// Get current user information
pub async fn auth_info(auth: AuthExtractor) -> HttpResult {
    Ok(HttpResponse::Ok().json(auth.user))
}

/// Sign out user
pub async fn sign_out(auth: AuthExtractor, session: MoneySession) -> HttpResult {
    match auth.method {
        AuthenticatedMethod::Cookie => {
            session.unset_current_user()?;
        }

        AuthenticatedMethod::Token(token) => {
            tokens_service::delete(token.user_id(), token.id()).await?;
        }

        AuthenticatedMethod::Dev => {
            // Nothing to be done, user is always authenticated
        }
    }

    Ok(HttpResponse::NoContent().finish())
}
