// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Shared HTML layout for all server-side rendered pages.
//!
//! Provides a single `wrap_page()` function so that branding, theme handling,
//! and navigation markup live in one place.

/// Active navigation section, used to highlight the current menu entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Nav {
    /// Dashboard / root page.
    Home,
    /// CSAF document CRUD pages.
    Csaf,
    /// Administration → Import page.
    Import,
    /// Administration → Export page.
    Export,
    /// Settings page.
    Settings,
    /// Info (About/License/System/Privacy/Security) pages.
    Info,
}

impl Nav {
    const fn as_str(self) -> &'static str {
        match self {
            Self::Home => "home",
            Self::Csaf => "csaf",
            Self::Import => "import",
            Self::Export => "export",
            Self::Settings => "settings",
            Self::Info => "info",
        }
    }
}

/// Wrap page content in the shared HTML layout.
///
/// `theme` must be either `"light"` or `"dark"` and comes from the persisted
/// user settings. The theme drives the `data-bs-theme` attribute which the
/// CSS rules below react to.
#[must_use]
pub fn wrap_page(title: &str, theme: &str, active: Nav, content: &str) -> String {
    let theme = if theme == "dark" { "dark" } else { "light" };
    let active_key = active.as_str();
    format!(
        r#"<!DOCTYPE html>
<html lang="en" data-bs-theme="{theme}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} — CSAF</title>
<style>
  @font-face {{
    font-family: "Roboto";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/static/fonts/roboto/roboto-regular.ttf") format("truetype");
  }}
  @font-face {{
    font-family: "Roboto";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/static/fonts/roboto/roboto-bold.ttf") format("truetype");
  }}
  @font-face {{
    font-family: "Roboto Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/static/fonts/roboto-mono/roboto-mono-regular.ttf") format("truetype");
  }}
  @font-face {{
    font-family: "Roboto Mono";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/static/fonts/roboto-mono/roboto-mono-bold.ttf") format("truetype");
  }}
  :root {{
    --bg: #f5f5f5;
    --surface: #ffffff;
    --text: #1a1a1a;
    --muted: #555555;
    --border: #e0e0e0;
    --primary: #8b1a1a;
    --primary-hover: #a0252a;
    --primary-contrast: #ffffff;
    --link: #8b1a1a;
    --row-hover: #f7f7f9;
    --input-bg: #ffffff;
    --input-border: #bdbdbd;
    --shadow: 0 1px 3px rgba(0,0,0,0.08);
    --badge-create-bg: #e8f5e9; --badge-create-fg: #2e7d32;
    --badge-update-bg: #fff3e0; --badge-update-fg: #e65100;
    --badge-delete-bg: #fce4ec; --badge-delete-fg: #c62828;
    --badge-import-bg: #f3e5f5; --badge-import-fg: #6a1b9a;
    --badge-export-bg: #ede7f6; --badge-export-fg: #4527a0;
    --sev-critical: #b71c1c; --sev-high: #ef6c00; --sev-medium: #f9a825;
    --sev-low: #2e7d32; --sev-none: #616161;
  }}
  html[data-bs-theme="dark"] {{
    --bg: #0f1115;
    --surface: #181b22;
    --text: #e9ecef;
    --muted: #9aa0a6;
    --border: #2a2f3a;
    --primary: #c44545;
    --primary-hover: #d85c5c;
    --primary-contrast: #ffffff;
    --link: #e89696;
    --row-hover: #1f232c;
    --input-bg: #12151b;
    --input-border: #394150;
    --shadow: 0 1px 3px rgba(0,0,0,0.5);
    --badge-create-bg: #1b3a24; --badge-create-fg: #9ef5b6;
    --badge-update-bg: #4a2e10; --badge-update-fg: #ffc889;
    --badge-delete-bg: #4a1c29; --badge-delete-fg: #ff98b3;
    --badge-import-bg: #3a1f4d; --badge-import-fg: #d9a6ff;
    --badge-export-bg: #221a4a; --badge-export-fg: #b2a4ff;
  }}
  * {{ box-sizing: border-box; }}
  html, body {{
    font-family: "Roboto", system-ui, -apple-system, "Segoe UI", sans-serif;
  }}
  code, pre, kbd, samp, tt {{
    font-family: "Roboto Mono", "SF Mono", Menlo, Consolas, monospace;
  }}
  body {{
    margin: 0; padding: 0; background: var(--bg); color: var(--text);
    line-height: 1.5;
    font-weight: 400;
  }}
  nav.primary {{
    background: var(--primary); color: var(--primary-contrast);
    padding: 0.6rem 1.5rem; display: flex; align-items: center;
    gap: 1.25rem; flex-wrap: wrap;
    border-bottom: 3px solid rgba(0,0,0,0.15);
  }}
  nav.primary a {{
    color: var(--primary-contrast); text-decoration: none;
    font-size: 0.9rem; padding: 0.35rem 0.6rem; border-radius: 4px;
  }}
  nav.primary a:hover {{ background: rgba(255,255,255,0.14); }}
  nav.primary a.active {{ background: rgba(255,255,255,0.22); font-weight: 600; }}
  nav.primary .brand {{
    display: flex; align-items: center; gap: 0.6rem;
    margin-right: 0.75rem; text-decoration: none;
  }}
  nav.primary .brand img {{
    height: 36px; width: auto; display: block;
    background: #ffffff; padding: 4px 8px; border-radius: 4px;
  }}
  nav.primary .brand .brand-text {{
    font-weight: 700; font-size: 1.2rem; letter-spacing: 0.02em;
    color: var(--primary-contrast);
  }}
  nav.primary .spacer {{ flex: 1; }}
  nav.primary .theme-toggle {{
    background: transparent; border: 1px solid rgba(255,255,255,0.5);
    color: var(--primary-contrast); padding: 0.35rem 0.75rem; border-radius: 4px;
    cursor: pointer; font-size: 0.85rem; font-family: inherit;
  }}
  nav.primary .theme-toggle:hover {{ background: rgba(255,255,255,0.14); }}
  main {{ max-width: 1200px; margin: 2rem auto; padding: 0 1.5rem; }}
  h1, h2, h3 {{ color: var(--text); }}
  h1 {{ margin-top: 0; }}
  a {{ color: var(--link); }}
  .card {{
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem;
    box-shadow: var(--shadow);
  }}
  .stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }}
  .stat {{ text-align: center; }}
  .stat .number {{ font-size: 2rem; font-weight: bold; color: var(--primary); }}
  .stat .label {{ font-size: 0.85rem; color: var(--muted); margin-top: 0.25rem; }}
  table {{ width: 100%; border-collapse: collapse; }}
  th, td {{
    text-align: left; padding: 0.55rem 0.75rem;
    border-bottom: 1px solid var(--border); font-size: 0.9rem;
  }}
  th {{ background: var(--row-hover); font-weight: 600; color: var(--muted); }}
  tr:hover td {{ background: var(--row-hover); }}
  label {{ display: block; margin-bottom: 0.35rem; font-weight: 500; font-size: 0.9rem; }}
  input[type=text], input[type=number], input[type=url], input[type=email],
  select, textarea {{
    width: 100%; padding: 0.5rem 0.65rem; border: 1px solid var(--input-border);
    border-radius: 4px; background: var(--input-bg); color: var(--text);
    font-family: inherit; font-size: 0.9rem;
  }}
  textarea {{ font-family: "Roboto Mono", "SF Mono", Menlo, monospace; font-size: 0.85rem; }}
  .form-row {{ display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }}
  .form-row.single {{ grid-template-columns: 1fr; }}
  .btn {{
    display: inline-block; padding: 0.55rem 1.1rem; border: none; border-radius: 4px;
    background: var(--primary); color: var(--primary-contrast); font-size: 0.9rem;
    font-weight: 500; cursor: pointer; text-decoration: none;
  }}
  .btn:hover {{ filter: brightness(1.1); }}
  .btn.secondary {{ background: transparent; color: var(--link); border: 1px solid var(--border); }}
  .btn.danger {{ background: #c62828; }}
  .badge {{ display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.8rem; font-weight: 500; }}
  .badge-create {{ background: var(--badge-create-bg); color: var(--badge-create-fg); }}
  .badge-update {{ background: var(--badge-update-bg); color: var(--badge-update-fg); }}
  .badge-delete {{ background: var(--badge-delete-bg); color: var(--badge-delete-fg); }}
  .badge-import {{ background: var(--badge-import-bg); color: var(--badge-import-fg); }}
  .badge-export {{ background: var(--badge-export-bg); color: var(--badge-export-fg); }}
  .sev-critical {{ color: var(--sev-critical); font-weight: 600; }}
  .sev-high     {{ color: var(--sev-high); font-weight: 600; }}
  .sev-medium   {{ color: var(--sev-medium); font-weight: 600; }}
  .sev-low      {{ color: var(--sev-low); font-weight: 600; }}
  .sev-none     {{ color: var(--sev-none); }}
  .flash        {{ padding: 0.75rem 1rem; border-radius: 4px; margin-bottom: 1rem; }}
  .flash.success {{ background: var(--badge-create-bg); color: var(--badge-create-fg); }}
  .flash.error   {{ background: var(--badge-delete-bg); color: var(--badge-delete-fg); }}
  .muted {{ color: var(--muted); font-size: 0.85rem; }}
  /* TLP 2.0 color-coding per https://www.first.org/tlp/
     Background + font colours taken from the FIRST spec. */
  .tlp-select                            {{ font-weight: 600; }}
  .tlp-select.tlp-red                    {{ background: #FF2B2B; color: #000; }}
  .tlp-select.tlp-amber,
  .tlp-select.tlp-amber-strict           {{ background: #FFC000; color: #000; }}
  .tlp-select.tlp-green                  {{ background: #33FF00; color: #000; }}
  .tlp-select.tlp-clear                  {{ background: #FFFFFF; color: #000;
                                              border: 1px solid var(--input-border); }}
  .tlp-select.tlp-unknown                {{ background: var(--input-bg); color: var(--text); }}
  .tlp-select option[value="RED"]            {{ background: #FF2B2B; color: #000; }}
  .tlp-select option[value="AMBER"],
  .tlp-select option[value="AMBER+STRICT"]   {{ background: #FFC000; color: #000; }}
  .tlp-select option[value="GREEN"]          {{ background: #33FF00; color: #000; }}
  .tlp-select option[value="CLEAR"]          {{ background: #FFFFFF; color: #000; }}
  footer {{ text-align: center; padding: 2rem 1rem; color: var(--muted); font-size: 0.8rem; }}
</style>
<script>
  // Sync the background colour of every .tlp-select to the current value.
  // Accepted labels: CLEAR, GREEN, AMBER, AMBER+STRICT, RED. Anything
  // else maps to `tlp-unknown` so hostile document values cannot inject
  // arbitrary class names.
  document.addEventListener("DOMContentLoaded", function() {{
    var allowed = {{
      "CLEAR": "tlp-clear",
      "GREEN": "tlp-green",
      "AMBER": "tlp-amber",
      "AMBER+STRICT": "tlp-amber-strict",
      "RED": "tlp-red"
    }};
    document.querySelectorAll(".tlp-select").forEach(function(sel) {{
      var sync = function() {{
        var key = allowed[sel.value] ? allowed[sel.value] : "tlp-unknown";
        sel.className = "tlp-select " + key;
      }};
      sel.addEventListener("change", sync);
      sync();
    }});
  }});
</script>
</head>
<body>
<nav class="primary">
  <a href="/" class="brand" aria-label="ndaal CSAF home">
    <img src="/static/img/logo.png" alt="ndaal — information security &amp; compliance">
    <span class="brand-text">CSAF</span>
  </a>
  <a href="/" class="{home_cls}">Dashboard</a>
  <a href="/csaf" class="{csaf_cls}">CSAF</a>
  <a href="/admin/import" class="{import_cls}">Import</a>
  <a href="/admin/export" class="{export_cls}">Export</a>
  <a href="/settings" class="{settings_cls}">Settings</a>
  <a href="/info/about" class="{info_cls}">Info</a>
  <span class="spacer"></span>
  <form method="post" action="/settings/toggle-theme" style="margin:0">
    <button type="submit" class="theme-toggle" title="Toggle theme">
      {theme_icon}
    </button>
  </form>
</nav>
<main>
{content}
</main>
<footer>
  CSAF · Pierre Gronau — ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH &amp; Co KG, Cologne · Apache-2.0
</footer>
</body>
</html>"#,
        title = title,
        theme = theme,
        content = content,
        theme_icon = if theme == "dark" {
            "☀ Light"
        } else {
            "☾ Dark"
        },
        home_cls = if active_key == "home" { "active" } else { "" },
        csaf_cls = if active_key == "csaf" { "active" } else { "" },
        import_cls = if active_key == "import" { "active" } else { "" },
        export_cls = if active_key == "export" { "active" } else { "" },
        settings_cls = if active_key == "settings" {
            "active"
        } else {
            ""
        },
        info_cls = if active_key == "info" { "active" } else { "" },
    )
}

/// Map a TLP 2.0 label to its CSS class name.
///
/// Returns one of the five fixed classes (`tlp-clear`, `tlp-green`,
/// `tlp-amber`, `tlp-amber-strict`, `tlp-red`) for a known label, or
/// `tlp-unknown` for anything else. This is the server-side mirror of
/// the inline script injected by [`wrap_page`] and guarantees that a
/// hostile document cannot produce an HTML-injectable class name.
#[must_use]
pub fn tlp_color_class(label: &str) -> &'static str {
    match label {
        "CLEAR" => "tlp-clear",
        "GREEN" => "tlp-green",
        "AMBER" => "tlp-amber",
        "AMBER+STRICT" => "tlp-amber-strict",
        "RED" => "tlp-red",
        _ => "tlp-unknown",
    }
}

/// HTML-escape a string for safe inclusion in element text or attribute values.
#[must_use]
pub fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

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

    #[test]
    fn test_wrap_page_light_theme_attribute() {
        let html = wrap_page("Test", "light", Nav::Home, "<p>body</p>");
        assert!(html.contains(r#"data-bs-theme="light""#));
        assert!(html.contains("☾ Dark"));
    }

    #[test]
    fn test_wrap_page_dark_theme_attribute() {
        let html = wrap_page("Test", "dark", Nav::Home, "<p>body</p>");
        assert!(html.contains(r#"data-bs-theme="dark""#));
        assert!(html.contains("☀ Light"));
    }

    #[test]
    fn test_wrap_page_invalid_theme_falls_back_to_light() {
        let html = wrap_page("Test", "gibberish", Nav::Home, "");
        assert!(html.contains(r#"data-bs-theme="light""#));
    }

    #[test]
    fn test_wrap_page_highlights_active_nav() {
        let html = wrap_page("Settings", "light", Nav::Settings, "");
        assert!(html.contains(r#"href="/settings" class="active""#));
        assert!(html.contains(r#"href="/" class="""#));
    }

    #[test]
    fn test_wrap_page_uses_csaf_branding_not_csaf_crud() {
        let html = wrap_page("Test", "light", Nav::Home, "");
        assert!(html.contains(r#"class="brand-text">CSAF</span>"#));
        assert!(!html.contains("CSAF CRUD"));
        assert!(html.contains("Test — CSAF</title>"));
    }

    #[test]
    fn test_wrap_page_includes_logo_image() {
        let html = wrap_page("Test", "light", Nav::Home, "");
        assert!(html.contains(r#"<img src="/static/img/logo.png""#));
        assert!(html.contains("alt=\"ndaal"));
    }

    #[test]
    fn test_layout_uses_burgundy_red_not_blue() {
        let html = wrap_page("Test", "light", Nav::Home, "");
        assert!(
            html.contains("#8b1a1a"),
            "Should use burgundy red from logo"
        );
        assert!(!html.contains("#1a237e"), "Should not contain old blue");
    }

    #[test]
    fn test_html_escape_special_chars() {
        assert_eq!(html_escape("<script>"), "&lt;script&gt;");
        assert_eq!(html_escape("a & b"), "a &amp; b");
        assert_eq!(html_escape(r#"'"<>&"#), "&#39;&quot;&lt;&gt;&amp;");
    }

    #[test]
    fn test_tlp_color_class_whitelist() {
        assert_eq!(tlp_color_class("CLEAR"), "tlp-clear");
        assert_eq!(tlp_color_class("GREEN"), "tlp-green");
        assert_eq!(tlp_color_class("AMBER"), "tlp-amber");
        assert_eq!(tlp_color_class("AMBER+STRICT"), "tlp-amber-strict");
        assert_eq!(tlp_color_class("RED"), "tlp-red");
    }

    #[test]
    fn test_tlp_color_class_rejects_untrusted() {
        // Hostile strings must never produce an HTML-injectable class.
        for bad in [
            "",
            "clear",
            "red\"><script>alert(1)</script>",
            " CLEAR ",
            "WHITE",
            "  ",
        ] {
            let class = tlp_color_class(bad);
            assert_eq!(class, "tlp-unknown", "bad={bad:?} -> {class}");
            // Class name is ASCII-only, no spaces, no quotes.
            assert!(
                class
                    .bytes()
                    .all(|b| b.is_ascii_alphanumeric() || b == b'-')
            );
        }
    }

    #[test]
    fn test_wrap_page_includes_tlp_color_css() {
        let html = wrap_page("Test", "light", Nav::Home, "");
        assert!(html.contains(".tlp-select.tlp-red"));
        assert!(html.contains(".tlp-select.tlp-amber"));
        assert!(html.contains(".tlp-select.tlp-green"));
        assert!(html.contains(".tlp-select.tlp-clear"));
        // And the accompanying JS syncs on change + DOMContentLoaded.
        assert!(html.contains("DOMContentLoaded"));
        assert!(html.contains(r#""AMBER+STRICT": "tlp-amber-strict""#));
    }
}
