//! Shared app state + router assembly.

use crate::auth::ReadToken;
use crate::fleet_index::FleetIndex;
use crate::persist::HighWater;
use axum::body::Body;
use axum::extract::State;
use axum::http::{header, Request, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum::Json;
use axum::{routing::get, routing::post, Router};
use serde_json::json;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

pub struct AppState {
    pub events_out_dir: PathBuf,
    pub policy_bundle_path: PathBuf,
    /// Path to the signed pack-set bundle. `None` ⇒ `GET /v1/rule-packs` 404s.
    pub rule_packs_bundle_path: Option<std::path::PathBuf>,
    /// Directory of signed agent release artifacts. `None` ⇒ `GET /v1/artifacts*`
    /// 404s (feature off). #182
    pub artifacts_dir: Option<std::path::PathBuf>,
    pub high_water_path: PathBuf,
    /// `None` ⇒ every authenticated host is accepted. Wrapped in a `RwLock` so
    /// the enroll handler can insert a newly enrolled host at runtime (#184 fix F)
    /// without a server restart. The events hot path takes a cheap read-lock.
    pub allowlist: parking_lot::RwLock<Option<HashSet<String>>>,
    /// host_id → highest persisted sequence. Guards the JSONL append + dedup.
    pub high_water: Mutex<HighWater>,
    /// Phase 3b.4 — in-memory per-host summary index. Updated synchronously
    /// inside the POST /v1/events handler after a successful persist.
    pub fleet_index: FleetIndex,
    /// Phase 3b.4 — bearer token for the read API. `None` ⇒ read endpoints 404.
    pub read_token: ReadToken,
    /// Loaded + verified at boot. Read-only thereafter. Free if none configured.
    pub license_state: sigil_core::license::status::LicenseState,
    /// Rolling window (days) for active-host counting.
    pub active_window_days: u32,
    /// Audit signing key (auto-generated). `None` ⇒ audit signing disabled.
    pub audit_key: Option<crate::audit_key::AuditKey>,
    /// Latest signed audit-chain head, updated by the audit task; read by /v1/meta.
    pub audit_head: Mutex<Option<sigil_core::audit::AuditHead>>,
    /// Path to the optional `hosts.json` allowlist file (for atomic add on
    /// enroll). `None` ⇒ no on-disk allowlist (and enrollment is disabled). #184
    pub allowlist_path: Option<PathBuf>,
    /// #184 — enrollment state (intermediate CA + tokens + mint mutex). `None` ⇒
    /// every `/v1/enroll*` route 404s (feature off).
    pub enroll: Option<crate::enroll::EnrollState>,
    /// #194.2 — when true, `POST /v1/events` requires the TLS client cert to
    /// match the envelope host_id (CN == host_id or host_id ∈ SAN DNS). Boot
    /// validation guarantees mTLS is on whenever this is set.
    pub events_require_cert_host_match: bool,
}

pub type SharedState = Arc<AppState>;

/// Health-check route path. Referenced by both the route registration and the
/// `boot_gate` middleware (which always lets it through), so it lives in one
/// place to keep the two in sync under rename.
pub const HEALTHZ_PATH: &str = "/v1/healthz";

pub fn build_router(state: SharedState) -> Router {
    use crate::auth::require_bearer;
    use axum::middleware::from_fn_with_state;

    let token = state.read_token.clone();
    Router::new()
        .route("/v1/events", post(crate::events_route::post_events))
        .route("/v1/policy", get(crate::policy_route::get_policy))
        .route(
            "/v1/rule-packs",
            get(crate::rule_packs_route::get_rule_packs),
        )
        .route(HEALTHZ_PATH, get(crate::routes::healthz::get_healthz))
        .route(
            "/v1/meta",
            get(crate::routes::meta::get_meta)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/policy/meta",
            get(crate::routes::policy_meta::get_policy_meta)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/fleet/hosts",
            get(crate::routes::fleet_hosts::get_fleet_hosts)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/fleet/hosts/:host_id",
            get(crate::routes::fleet_hosts::get_fleet_host_by_id)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/fleet/risk",
            get(crate::routes::fleet_risk::get_fleet_risk)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/fleet/compliance",
            get(crate::routes::fleet_compliance::get_fleet_compliance)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/events",
            get(crate::routes::events::get_events)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/events/:event_id",
            get(crate::routes::events::get_event_by_id)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        // #182 — read-only signed-artifact serving for air-gapped/PMS fleet
        // install. Gated by the read-API bearer token, same as the read routes.
        .route(
            "/v1/artifacts",
            get(crate::routes::artifacts::get_artifacts_index)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        .route(
            "/v1/artifacts/:filename",
            get(crate::routes::artifacts::get_artifact_by_name)
                .route_layer(from_fn_with_state(token.clone(), require_bearer)),
        )
        // #184 — B-mint enrollment. NO read-bearer layer: the enroll token in
        // the body is the credential; mTLS still gates at the TLS layer. Feature
        // off (enroll == None) ⇒ handler returns 404.
        .route("/v1/enroll", post(crate::routes::enroll::post_enroll))
        .with_state(state)
}

/// Boot-gate middleware. While the in-memory fleet index is still being rebuilt
/// (`boot_complete == false`), every route except `/v1/healthz` returns
/// 503 + `Retry-After: 5` so agent senders and consoles back off and retry (#19).
pub async fn boot_gate(
    State(boot_complete): State<Arc<AtomicBool>>,
    req: Request<Body>,
    next: Next,
) -> Response {
    if req.uri().path() == HEALTHZ_PATH || boot_complete.load(Ordering::Relaxed) {
        return next.run(req).await;
    }
    (
        StatusCode::SERVICE_UNAVAILABLE,
        [(header::RETRY_AFTER, "5")],
        Json(json!({"error": {"code": "rebuilding", "message": "fleet index rebuilding; retry shortly"}})),
    )
        .into_response()
}
