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

//! Dashboard, log detail and per-log validate / delete / download handlers.

use std::fmt::Write as _;
use std::sync::Arc;

use http::{header, HeaderValue, Response, StatusCode};

use crate::app_state::AppState;
use crate::html::{self, alert, escape, level_badge, level_count_badge, Nav};
use crate::model::LevelCounts;
use crate::router::{html_response, json_response, path_param, Body};
use crate::store::LoadedLog;
use crate::validation::{self, ValidationReport};

/// Truncate `text` to `max` characters, appending `…` when cut.
fn truncate(text: &str, max: usize) -> String {
    if text.chars().count() <= max {
        return text.to_owned();
    }
    let mut out: String = text.chars().take(max).collect();
    out.push('…');
    out
}

/// The four severity counts as inline badges.
fn counts_badges(counts: LevelCounts) -> String {
    format!(
        "{} {} {} {}",
        level_count_badge("error", counts.error),
        level_count_badge("warning", counts.warning),
        level_count_badge("note", counts.note),
        level_count_badge("none", counts.none),
    )
}

/// One dashboard statistic card.
fn stat_card(value: usize, label: &str, icon: &str, accent: &str) -> String {
    format!(
        "<div class=\"col-6 col-md-3\"><div class=\"card shadow-sm h-100\"><div class=\"card-body text-center\">\
         <i class=\"bi {icon} fs-3 {accent}\"></i>\
         <div class=\"stat-value\">{value}</div><div class=\"stat-label\">{label}</div>\
         </div></div></div>"
    )
}

/// Render the dashboard, optionally with a banner.
fn render_dashboard(state: &AppState, banner: Option<(bool, String)>) -> Response<Body> {
    let logs = state.store().list();
    let mut total_results = 0usize;
    let mut totals = LevelCounts::default();
    for log in &logs {
        total_results = total_results.saturating_add(log.result_count());
        totals.error = totals.error.saturating_add(log.counts.error);
        totals.warning = totals.warning.saturating_add(log.counts.warning);
        totals.note = totals.note.saturating_add(log.counts.note);
        totals.none = totals.none.saturating_add(log.counts.none);
    }

    let banner_html = banner.map_or_else(String::new, |(ok, msg)| {
        alert(if ok { "success" } else { "danger" }, &escape(&msg))
    });

    let stats = format!(
        "<div class=\"row g-3 mb-4\">{}{}{}{}</div>",
        stat_card(logs.len(), "Logs", "bi-files", "text-primary"),
        stat_card(total_results, "Results", "bi-list-check", "text-info"),
        stat_card(totals.error, "Errors", "bi-exclamation-octagon", "text-danger"),
        stat_card(totals.warning, "Warnings", "bi-exclamation-triangle", "text-warning"),
    );

    let import_form = "<div class=\"card shadow-sm mb-4\"><div class=\"card-body\">\
         <h2 class=\"h5 card-title\"><i class=\"bi bi-box-arrow-in-down me-2\"></i>Import a SARIF file or folder</h2>\
         <form class=\"row g-2\" method=\"get\" action=\"/import/run\">\
         <div class=\"col\"><input type=\"text\" name=\"path\" class=\"form-control\" \
         placeholder=\"/absolute/path/to/report.sarif or a directory\" required></div>\
         <div class=\"col-auto\"><button class=\"btn btn-primary\" type=\"submit\">Import &amp; validate</button></div>\
         </form>\
         <p class=\"text-body-secondary small mt-2 mb-0\">Every imported document is schema-validated before it is loaded.</p>\
         </div></div>";

    let mut rows = String::new();
    if logs.is_empty() {
        rows.push_str(
            "<tr><td colspan=\"6\" class=\"text-body-secondary text-center py-4\">\
             No logs loaded. Import one above.</td></tr>",
        );
    } else {
        for log in &logs {
            let id = escape(&log.id);
            let _ = write!(
                rows,
                "<tr><td><a href=\"/logs/{id}\" class=\"fw-semibold text-decoration-none\">{src}</a>\
                 <div class=\"text-body-secondary small\">{title}</div></td>\
                 <td><span class=\"badge rounded-pill text-bg-light border\">{ver}</span></td>\
                 <td>{runs}</td><td>{results}</td><td>{counts}</td>\
                 <td class=\"text-end\"><div class=\"btn-group btn-group-sm\" role=\"group\">\
                 <a class=\"btn btn-outline-secondary\" href=\"/logs/{id}/validate\" title=\"Validate\"><i class=\"bi bi-check2-circle\"></i></a>\
                 <a class=\"btn btn-outline-secondary\" href=\"/export?log={id}\" title=\"Export\"><i class=\"bi bi-box-arrow-up\"></i></a>\
                 <a class=\"btn btn-outline-secondary\" href=\"/logs/{id}/download\" title=\"Download SARIF\"><i class=\"bi bi-download\"></i></a>\
                 <a class=\"btn btn-outline-danger\" href=\"/logs/{id}/delete\" title=\"Remove\"><i class=\"bi bi-trash\"></i></a>\
                 </div></td></tr>",
                src = escape(&log.source),
                title = escape(&log.title()),
                ver = escape(log.version.label()),
                runs = log.runs.len(),
                results = log.result_count(),
                counts = counts_badges(log.counts),
            );
        }
    }

    let table = format!(
        "<div class=\"card shadow-sm\"><div class=\"card-body\">\
         <h2 class=\"h5 card-title\"><i class=\"bi bi-collection me-2\"></i>Loaded logs</h2>\
         <div class=\"table-responsive\"><table class=\"table table-hover align-middle\">\
         <thead><tr><th>Source</th><th>Version</th><th>Runs</th><th>Results</th>\
         <th>Severity</th><th class=\"text-end\">Actions</th></tr></thead><tbody>{rows}</tbody></table></div>\
         </div></div>"
    );

    let content = format!("<h1 class=\"h3 mb-4\">Dashboard</h1>{banner_html}{stats}{import_form}{table}");
    html_response(StatusCode::OK, html::page("Dashboard", Nav::Home, &content))
}

