//! Server wiring: configuration and startup.

mod config;

#[cfg(feature = "embed-web")]
mod web_assets;

pub use config::Config;

use anyhow::Context;
use api::{AppState, build_app, migrate_sessions};
use db::Db;
use domain::{AuditActor, Email, NewUser, Role};
use tokio::net::TcpListener;

/// Connect dependencies from `config` and serve until shutdown.
pub async fn run(config: Config) -> anyhow::Result<()> {
    let db = Db::connect(&config.database_url, config.db_max_connections)
        .await
        .context("connecting to the database")?;

    db.migrate().await.context("running database migrations")?;

    migrate_sessions(&db)
        .await
        .context("creating the session store")?;

    let search = match (&config.meili_url, &config.meili_master_key) {
        (Some(url), Some(key)) => {
            let client = search::SearchClient::connect(url, key, &config.meili_index)
                .context("connecting to Meilisearch")?;

            client
                .ensure_index()
                .await
                .context("ensuring the search index exists")?;

            tracing::info!(index = %config.meili_index, "search indexing enabled");

            Some(client)
        }
        _ => {
            tracing::warn!(
                "MEILI_URL/MEILI_MASTER_KEY not set — search indexing disabled (reindex_all remains the rebuild path)"
            );

            None
        }
    };

    let state = AppState {
        db,
        app_name: config.app_name,
        cookie_secure: config.cookie_secure,
        search,
        default_language: config.default_language,
        default_timezone: config.default_timezone,
    };

    let listener = TcpListener::bind(&config.bind_addr)
        .await
        .with_context(|| format!("binding to {}", config.bind_addr))?;

    tracing::info!(addr = %config.bind_addr, "server listening");

    serve(listener, state).await
}

/// Resolves when the process receives SIGINT (Ctrl-C) or SIGTERM, so the server can
/// drain in-flight requests before exiting.
async fn shutdown_signal() {
    let ctrl_c = async {
        tokio::signal::ctrl_c()
            .await
            .expect("install Ctrl-C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            .expect("install SIGTERM handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    tracing::info!("shutdown signal received; draining");
}

/// Serve the API on an already-bound listener (used by `run` and tests).
pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()> {
    let app = build_app(state);

    #[cfg(feature = "embed-web")]
    let app = app.merge(web_assets::routes());

    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .context("running the HTTP server")?;

    Ok(())
}

#[cfg(feature = "embed-web")]
pub mod test_support {
    /// The SPA-asset router, for tests.
    pub fn web_router() -> axum::Router {
        super::web_assets::routes()
    }
}

/// One-shot: apply migrations (idempotent), then seed the baseline Spectrum cataloguing
/// vocabularies + field definitions. Safe to re-run (the seed is idempotent).
pub async fn seed(database_url: &str) -> anyhow::Result<()> {
    // CLI one-shot: a tiny pool is plenty.
    let db = Db::connect(database_url, 2)
        .await
        .context("connecting to the database")?;

    // Apply migrations first so `server seed` works on a fresh DB without first
    // starting the server. Migrations are idempotent.
    db.migrate().await.context("running database migrations")?;

    let mut tx = db.pool().begin().await?;

    db::seed::seed_spectrum_cataloguing(&mut tx)
        .await
        .context("seeding Spectrum cataloguing baseline")?;

    tx.commit().await?;

    println!("seeded Spectrum cataloguing baseline (idempotent)");

    Ok(())
}

/// Create a user from the CLI (admin bootstrap). Opens its own connection (CLI
/// one-shot); reads the password from the `BOOTSTRAP_PASSWORD` env var if set,
/// otherwise prompts (hidden input). The plaintext is not zeroized, but it is
/// confined to the scope below and dropped before any network I/O.
pub async fn create_user(database_url: &str, email: &str, role: Role) -> anyhow::Result<()> {
    let email = Email::parse(email).map_err(|err| anyhow::anyhow!("{err}"))?;

    // Read, validate, and hash the password in a scope so the plaintext `String` is
    // dropped before we open a connection / run any awaits.
    let password_hash = {
        let password = match std::env::var("BOOTSTRAP_PASSWORD") {
            Ok(p) => p,
            Err(_) => rpassword::prompt_password("Password: ").context("reading password")?,
        };
        anyhow::ensure!(
            password.chars().count() >= 8,
            "password must be at least 8 characters"
        );
        auth::hash_password(&password).map_err(|err| anyhow::anyhow!("hashing password: {err}"))?
    };

    // CLI one-shot: a tiny pool is plenty.
    let db = Db::connect(database_url, 2)
        .await
        .context("connecting to the database")?;

    let mut tx = db.pool().begin().await?;

    let id = db::users::create_user(
        &mut tx,
        AuditActor::System,
        &NewUser {
            email,
            password_hash,
            role,
        },
    )
    .await
    .context("creating the user (is the email already taken?)")?;

    tx.commit().await?;

    println!("created user {id} ({role:?})");

    Ok(())
}
