// 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>

//! Export page: render a loaded log to SARIF / Markdown / ODT / PDF with
//! checksum sidecars. The document is re-validated against its schema before
//! anything is written.

use std::fmt::Write as _;

use http::{Response, StatusCode};

use crate::app_state::AppState;
use crate::export::{self, ExportFormat};
use crate::html::{self, alert, escape, Nav};
use crate::router::{html_response, Body};
use crate::sidecar::SidecarHashes;

use super::{all_values, first, query_pairs};

/// The selectable sidecar checksum tokens and labels.
const SIDECARS: [(&str, &str); 5] = [
    ("sha-256", "SHA-256"),
    ("sha-512", "SHA-512"),
    ("sha3-512", "SHA3-512"),
    ("blake3-512", "BLAKE3-512"),
    ("shake256-512", "SHAKE256-512"),
];

/// A Bootstrap checkbox.
fn checkbox(name: &str, value: &str, label: &str, checked: bool) -> String {
    let attr = if checked { " checked" } else { "" };
    let id = format!("{name}-{value}");
    format!(
        "<div class=\"form-check form-check-inline\">\
         <input class=\"form-check-input\" type=\"checkbox\" name=\"{name}\" value=\"{value}\" id=\"{id}\"{attr}>\
         <label class=\"form-check-label\" for=\"{id}\">{}</label></div>",
        escape(label)
    )
}

/// Render the chooser when no (valid) log is selected.
fn chooser(state: &AppState) -> String {
    let logs = state.store().list();
    if logs.is_empty() {
        return "<h1 class=\"h3 mb-4\">Export</h1>\
            <div class=\"card shadow-sm\"><div class=\"card-body\">\
            <p class=\"text-body-secondary mb-0\">No logs loaded. <a href=\"/import\">Import one</a> first.</p>\
            </div></div>"
            .to_owned();
    }
    let mut items = String::new();
    for log in &logs {
        let _ = write!(
            items,
            "<li class=\"list-group-item d-flex justify-content-between align-items-center\">\
             <a href=\"/export?log={id}\" class=\"text-decoration-none\">{src}</a>\
             <span class=\"badge rounded-pill text-bg-secondary\">{results} results</span></li>",
            id = escape(&log.id),
            src = escape(&log.source),
            results = log.result_count(),
        );
    }
    format!(
        "<h1 class=\"h3 mb-4\">Export</h1>\
         <div class=\"card shadow-sm\"><div class=\"card-body\">\
         <p>Choose a log to export:</p><ul class=\"list-group list-group-flush\">{items}</ul></div></div>"
    )
}

/// The export form for a specific log.
fn export_form(state: &AppState, log_id: &str, source: &str) -> String {
    let format_boxes: String = ExportFormat::ALL
        .iter()
        .map(|f| {
            let checked = matches!(
                f,
                ExportFormat::Sarif | ExportFormat::Markdown | ExportFormat::Pdf
            );
            checkbox("formats", f.token(), f.label(), checked)
        })
        .collect();

    let sidecar_boxes: String = SIDECARS
        .iter()
        .map(|(token, label)| checkbox("sidecars", token, label, true))
        .collect();

    format!(
        "<h1 class=\"h3 mb-4\">Export — {src}</h1>\
         <div class=\"card shadow-sm\"><div class=\"card-body\">\
         <form method=\"get\" action=\"/export/run\">\
         <input type=\"hidden\" name=\"log\" value=\"{id}\">\
         <h2 class=\"h6 text-uppercase text-body-secondary\">Formats</h2><div class=\"mb-3\">{format_boxes}</div>\
         <h2 class=\"h6 text-uppercase text-body-secondary\">Checksum sidecars</h2><div class=\"mb-3\">{sidecar_boxes}</div>\
         <div class=\"form-check mb-3\">\
         <input class=\"form-check-input\" type=\"checkbox\" name=\"overwrite\" value=\"on\" id=\"overwrite\">\
         <label class=\"form-check-label\" for=\"overwrite\">Overwrite existing files</label></div>\
         <p class=\"text-body-secondary small\">Files are written to <span class=\"mono\">{dir}</span>. \
         The document is re-validated against its SARIF schema before export; an invalid document is refused.</p>\
         <div class=\"d-flex gap-2\"><button class=\"btn btn-primary\" type=\"submit\">\
         <i class=\"bi bi-box-arrow-up me-1\"></i>Export</button>\
         <a class=\"btn btn-outline-secondary\" href=\"/logs/{id}\">Cancel</a></div>\
         </form></div></div>",
        src = escape(source),
        id = escape(log_id),
        dir = escape(state.export_dir()),
    )
}

