//! HTTP API: router, handlers, and OpenAPI document.

mod admin;
mod admin_authorities;
mod admin_objects;
mod admin_search;
mod admin_vocab;
mod config;
mod health;
mod openapi;
mod pagination;
mod public;

use axum::Router;
use db::Db;
use time::Duration;
use tower_sessions::cookie::SameSite;
use tower_sessions::{Expiry, SessionManagerLayer};
use tower_sessions_sqlx_store::PostgresStore;

/// Shared application state passed to handlers.
#[derive(Clone)]
pub struct AppState {
    /// Database handle for this organization.
    pub db: Db,
    /// User-facing product name (from config). Never hardcoded.
    pub app_name: String,
    /// Whether the session cookie carries the `Secure` attribute (default true;
    /// disable only for plain-HTTP self-hosting).
    pub cookie_secure: bool,
    /// Search client for on-write index sync. `None` disables indexing (search is a
    /// best-effort feature; absent when Meilisearch is not configured).
    pub search: Option<search::SearchClient>,
    /// Instance default UI/content language (from config).
    pub default_language: String,
    /// Instance default display timezone, IANA name (from config). Storage stays UTC.
    pub default_timezone: String,
}

/// Best-effort: keep the search index in step with a catalogue write that has already
/// committed. Re-projects and indexes the object, or removes it if it no longer exists.
/// Never fails the request — a search outage must not undo a committed write, and
/// `reindex_all` is the recovery path. A no-op when search is not configured.
pub(crate) async fn reindex(state: &AppState, id: domain::ObjectId) {
    let Some(search) = &state.search else {
        return;
    };

    if let Err(err) = search.sync_object(&state.db, id).await {
        tracing::error!(?err, object_id = %id, "search reindex after write failed");
    }
}

/// Build the application router from shared state.
pub fn build_app(state: AppState) -> Router {
    let store = PostgresStore::new(state.db.pool().clone());

    let session_layer = SessionManagerLayer::new(store)
        .with_name("id")
        .with_http_only(true)
        .with_secure(state.cookie_secure)
        .with_same_site(SameSite::Strict)
        .with_expiry(Expiry::OnInactivity(Duration::hours(8)));

    Router::new()
        .merge(config::routes())
        .merge(health::routes())
        .merge(openapi::routes())
        .merge(public::routes())
        .merge(admin::routes())
        .merge(admin_objects::routes())
        .merge(admin_vocab::routes())
        .merge(admin_search::routes())
        .merge(admin_authorities::routes())
        .layer(session_layer)
        .with_state(state)
}

/// Create the session store's table if absent. Run once at startup (and in tests
/// before exercising auth). Separate from `Db::migrate` — the session library's own
/// bookkeeping table.
pub async fn migrate_sessions(db: &Db) -> Result<(), sqlx::Error> {
    PostgresStore::new(db.pool().clone()).migrate().await
}