/// `GET /` — the dashboard.
pub fn dashboard(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    render_dashboard(&state, None)
}

/// Look up a log id from the path, or render a 404 dashboard banner.
fn require_log(
    state: &AppState,
    params: &[(String, String)],
) -> Result<Arc<LoadedLog>, Response<Body>> {
    let id = path_param(params, "id").unwrap_or_default();
    state
        .store()
        .get(&id)
        .ok_or_else(|| render_dashboard(state, Some((false, format!("No log with id '{id}'.")))))
}

/// `GET /logs/{id}` — the log detail view.
pub fn log_view(
    state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let log = match require_log(&state, &params) {
        Ok(log) => log,
        Err(response) => return response,
    };
    let id = escape(&log.id);

    let mut runs_html = String::new();
    for (index, run) in log.runs.iter().enumerate() {
        let version = run
            .tool_version
            .as_deref()
            .map_or_else(String::new, |v| format!(" <span class=\"badge text-bg-light border\">{}</span>", escape(v)));
        let org = run
            .organization
            .as_deref()
            .map_or_else(String::new, |o| format!(" · {}", escape(o)));
        let counts = crate::model::count_levels(std::slice::from_ref(run));

        let mut result_rows = String::new();
        if run.results.is_empty() {
            result_rows.push_str(
                "<tr><td colspan=\"4\" class=\"text-body-secondary\">No results in this run.</td></tr>",
            );
        } else {
            for result in &run.results {
                let name = result.rule_name.as_deref().map_or_else(String::new, |n| {
                    format!("<div class=\"text-body-secondary small\">{}</div>", escape(n))
                });
                let locs = if result.locations.is_empty() {
                    "<span class=\"text-body-secondary\">—</span>".to_owned()
                } else {
                    format!("<span class=\"mono\">{}</span>", escape(&result.locations.join(", ")))
                };
                let _ = write!(
                    result_rows,
                    "<tr><td>{}</td><td><span class=\"mono fw-semibold\">{}</span>{name}</td>\
                     <td>{}</td><td class=\"small\">{locs}</td></tr>",
                    level_badge(&result.level),
                    escape(&result.rule_id),
                    escape(&truncate(&result.message, 300)),
                );
            }
        }

        let _ = write!(
            runs_html,
            "<div class=\"card shadow-sm mb-4\"><div class=\"card-body\">\
             <h2 class=\"h5 card-title\"><i class=\"bi bi-tools me-2\"></i>Run {n}: {tool}{version}</h2>\
             <p class=\"text-body-secondary small\">{total} results — {e} error, {w} warning, {nt} note, {nn} none{org} · {rules} rule(s) defined</p>\
             <div class=\"table-responsive\"><table class=\"table table-sm table-hover align-middle\">\
             <thead><tr><th>Level</th><th>Rule</th><th>Message</th><th>Location</th></tr></thead>\
             <tbody>{result_rows}</tbody></table></div></div></div>",
            n = index.saturating_add(1),
            tool = escape(&run.tool_name),
            total = counts.total(),
            e = counts.error,
            w = counts.warning,
            nt = counts.note,
            nn = counts.none,
            rules = run.rules_count,
        );
    }

    let schema_line = log.schema_uri.as_deref().map_or_else(
        || "<span class=\"text-body-secondary\">no $schema declared</span>".to_owned(),
        |u| format!("<span class=\"mono\">{}</span>", escape(u)),
    );

    let header = format!(
        "<div class=\"card shadow-sm mb-4\"><div class=\"card-body\">\
         <h1 class=\"h3\">{title}</h1>\
         <p class=\"text-body-secondary mb-1\">Source <span class=\"mono\">{src}</span> · {ver} · declared <span class=\"mono\">{vraw}</span></p>\
         <p class=\"small mb-3\">$schema: {schema_line}</p>\
         <div class=\"d-flex flex-wrap gap-2\">\
         <a class=\"btn btn-primary\" href=\"/logs/{id}/validate\"><i class=\"bi bi-check2-circle me-1\"></i>Validate</a>\
         <a class=\"btn btn-outline-primary\" href=\"/export?log={id}\"><i class=\"bi bi-box-arrow-up me-1\"></i>Export</a>\
         <a class=\"btn btn-outline-secondary\" href=\"/logs/{id}/download\"><i class=\"bi bi-download me-1\"></i>Download SARIF</a>\
         <a class=\"btn btn-outline-secondary ms-auto\" href=\"/\"><i class=\"bi bi-arrow-left me-1\"></i>Dashboard</a>\
         </div></div></div>",
        title = escape(&log.title()),
        src = escape(&log.source),
        ver = escape(log.version.label()),
        vraw = escape(&log.version_raw),
    );

    let content = format!("{header}{runs_html}");
    html_response(StatusCode::OK, html::page(&log.title(), Nav::None, &content))
}

