use actix_remote_ip::RemoteIP;
use actix_web::web::Data;
use actix_web::{HttpResponse, Responder, web};
use light_openid::basic_state_manager::BasicStateManager;

use crate::app_config::AppConfig;
use crate::controllers::HttpResult;
use crate::extractors::auth_extractor::AuthExtractor;
use crate::extractors::local_auth_extractor::LocalAuthEnabled;

#[derive(serde::Deserialize)]
pub struct LocalAuthReq {
    username: String,
    password: String,
}

/// Perform local authentication
pub async fn local_auth(
    local_auth_enabled: LocalAuthEnabled,
    req: web::Json<LocalAuthReq>,
    auth: AuthExtractor,
) -> impl Responder {
    if !*local_auth_enabled {
        log::error!("Local auth attempt while this authentication method is disabled!");
        return HttpResponse::UnprocessableEntity().json("Local authentication is disabled!");
    }

    if !AppConfig::get().check_local_login(&req.username, &req.password) {
        log::error!("Local auth attempt with invalid credentials!");
        return HttpResponse::Unauthorized().json("Invalid credentials!");
    }

    auth.authenticate(&req.username);

    HttpResponse::Accepted().json("Welcome")
}

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

/// Start OIDC authentication
pub async fn start_oidc(sm: Data<BasicStateManager>, ip: RemoteIP) -> HttpResult {
    let prov = match AppConfig::get().openid_provider() {
        None => {
            return Ok(HttpResponse::UnprocessableEntity().json("OpenID is disabled!"));
        }
        Some(conf) => conf,
    };

    let conf =
        light_openid::primitives::OpenIDConfig::load_from_url(prov.configuration_url).await?;

    Ok(HttpResponse::Ok().json(StartOIDCResponse {
        url: conf.gen_authorization_url(
            prov.client_id,
            &sm.gen_state(ip.0)?,
            &AppConfig::get().oidc_redirect_url(),
        ),
    }))
}

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

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

    let prov = match AppConfig::get().openid_provider() {
        None => {
            return Ok(HttpResponse::UnprocessableEntity().json("OpenID is disabled!"));
        }
        Some(conf) => conf,
    };

    let conf =
        light_openid::primitives::OpenIDConfig::load_from_url(prov.configuration_url).await?;

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

    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!"));
        }
    };

    auth.authenticate(mail);

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

#[derive(serde::Serialize)]
struct CurrentUser {
    id: String,
}

/// Get current authenticated user
pub async fn current_user(auth: AuthExtractor) -> impl Responder {
    HttpResponse::Ok().json(CurrentUser {
        id: auth.id().unwrap_or_else(|| "Anonymous".to_string()),
    })
}

/// Sign out
pub async fn sign_out(auth: AuthExtractor) -> impl Responder {
    auth.sign_out();
    HttpResponse::Ok().finish()
}