//! LDAP bind authentication for the dashboard.
//!
//! Posts a `{username, password}` JSON body; we substitute the username
//! into the configured bind DN template and try a simple bind. On
//! success we upsert a local user record (so subsequent dashboard
//! actions have something to attach permissions to) and hand back a
//! session cookie identical to the OAuth flow.

use axum::{
  Json,
  Router,
  extract::State,
  http::{StatusCode, header},
  response::{IntoResponse, Response},
  routing::post,
};
use circus_common::{models::UserType, repo};
use serde::Deserialize;

use crate::{
  error::ApiError,
  session_cookie::user_session_cookie,
  state::AppState,
};

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

fn escape_ldap_value(input: &str) -> String {
  ldap3::dn_escape(input).into_owned()
}

async fn ldap_login(
  State(state): State<AppState>,
  Json(body): Json<LdapLoginRequest>,
) -> Result<Response, ApiError> {
  if body.username.trim().is_empty() || body.password.is_empty() {
    return Ok(StatusCode::BAD_REQUEST.into_response());
  }

  let Some(ldap_cfg) = state.config.server.ldap.as_ref() else {
    return Ok(StatusCode::NOT_FOUND.into_response());
  };
  if !ldap_cfg.enabled {
    return Ok(StatusCode::NOT_FOUND.into_response());
  }

  let escaped = escape_ldap_value(&body.username);
  let bind_dn = ldap_cfg.bind_dn_template.replace("{username}", &escaped);

  let bind_result = ldap3::LdapConnAsync::new(&ldap_cfg.url).await;
  let (conn, mut ldap) = match bind_result {
    Ok(pair) => pair,
    Err(e) => {
      tracing::debug!("LDAP connect failed: {e}");
      return Ok(StatusCode::SERVICE_UNAVAILABLE.into_response());
    },
  };
  ldap3::drive!(conn);

  let bind = ldap.simple_bind(&bind_dn, &body.password).await;
  if let Err(e) = ldap.unbind().await {
    tracing::warn!("LDAP unbind failed: {e}");
  }

  match bind {
    Ok(res) if res.rc == 0 => {},
    Ok(res) => {
      tracing::debug!(rc = res.rc, "LDAP bind rejected");
      crate::audit::record_with_actor(
        &state.pool,
        &circus_common::audit::Actor::anonymous(),
        None,
        "LOGIN_FAILURE",
        Some("ldap"),
        Some(&body.username),
        serde_json::json!({ "reason": "bind_rejected", "rc": res.rc }),
      )
      .await;
      return Ok(StatusCode::UNAUTHORIZED.into_response());
    },
    Err(e) => {
      tracing::debug!("LDAP bind error: {e}");
      crate::audit::record_with_actor(
        &state.pool,
        &circus_common::audit::Actor::anonymous(),
        None,
        "LOGIN_FAILURE",
        Some("ldap"),
        Some(&body.username),
        serde_json::json!({ "reason": "bind_error" }),
      )
      .await;
      return Ok(StatusCode::UNAUTHORIZED.into_response());
    },
  }

  // Treat the username as the LDAP-side identity for upsert. This mirrors
  // OAuth's "use the provider id to disambiguate users with the same login".
  let user = repo::users::upsert_oauth_user(
    &state.pool,
    &body.username,
    None,
    UserType::Ldap,
    "ldap",
    state.email_regex.as_deref(),
  )
  .await?;

  let session = repo::users::create_session(&state.pool, user.id).await?;

  crate::audit::record_with_actor(
    &state.pool,
    &circus_common::audit::Actor::user(user.id, user.username.clone()),
    None,
    "LOGIN_SUCCESS",
    Some("ldap"),
    Some(&user.id.to_string()),
    serde_json::json!({ "method": "ldap" }),
  )
  .await;

  let cookie = user_session_cookie(&session.0, &state.config.server);

  Ok(
    Response::builder()
      .status(StatusCode::NO_CONTENT)
      .header(header::SET_COOKIE, cookie)
      .body(axum::body::Body::empty())
      .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
  )
}

pub fn router() -> Router<AppState> {
  Router::new().route("/auth/ldap", post(ldap_login))
}
