//! Admin dashboard router. Mounted at `/admin/*` by `api::router` when
//! the operator hasn't disabled it via `--admin-ui-dir=` (empty).
//!
//! Static HTML/CSS lives in `admin_ui/` next to the source tree and is
//! embedded into the binary at build time via `include_str!` — no separate
//! deploy artifact, no ServeDir wildcard route conflicting with the
//! literal /admin/login etc. The ASSETS table at the bottom is the
//! authoritative list of files we ship.
//!
//! Layout served at runtime:
//!   /admin/                  ← index.html (the SPA shell)
//!   /admin/login.html        ← login form
//!   /admin/login             POST handler (form-encoded, sets session cookie)
//!   /admin/logout            POST handler (clears session cookie)
//!   /admin/me                GET fragment (current user, sidebar widget)
//!   /admin/pages/*           GET fragments (one per page)

pub mod auth;
pub mod me;
pub mod oidc_login;
pub mod pages;

use axum::http::{header, HeaderValue, StatusCode};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use axum::Router;
use std::sync::Arc;

/// Files embedded into the binary. Paths are relative to this source file
/// per `include_str!`. Adding a new HTML asset = one new entry here.
const INDEX_HTML: &str = include_str!("../../../admin_ui/index.html");
const LOGIN_HTML: &str = include_str!("../../../admin_ui/login.html");

/// Third-party JS dependencies vendored under `admin_ui/assets/` so the
/// dashboard doesn't fetch from cdn.tailwindcss.com / unpkg.com at runtime.
/// See docs/CONFIGURATION.md "Web client" for the upgrade procedure.
const TAILWIND_JS: &[u8] = include_bytes!("../../../admin_ui/assets/tailwindcss.js");
const HTMX_JS: &[u8] = include_bytes!("../../../admin_ui/assets/htmx.min.js");