/// Render a validation report as an HTML card (shared with the validate page).
#[must_use]
pub fn report_html(title: &str, report: &ValidationReport) -> String {
    let (kind, verdict) = if report.is_valid() {
        ("success", "VALID")
    } else {
        ("danger", "INVALID")
    };
    let banner = alert(
        kind,
        &format!(
            "<strong>{verdict}</strong> — checked against {schema} (declared version <span class=\"mono\">{vraw}</span>)",
            schema = escape(report.schema_label),
            vraw = escape(&report.version_raw),
        ),
    );

    let mut sections = String::new();
    sections.push_str(&error_list("Structural checks", &report.structural));
    sections.push_str(&error_list("Schema validation", &report.schema_errors));

    if let (Some(label), Some(errors)) = (report.cross_label, &report.cross_errors) {
        let heading = format!("Cross-check against {label} (advisory)");
        if errors.is_empty() {
            let _ = write!(
                sections,
                "<h3 class=\"h6 mt-3\">{}</h3><p class=\"text-success mb-3\"><i class=\"bi bi-check-circle me-1\"></i>Also clean against {}.</p>",
                escape(&heading),
                escape(label)
            );
        } else {
            sections.push_str(&error_list(&heading, errors));
        }
    }

    format!(
        "<div class=\"card shadow-sm mb-4\"><div class=\"card-body\">\
         <h2 class=\"h5 card-title\">{title}</h2>{banner}{sections}</div></div>",
        title = escape(title),
    )
}

