//! `POST /api/oidc/auth` — start the device-flow login.

use crate::api::error::ApiError;
use crate::api::oidc::{discovery, random_token, require_provider, OIDC_SESSION_TTL_SECS};
use crate::api::state::AppState;
use crate::database::OidcSessionInsert;
use axum::extract::Extension;
use axum::Json;
use serde::Deserialize;
use serde_json::{json, Value};
use std::sync::Arc;

#[derive(Debug, Deserialize)]
pub struct AuthBody {
    /// Provider short-name from `oidc_providers.name`. The Flutter client
    /// sends this from the `op` field of the OIDC dialog.
    #[serde(default)]
    pub op: String,
    #[serde(default)]
    pub id: String,
    #[serde(default)]
    pub uuid: String,
    #[serde(default, rename = "deviceInfo")]
    pub device_info: Option<Value>,
}

pub async fn auth(
    Extension(state): Extension<Arc<AppState>>,
    Json(body): Json<AuthBody>,
) -> Result<Json<Value>, ApiError> {
    if state.cfg.public_base_url.is_empty() {
        return Err(ApiError::Internal(
            "OIDC requires --public-base-url to be set".into(),
        ));
    }
    if body.op.is_empty() {
        return Err(ApiError::BadRequest("op (provider name) required".into()));
    }
    let provider = require_provider(&state, &body.op).await?;
    let disc = discovery::discover(&provider.issuer_url)
        .await
        .map_err(ApiError::Internal)?;

    let code = random_token();
    let csrf_state = random_token();
    let device_info_json = body
        .device_info
        .as_ref()
        .map(|v| v.to_string())
        .unwrap_or_else(|| "{}".to_string());

    let expires_at = chrono::Utc::now().timestamp() + OIDC_SESSION_TTL_SECS;
    state
        .db
        .oidc_session_create(&OidcSessionInsert {
            code: &code,
            provider: &provider.name,
            state: &csrf_state,
            client_id_str: &body.id,
            client_uuid: &body.uuid,
            device_info_json: &device_info_json,
            expires_at,
        })
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;

    // Build the IdP authorization URL.
    let url = format!(
        "{auth}?response_type=code&client_id={cid}&redirect_uri={ru}&scope={scope}&state={state}",
        auth = disc.authorization_endpoint,
        cid = url_encode(&provider.client_id),
        ru = url_encode(&provider.redirect_url),
        scope = url_encode(&provider.scopes),
        state = url_encode(&csrf_state),
    );

    Ok(Json(json!({
        "code": code,
        "url":  url,
    })))
}

/// Inline percent-encoder for the auth URL query string. See
/// `api::twofa::url_encode` for the same routine.
fn url_encode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.as_bytes() {
        match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(*b as char);
            }
            _ => {
                use std::fmt::Write;
                let _ = write!(out, "%{:02X}", b);
            }
        }
    }
    out
}