pub fn build(state: Arc<crate::api::state::AppState>) -> Option<Router> {
    if state.cfg.admin_ui_dir.is_empty() {
        // Operator opted out by setting the flag to empty.
        return None;
    }
    let r = Router::new()
        // Static HTML pages — explicit routes per file, no wildcard.
        .route("/admin", get(serve_index))
        .route("/admin/", get(serve_index))
        .route("/admin/index.html", get(serve_index))
        .route("/admin/login.html", get(serve_login))
        // Vendored third-party JS — versions pinned in source, so we can
        // cache aggressively (immutable + 1-year max-age).
        .route("/admin/assets/tailwindcss.js", get(serve_tailwind))
        .route("/admin/assets/htmx.min.js", get(serve_htmx))
        // Dynamic dashboard endpoints.
        .route("/admin/login", post(auth::login))
        .route("/admin/logout", post(auth::logout))
        .route("/admin/me", get(me::me))
        // OIDC entry points consumed by login.html (unauthenticated — they
        // *initiate* a sign-in). The matching /oidc/callback is mounted by
        // the public api router and finishes both desktop and admin flows.
        .route("/admin/oidc/providers", get(oidc_login::list_providers))
        .route("/admin/login/oidc/:provider", get(oidc_login::start_login))
        // Page fragments — one per sidebar entry.
        .route("/admin/pages/users", get(pages::users::index))
        .route("/admin/pages/users/create", post(pages::users::create))
        .route(
            "/admin/pages/users/:id/update-info",
            post(pages::users::update_info),
        )
        .route(
            "/admin/pages/users/:id/password-reset",
            post(pages::users::reset_password),
        )
        .route(
            "/admin/pages/users/:id/toggle-admin",
            post(pages::users::toggle_admin),
        )
        .route(
            "/admin/pages/users/:id/toggle-status",
            post(pages::users::toggle_status),
        )
        .route(
            "/admin/pages/users/:id/totp-enroll",
            post(pages::users::totp_enroll),
        )
        .route(
            "/admin/pages/users/:id/totp-unenroll",
            post(pages::users::totp_unenroll),
        )
        .route("/admin/pages/users/:id/delete", post(pages::users::delete))
        // Devices
        .route(
            "/admin/pages/devices/:peer_id/disconnect",
            post(pages::devices::force_disconnect),
        )
        .route(
            "/admin/pages/devices/:peer_id/sysinfo-refresh",
            post(pages::devices::force_sysinfo),
        )
        .route(
            "/admin/pages/devices/:peer_id/delete",
            post(pages::devices::delete),
        )
        // Groups
        .route("/admin/pages/groups/create", post(pages::groups::create))
        .route("/admin/pages/groups/:id/delete", post(pages::groups::delete))
        .route(
            "/admin/pages/groups/:id/members/add",
            post(pages::groups::add_member),
        )
        .route(
            "/admin/pages/groups/:id/members/:user_id/remove",
            post(pages::groups::remove_member),
        )
        // Strategies
        .route(
            "/admin/pages/strategies/create",
            post(pages::strategies::create),
        )
        .route(
            "/admin/pages/strategies/:id/update",
            post(pages::strategies::update),
        )
        .route(
            "/admin/pages/strategies/:id/delete",
            post(pages::strategies::delete),
        )
        .route("/admin/pages/deploy", get(pages::deploy::index))
        .route(
            "/admin/pages/deploy/generate",
            post(pages::deploy::generate),
        )
        .route(
            "/admin/pages/customization",
            get(pages::customization::index),
        )
        .route(
            "/admin/pages/customization/generate",
            post(pages::customization::generate),
        )
        .route(
            "/admin/pages/customization/download",
            post(pages::customization::download),
        )
        // Web client (M6) — full-page SPA, NOT an HTMX fragment. Mounted
        // outside /admin/pages/ because it's a standalone document the
        // operator opens in a new tab from the Devices action menu.
        .route(
            "/admin/connect/:peer_id",
            get(pages::connect::index),
        )
        .route(
            "/admin/connect/assets/bundle.js",
            get(pages::connect::bundle_js),
        )
        .route(
            "/admin/connect/assets/bundle.css",
            get(pages::connect::bundle_css),
        )
        .route("/admin/pages/devices", get(pages::devices::index))
        .route("/admin/pages/groups", get(pages::groups::index))
        .route("/admin/pages/strategies", get(pages::strategies::index))
        .route(
            "/admin/pages/address-books",
            get(pages::address_books::index),
        )
        .route(
            "/admin/pages/address-books/create",
            post(pages::address_books::create),
        )
        .route(
            "/admin/pages/address-books/:guid/delete",
            post(pages::address_books::delete),
        )
        .route(
            "/admin/pages/address-books/:guid/manage",
            get(pages::address_books::manage),
        )
        .route(
            "/admin/pages/address-books/:guid/shares/add",
            post(pages::address_books::share_add),
        )
        .route(
            "/admin/pages/address-books/:guid/shares/:user_id/remove",
            post(pages::address_books::share_remove),
        )
        .route("/admin/pages/oidc", get(pages::oidc::index))
        // Self-service profile — cookie-only, no admin gate.
        .route("/admin/pages/profile", get(pages::profile::index))
        .route(
            "/admin/pages/profile/update-info",
            post(pages::profile::update_info),
        )
        .route(
            "/admin/pages/profile/change-password",
            post(pages::profile::change_password),
        )
        .route(
            "/admin/pages/profile/totp/start",
            post(pages::profile::totp_start),
        )
        .route(
            "/admin/pages/profile/totp/confirm",
            post(pages::profile::totp_confirm),
        )
        .route(
            "/admin/pages/profile/totp/remove",
            post(pages::profile::totp_remove),
        )
        .route("/admin/pages/audit", get(pages::audit::index))
        .route("/admin/pages/recordings", get(pages::recordings::index));
    hbb_common::log::info!(
        "admin dashboard mounted at /admin (HTML embedded; --admin-ui-dir is informational)"
    );
    Some(r)
}

async fn serve_index() -> Response {
    html_response(INDEX_HTML)
}

async fn serve_login() -> Response {
    html_response(LOGIN_HTML)
}

fn html_response(body: &'static str) -> Response {
    // We hand back `Html<&'static str>` so axum sets `text/html` for us.
    // Cache-Control: no-cache so the operator sees fresh HTML after a
    // server upgrade without having to bump asset URLs.
    let mut resp = Html(body).into_response();
    resp.headers_mut().insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("no-cache"),
    );
    resp
}

async fn serve_tailwind() -> Response {
    js_response(TAILWIND_JS)
}

async fn serve_htmx() -> Response {
    js_response(HTMX_JS)
}

fn js_response(body: &'static [u8]) -> Response {
    let mut resp = (StatusCode::OK, body).into_response();
    let h = resp.headers_mut();
    h.insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("application/javascript; charset=utf-8"),
    );
    // Vendored at a pinned version — safe to cache for a year. If we
    // ever bump the version we should also bump the asset path so
    // browsers don't keep stale copies; for now the path-pinned version
    // is implicit in the binary build.
    h.insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("public, max-age=31536000, immutable"),
    );
    resp
}