/// Render a labelled error list (or an "OK" line when empty).
fn error_list(heading: &str, errors: &[String]) -> String {
    if errors.is_empty() {
        return format!(
            "<h3 class=\"h6 mt-3\">{}</h3><p class=\"text-success mb-3\"><i class=\"bi bi-check-circle me-1\"></i>No issues.</p>",
            escape(heading)
        );
    }
    let items: String = errors
        .iter()
        .map(|e| format!("<li class=\"list-group-item\"><span class=\"mono\">{}</span></li>", escape(e)))
        .collect();
    format!(
        "<h3 class=\"h6 mt-3\">{} <span class=\"badge text-bg-danger\">{} issue(s)</span></h3>\
         <ul class=\"list-group list-group-flush mb-3\">{items}</ul>",
        escape(heading),
        errors.len()
    )
}

/// `GET /logs/{id}/validate` — validate a loaded log and show the report.
pub fn log_validate(
    state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let log = match require_log(&state, &params) {
        Ok(log) => log,
        Err(response) => return response,
    };
    let report = match validation::validate(&log.raw, true) {
        Ok(report) => report,
        Err(error) => {
            return html_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                html::page(
                    "Validation error",
                    Nav::None,
                    &alert("danger", &escape(&error)),
                ),
            )
        }
    };
    let back = format!(
        "<a class=\"btn btn-outline-secondary\" href=\"/logs/{}\"><i class=\"bi bi-arrow-left me-1\"></i>Back to the log</a>",
        escape(&log.id)
    );
    let content = format!(
        "<h1 class=\"h3 mb-4\">Validation — {}</h1>{}{}",
        escape(&log.source),
        report_html("Schema validation report", &report),
        back
    );
    html_response(StatusCode::OK, html::page("Validation", Nav::None, &content))
}

/// `GET /logs/{id}/delete` — remove a log, returning to the dashboard.
pub fn log_delete(
    state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let id = path_param(&params, "id").unwrap_or_default();
    let banner = if state.store().remove(&id) {
        (true, format!("Removed log '{id}'."))
    } else {
        (false, format!("No log with id '{id}'."))
    };
    render_dashboard(&state, Some(banner))
}

/// `GET /logs/{id}/download` — the raw SARIF document as a JSON download.
pub fn log_download(
    state: AppState,
    _parts: http::request::Parts,
    params: Vec<(String, String)>,
) -> Response<Body> {
    let log = match require_log(&state, &params) {
        Ok(log) => log,
        Err(response) => return response,
    };
    let body = serde_json::to_vec_pretty(&log.raw).unwrap_or_default();
    let mut response = json_response(StatusCode::OK, body);
    if let Ok(value) = HeaderValue::from_str(&format!("attachment; filename=\"{}.sarif\"", log.id)) {
        response
            .headers_mut()
            .insert(header::CONTENT_DISPOSITION, value);
    }
    response
}

/// The dashboard with a banner — used by import/export after an action.
#[must_use]
pub fn dashboard_with_banner(state: &AppState, ok: bool, message: String) -> Response<Body> {
    render_dashboard(state, Some((ok, message)))
}
