// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG, Cologne
// SPDX-FileCopyrightText: Author: Pierre Gronau <Pierre.Gronau@ndaal.eu>

//! Route table and shared request/response helpers for the web viewer.

pub mod export_page;
pub mod home;
pub mod import_page;
pub mod info;
pub mod validate_page;

use http::StatusCode;

use crate::app_state::AppState;
use crate::html::{self, Nav};
use crate::router::{
    decode_query_value, html_response, path_param, sync_handler_fn, Body, RouterBuildError,
    RouterBuilder, SharedRouter,
};

/// Build the viewer's router.
///
/// # Errors
///
/// Returns [`RouterBuildError`] if two route patterns conflict (a programming
/// error caught at start-up).
pub fn build_viewer_router(state: AppState) -> Result<SharedRouter, RouterBuildError> {
    RouterBuilder::new()
        .get("/", sync_handler_fn(home::dashboard))
        .get("/logs/{id}", sync_handler_fn(home::log_view))
        .get("/logs/{id}/validate", sync_handler_fn(home::log_validate))
        .get("/logs/{id}/delete", sync_handler_fn(home::log_delete))
        .get("/logs/{id}/download", sync_handler_fn(home::log_download))
        .get("/import", sync_handler_fn(import_page::page))
        .get("/import/run", sync_handler_fn(import_page::run))
        .get("/validate", sync_handler_fn(validate_page::page))
        .get("/validate/run", sync_handler_fn(validate_page::run))
        .get("/export", sync_handler_fn(export_page::page))
        .get("/export/run", sync_handler_fn(export_page::run))
        .get("/about", sync_handler_fn(info::about))
        .get("/system-info", sync_handler_fn(info::system_info))
        .get("/privacy", sync_handler_fn(info::privacy))
        .get("/security", sync_handler_fn(info::security))
        .get("/license", sync_handler_fn(info::license))
        .get("/imprint", sync_handler_fn(info::imprint))
        .get("/changelog", sync_handler_fn(info::changelog))
        .get("/static/{*path}", sync_handler_fn(serve_static))
        .fallback(sync_handler_fn(not_found))
        .build(state)
}

/// Serve an embedded static asset.
fn serve_static(
    _state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> http::Response<Body> {
    let rel = path_param(&params, "path").unwrap_or_default();
    crate::static_files::serve(&rel)
}

/// The 404 page.
fn not_found(
    _state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> http::Response<Body> {
    let body = html::page(
        "Not found",
        Nav::None,
        "<div class=\"text-center py-5\">\
         <i class=\"bi bi-compass display-1 text-body-secondary\"></i>\
         <h1 class=\"h3 mt-3\">404 — Not found</h1>\
         <p class=\"text-body-secondary\">No page matches this URL.</p>\
         <a class=\"btn btn-primary\" href=\"/\"><i class=\"bi bi-house-door me-1\"></i>Dashboard</a></div>",
    );
    html_response(StatusCode::NOT_FOUND, body)
}

// ---------------------------------------------------------------------------
// Query-string helpers (the router is GET-only; actions take query params)
// ---------------------------------------------------------------------------

/// Parse a URI's query string into decoded `(key, value)` pairs, preserving
/// order and repeats (so multi-valued fields like `formats` work).
#[must_use]
pub fn query_pairs(uri: &http::Uri) -> Vec<(String, String)> {
    uri.query()
        .unwrap_or_default()
        .split('&')
        .filter(|s| !s.is_empty())
        .map(|pair| {
            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
            (decode_query_value(k), decode_query_value(v))
        })
        .collect()
}

/// The first value for `key`, if any.
#[must_use]
pub fn first<'a>(pairs: &'a [(String, String)], key: &str) -> Option<&'a str> {
    pairs
        .iter()
        .find(|(k, _)| k == key)
        .map(|(_, v)| v.as_str())
}

/// Every value for `key`, in order.
#[must_use]
pub fn all_values<'a>(pairs: &'a [(String, String)], key: &str) -> Vec<&'a str> {
    pairs
        .iter()
        .filter(|(k, _)| k == key)
        .map(|(_, v)| v.as_str())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::{all_values, first, query_pairs};

    #[test]
    fn parses_multi_valued_query() {
        let uri: http::Uri = "http://x/y?log=a&formats=md&formats=pdf&q=hello%20world"
            .parse()
            .expect("uri");
        let pairs = query_pairs(&uri);
        assert_eq!(first(&pairs, "log"), Some("a"));
        assert_eq!(all_values(&pairs, "formats"), vec!["md", "pdf"]);
        assert_eq!(first(&pairs, "q"), Some("hello world"));
    }
}
