use std::sync::Arc;

use axum::{Json, Router, extract::State, routing::post};
use kube::api::{Api, ListParams};
use kube::core::DynamicObject;
use kube::core::admission::{AdmissionRequest, AdmissionResponse, AdmissionReview};
use sqlx::PgPool;

struct AppState {
    db: PgPool,
    kube: kube::Client,
    namespace: String,
}

async fn validate(
    State(state): State<Arc<AppState>>,
    Json(review): Json<AdmissionReview<DynamicObject>>,
) -> Json<AdmissionReview<DynamicObject>> {
    let req: AdmissionRequest<DynamicObject> = match review.try_into() {
        Ok(r) => r,
        Err(e) => {
            let resp = AdmissionResponse::invalid(format!("invalid admission request: {e}"));
            return Json(resp.into_review());
        }
    };

    let resp = match validate_bouncer(&state, &req).await {
        Ok(resp) => resp,
        Err(e) => {
            tracing::error!("validation failed: {e}");
            AdmissionResponse::invalid(e.to_string()).into_review()
        }
    };

    Json(resp)
}

async fn validate_bouncer(
    state: &AppState,
    req: &AdmissionRequest<DynamicObject>,
) -> anyhow::Result<AdmissionReview<DynamicObject>> {
    let obj = req.object.as_ref().ok_or_else(|| anyhow::anyhow!("missing object"))?;
    let labels = obj.metadata.labels.clone().unwrap_or_default();

    let owner = match labels.get("irc.now/owner") {
        Some(o) => o,
        None => return Ok(AdmissionResponse::from(req).into_review()),
    };

    let plan: Option<(String,)> =
        sqlx::query_as("SELECT plan FROM users WHERE keycloak_sub = $1")
            .bind(owner)
            .fetch_optional(&state.db)
            .await?;

    let plan = match plan {
        Some((p,)) => p,
        None => return Ok(AdmissionResponse::from(req).deny("user not found").into_review()),
    };

    if plan != "pro" {
        return Ok(AdmissionResponse::from(req)
            .deny("upgrade to pro to create a dedicated bouncer")
            .into_review());
    }

    let ar = kube::core::ApiResource {
        group: "irc.now".into(),
        version: "v1alpha1".into(),
        api_version: "irc.now/v1alpha1".into(),
        kind: "SojuBouncer".into(),
        plural: "sojubouncers".into(),
    };
    let bouncers: Api<DynamicObject> =
        Api::namespaced_with(state.kube.clone(), &state.namespace, &ar);
    let lp = ListParams::default().labels(&format!("irc.now/owner={owner}"));
    let count = bouncers
        .list(&lp)
        .await
        .map(|l| l.items.len())
        .unwrap_or(0);

    if count >= 1 {
        return Ok(AdmissionResponse::from(req)
            .deny("max 1 dedicated bouncer per pro account")
            .into_review());
    }

    Ok(AdmissionResponse::from(req).into_review())
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    let db_config = irc_now_common::db::DbConfig::from_env("ACCOUNTS");
    let db = db_config.connect().await.expect("database connection failed");

    let kube = kube::Client::try_default()
        .await
        .expect("kube client init failed");

    let namespace =
        std::env::var("NAMESPACE").unwrap_or_else(|_| "irc-josie-cloud".to_string());

    let state = Arc::new(AppState {
        db,
        kube,
        namespace,
    });

    let app = Router::new()
        .route("/validate", post(validate))
        .with_state(state);

    let cert = std::fs::read("/etc/webhook/tls/tls.crt").expect("reading TLS cert");
    let key = std::fs::read("/etc/webhook/tls/tls.key").expect("reading TLS key");

    let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem(cert, key)
        .await
        .expect("TLS config failed");

    let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 8443));
    tracing::info!("listening on {addr}");
    axum_server::bind_rustls(addr, tls_config)
        .serve(app.into_make_service())
        .await
        .expect("server failed");
}