/// `GET /export?log=…` — the chooser, or the form for the selected log.
pub fn page(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let pairs = query_pairs(&parts.uri);
    let body = match first(&pairs, "log").and_then(|id| state.store().get(id)) {
        Some(log) => export_form(&state, &log.id, &log.source),
        None => chooser(&state),
    };
    html_response(StatusCode::OK, html::page("Export", Nav::Export, &body))
}

/// Render an error alert above the form.
fn error_page(state: &AppState, log_id: &str, source: &str, message: &str) -> Response<Body> {
    let body = format!("{}{}", alert("danger", &escape(message)), export_form(state, log_id, source));
    html_response(StatusCode::OK, html::page("Export", Nav::Export, &body))
}

/// `GET /export/run?log=…&formats=…&sidecars=…&overwrite=…` — do the export.
pub fn run(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let pairs = query_pairs(&parts.uri);
    let Some(log) = first(&pairs, "log").and_then(|id| state.store().get(id)) else {
        let body = format!("{}{}", alert("danger", "Unknown or missing log."), chooser(&state));
        return html_response(StatusCode::OK, html::page("Export", Nav::Export, &body));
    };

    let formats: Vec<ExportFormat> = all_values(&pairs, "formats")
        .iter()
        .filter_map(|t| ExportFormat::from_token(t))
        .collect();
    if formats.is_empty() {
        return error_page(&state, &log.id, &log.source, "Select at least one format.");
    }
    let sidecars = SidecarHashes::from_tokens(all_values(&pairs, "sidecars").into_iter());
    let overwrite = first(&pairs, "overwrite").is_some_and(|v| v == "on" || v == "true");

    let artifacts = match export::build_artifacts(&log, &formats) {
        Ok(artifacts) => artifacts,
        Err(error) => return error_page(&state, &log.id, &log.source, &error.to_string()),
    };
    let dir = match export::open_export_dir(state.export_dir()) {
        Ok(dir) => dir,
        Err(error) => {
            return error_page(
                &state,
                &log.id,
                &log.source,
                &format!("cannot open export directory {}: {error}", state.export_dir()),
            )
        }
    };
    let written = match export::write_artifacts(&dir, &artifacts, &sidecars, overwrite) {
        Ok(written) => written,
        Err(error) => return error_page(&state, &log.id, &log.source, &error.to_string()),
    };

    let mut rows = String::new();
    for item in &written {
        let sidecars = if item.sidecars.is_empty() {
            "<span class=\"text-body-secondary\">—</span>".to_owned()
        } else {
            format!("<span class=\"mono small\">{}</span>", escape(&item.sidecars.join(", ")))
        };
        let _ = write!(
            rows,
            "<tr><td><span class=\"mono\">{}</span></td><td>{} bytes</td><td>{sidecars}</td></tr>",
            escape(&item.file),
            item.bytes,
        );
    }

    let body = format!(
        "<h1 class=\"h3 mb-4\">Export complete</h1>{banner}\
         <div class=\"card shadow-sm mb-4\"><div class=\"card-body\"><div class=\"table-responsive\">\
         <table class=\"table table-hover align-middle\"><thead><tr><th>File</th><th>Size</th><th>Sidecars</th></tr></thead>\
         <tbody>{rows}</tbody></table></div></div></div>\
         <div class=\"d-flex gap-2\">\
         <a class=\"btn btn-outline-primary\" href=\"/export?log={id}\"><i class=\"bi bi-arrow-repeat me-1\"></i>Export again</a>\
         <a class=\"btn btn-outline-secondary\" href=\"/\"><i class=\"bi bi-arrow-left me-1\"></i>Dashboard</a></div>",
        banner = alert(
            "success",
            &format!(
                "Wrote <strong>{n}</strong> file(s) to <span class=\"mono\">{dir}</span> (after schema re-validation).",
                n = written.len(),
                dir = escape(state.export_dir()),
            )
        ),
        id = escape(&log.id),
    );
    html_response(StatusCode::OK, html::page("Export complete", Nav::Export, &body))
}
