//! Embedded Solid management UI served by the control-plane listener.
//!
//! The router is empty in normal Debug builds because Vite owns Web assets
//! during development. Release builds embed Cargo `OUT_DIR/web`; requests only
//! resolve exact files, so an unknown URL cannot accidentally receive the Host
//! document as an SPA fallback.

use axum::Router;

#[cfg(wasmeld_embedded_web)]
use axum::{
    body::{Body, Bytes},
    extract::Path,
    http::{StatusCode, header},
    response::{IntoResponse, Response},
    routing::get,
};
#[cfg(wasmeld_embedded_web)]
use include_dir::{Dir, include_dir};

#[cfg(wasmeld_embedded_web)]
static WEB_ASSETS: Dir<'static> = include_dir!("$OUT_DIR/web");

pub(crate) fn router() -> Router {
    #[cfg(wasmeld_embedded_web)]
    {
        Router::new()
            .route("/", get(index))
            .route("/{*path}", get(asset))
    }

    #[cfg(not(wasmeld_embedded_web))]
    {
        Router::new()
    }
}

#[cfg(wasmeld_embedded_web)]
async fn index() -> Response {
    response("index.html")
}

#[cfg(wasmeld_embedded_web)]
async fn asset(Path(path): Path<String>) -> Response {
    response(&path)
}

#[cfg(wasmeld_embedded_web)]
fn response(path: &str) -> Response {
    // The wildcard route is public input. Reject non-canonical paths before
    // looking inside the compile-time directory, even though `include_dir`
    // itself cannot access the host filesystem at runtime.
    if !valid_asset_path(path) {
        return StatusCode::NOT_FOUND.into_response();
    }
    let Some(file) = WEB_ASSETS.get_file(path) else {
        return StatusCode::NOT_FOUND.into_response();
    };
    let content_type = mime_guess::from_path(path)
        .first_or_octet_stream()
        .essence_str()
        .to_owned();
    let cache_control = if path.starts_with("assets/") {
        "public, max-age=31536000, immutable"
    } else {
        "no-cache"
    };
    let mut builder = Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, content_type)
        .header(header::CACHE_CONTROL, cache_control)
        .header("x-content-type-options", "nosniff");
    if path == "sw.js" {
        builder = builder.header("service-worker-allowed", "/");
    }
    builder
        .body(Body::from(Bytes::from_static(file.contents())))
        .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}

#[cfg(wasmeld_embedded_web)]
fn valid_asset_path(path: &str) -> bool {
    !path.is_empty()
        && !path.starts_with('/')
        && !path.contains('\\')
        && path
            .split('/')
            .all(|segment| !matches!(segment, "" | "." | ".."))
}

#[cfg(all(test, wasmeld_embedded_web))]
mod tests {
    use super::*;

    #[test]
    fn embeds_both_document_boundaries_and_pwa_assets() {
        for path in ["index.html", "page.html", "manifest.webmanifest", "sw.js"] {
            assert!(WEB_ASSETS.get_file(path).is_some(), "missing {path}");
        }
    }

    #[test]
    fn rejects_non_canonical_asset_paths() {
        assert!(!valid_asset_path("../index.html"));
        assert!(!valid_asset_path("assets//app.js"));
        assert!(!valid_asset_path(r"assets\app.js"));
    }
}
