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

//! Import and export administration route handlers.
//!
//! Provides pages for importing CSAF documents from a directory and
//! exporting all stored documents to the filesystem.

use std::path::Path;

use http::{Response, StatusCode};

use csaf_core::audit_export::{AuditExportOptions, export_audit_log};
use csaf_core::dump::dump_database;
use csaf_core::export::export_document;
use csaf_core::import::import_directory;

use crate::app_state::AppState;
use crate::router::{Body, form_checkbox, html_response, parse_form_body};
use crate::routes::layout::{Nav, html_escape, wrap_page};

// ---------------------------------------------------------------------------
// Import handlers
// ---------------------------------------------------------------------------

/// `GET /admin/import` -- Import configuration page.
pub async fn import_page(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let settings = state.settings();
    let content = format!(
        r#"<h1>Import CSAF Documents</h1>
<div class="card">
  <p>Import CSAF JSON documents from the configured import directory.
  Every file is <strong>validated</strong> before being stored. Files whose
  CSAF schema or semantic rules fail hard validation are skipped with an
  error entry in the result.</p>
  <div class="flash success">
    <strong>Import directory:</strong> <code>{import_dir}</code>
  </div>
  <p>Place <code>*.json</code> CSAF files in the import directory (subdirectories are scanned
  recursively). Files named <code>provider-metadata.json</code> are skipped automatically.</p>
  <form method="POST" action="/admin/import"
        onsubmit="this.querySelector('button').disabled=true; this.querySelector('button').textContent='Importing...';">
    <button type="submit" class="btn">Start Import</button>
  </form>
</div>"#,
        import_dir = html_escape(&settings.import_directory),
    );

    html_response(
        StatusCode::OK,
        wrap_page("Import", &settings.theme, Nav::Import, &content),
    )
}

/// `POST /admin/import` -- Execute the import.
pub async fn import_execute(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let settings = state.settings();
    let import_dir = Path::new(&settings.import_directory);

    let result = import_directory(import_dir, state.csaf_storage());

    match result {
        Ok(import_result) => {
            let _ = state.db_pool().with_conn(|conn| {
                csaf_models::audit_log::record(
                    conn,
                    "import",
                    "bulk-import",
                    None,
                    Some(&format!(
                        "imported={}, skipped={}, errors={}",
                        import_result.imported,
                        import_result.skipped,
                        import_result.errors.len(),
                    )),
                )
            });

            let mut error_section = String::new();
            if !import_result.errors.is_empty() {
                error_section.push_str(r#"<div class="flash error"><strong>Errors:</strong><ul>"#);
                for err in &import_result.errors {
                    error_section.push_str(&format!("<li>{}</li>", html_escape(err)));
                }
                error_section.push_str("</ul></div>");
            }

            let content = format!(
                r#"<h1>Import Complete</h1>
<div class="flash success">
  <strong>Results:</strong>
  {imported} document(s) imported, {skipped} skipped.
</div>
{error_section}
<a class="btn" href="/csaf">View Documents</a>
<a class="btn secondary" href="/admin/import" style="margin-left:0.5rem;">Import Again</a>"#,
                imported = import_result.imported,
                skipped = import_result.skipped,
            );

            html_response(
                StatusCode::OK,
                wrap_page("Import Complete", &settings.theme, Nav::Import, &content),
            )
        },
        Err(err) => {
            let content = format!(
                r#"<h1>Import Failed</h1>
<div class="flash error">{err}</div>
<a class="btn secondary" href="/admin/import">Back</a>"#,
                err = html_escape(&err.to_string()),
            );
            html_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                wrap_page("Import Failed", &settings.theme, Nav::Import, &content),
            )
        },
    }
}

// ---------------------------------------------------------------------------
// Export handlers
// ---------------------------------------------------------------------------

/// `GET /admin/export` -- Export configuration page.
pub async fn export_page(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let settings = state.settings();
    let doc_count = state.csaf_storage().count_documents().unwrap_or(0);

    let content = format!(
        r#"<h1>Export CSAF Documents</h1>
<div class="card">
  <p>Export all stored CSAF documents to the filesystem with sidecar hash files.
  Each document is <strong>validated</strong> and its <code>csaf_version</code> is
  rewritten to match the currently active CSAF mode.</p>
  <div class="flash success">
    <strong>Export directory:</strong> <code>{export_dir}</code><br>
    <strong>CSAF mode (written into files):</strong> <code>{mode}</code><br>
    <strong>Documents to export:</strong> {doc_count}<br>
    <strong>SHA-256 sidecars:</strong> {sha256}<br>
    <strong>SHA-512 sidecars:</strong> {sha512}<br>
    <strong>SHA3-512 sidecars:</strong> {sha3}
  </div>
  <form method="POST" action="/admin/export"
        onsubmit="this.querySelector('button').disabled=true; this.querySelector('button').textContent='Exporting...';">
    <button type="submit" class="btn">Start Export</button>
  </form>
</div>
<div class="card">
  <h2>Dump Database</h2>
  <p>Snapshot the live <code>csaf.redb</code> and <code>csaf.sqlite</code>
  databases into the configured <strong>Dump Directory</strong> with matching
  SHA-256, SHA-512, and SHA3-512 hash sidecars. The sqlite snapshot uses the
  online-backup API so writes in flight are not lost, and the redb copy is
  pinned by an MVCC read transaction for the duration of the copy.</p>
  <div class="flash success">
    <strong>Dump directory:</strong> <code>{dump_dir}</code><br>
    <strong>SHA-256 sidecars:</strong> {sha256}<br>
    <strong>SHA-512 sidecars:</strong> {sha512}<br>
    <strong>SHA3-512 sidecars:</strong> {sha3}
  </div>
  <form method="POST" action="/admin/dump"
        onsubmit="this.querySelector('button').disabled=true; this.querySelector('button').textContent='Dumping...';">
    <button type="submit" class="btn">Start Dump</button>
  </form>
</div>
<div class="card">
  <h2>Export Audit Log</h2>
  <p>Export every row of the <code>audit_log</code> table (CRUD, settings
  changes, import / export events) in four formats, each with matching
  hash sidecars. Outputs land in the <strong>Export Directory</strong>
  under an ISO-8601 timestamp stem:
  <code>audit-YYYY-MM-DDTHH-MM-SSZ.{{md,csv,json,sarif}}</code>.
  SARIF 2.1.0 output is self-validated before write.</p>
  <div class="flash success">
    <strong>Export directory:</strong> <code>{export_dir}</code><br>
    <strong>SHA-256 sidecars:</strong> {sha256}<br>
    <strong>SHA-512 sidecars:</strong> {sha512}<br>
    <strong>SHA3-512 sidecars:</strong> {sha3}
  </div>
  <form method="POST" action="/admin/export-audit"
        onsubmit="this.querySelector('button').disabled=true; this.querySelector('button').textContent='Exporting audit log...';">
    <div style="margin-bottom:0.75rem;">
      <label style="font-weight:normal;"><input type="checkbox" name="audit_markdown" checked> Markdown (<code>.md</code>)</label>
      <label style="font-weight:normal; margin-left:1rem;"><input type="checkbox" name="audit_csv" checked> CSV (<code>.csv</code>)</label>
      <label style="font-weight:normal; margin-left:1rem;"><input type="checkbox" name="audit_json" checked> JSON (<code>.json</code>)</label>
      <label style="font-weight:normal; margin-left:1rem;"><input type="checkbox" name="audit_sarif" checked> SARIF 2.1 (<code>.sarif</code>)</label>
    </div>
    <button type="submit" class="btn">Export Audit Log</button>
  </form>
</div>"#,
        export_dir = html_escape(&settings.export_directory),
        dump_dir = html_escape(&settings.dump_directory),
        mode = html_escape(&settings.csaf_mode),
        sha256 = if settings.sidecar_sha256 {
            "enabled"
        } else {
            "disabled"
        },
        sha512 = if settings.sidecar_sha512 {
            "enabled"
        } else {
            "disabled"
        },
        sha3 = if settings.sidecar_sha3_512 {
            "enabled"
        } else {
            "disabled"
        },
    );

    html_response(
        StatusCode::OK,
        wrap_page("Export", &settings.theme, Nav::Export, &content),
    )
}

/// `POST /admin/export` -- Execute the export.
pub async fn export_execute(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let settings = state.settings();
    let metas = state
        .csaf_storage()
        .list_meta(10_000, 0)
        .unwrap_or_default();

    let mut exported: usize = 0;
    let mut errors: Vec<String> = Vec::new();

    for meta in &metas {
        match state.csaf_storage().get_document(&meta.tracking_id) {
            Ok(Some(doc)) => match export_document(&doc, &settings) {
                Ok(_path) => {
                    exported += 1;
                },
                Err(err) => {
                    errors.push(format!("{}: {err}", meta.tracking_id));
                },
            },
            Ok(None) => {
                errors.push(format!(
                    "{}: document not found in storage",
                    meta.tracking_id
                ));
            },
            Err(err) => {
                errors.push(format!("{}: {err}", meta.tracking_id));
            },
        }
    }

    // Audit log.
    let _ = state.db_pool().with_conn(|conn| {
        csaf_models::audit_log::record(
            conn,
            "export",
            "bulk-export",
            None,
            Some(&format!("exported={exported}, errors={}", errors.len())),
        )
    });

    let mut error_section = String::new();
    if !errors.is_empty() {
        error_section.push_str(r#"<div class="flash error"><strong>Errors:</strong><ul>"#);
        for err in &errors {
            error_section.push_str(&format!("<li>{}</li>", html_escape(err)));
        }
        error_section.push_str("</ul></div>");
    }

    let content = format!(
        r#"<h1>Export Complete</h1>
<div class="flash success">
  <strong>Results:</strong>
  {exported} document(s) exported (CSAF {mode}) to <code>{export_dir}</code>.
</div>
{error_section}
<a class="btn" href="/csaf">View Documents</a>
<a class="btn secondary" href="/admin/export" style="margin-left:0.5rem;">Export Again</a>"#,
        mode = html_escape(&settings.csaf_mode),
        export_dir = html_escape(&settings.export_directory),
    );

    html_response(
        StatusCode::OK,
        wrap_page("Export Complete", &settings.theme, Nav::Export, &content),
    )
}

// ---------------------------------------------------------------------------
// Database dump handler
// ---------------------------------------------------------------------------

/// `POST /admin/dump` -- Snapshot the live database into the Dump Directory.
pub async fn dump_execute(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let settings = state.settings();
    let data_dir = state.config().data_dir.clone();
    let dump_dir = Path::new(&settings.dump_directory).to_path_buf();

    let result = dump_database(
        &data_dir,
        &dump_dir,
        state.csaf_storage(),
        state.db_pool(),
        &settings,
    );

    match result {
        Ok(dump) => {
            // Audit log the dump.
            let _ = state.db_pool().with_conn(|conn| {
                csaf_models::audit_log::record(
                    conn,
                    "dump",
                    "db-dump",
                    None,
                    Some(&format!(
                        "redb={} bytes, sqlite={} bytes, sidecars={}",
                        dump.redb_bytes,
                        dump.sqlite_bytes,
                        dump.sidecars.len()
                    )),
                )
            });

            let mut sidecar_list = String::new();
            for side in &dump.sidecars {
                sidecar_list.push_str(&format!(
                    "<li><code>{}</code></li>",
                    html_escape(&side.display().to_string())
                ));
            }

            let content = format!(
                r#"<h1>Dump Complete</h1>
<div class="flash success">
  <strong>Results:</strong> database snapshot written to
  <code>{dump_dir}</code> with timestamp <code>{ts}</code>.
</div>
<div class="card">
  <table>
    <tr><th>redb</th><td><code>{redb_path}</code> ({redb_bytes} bytes)</td></tr>
    <tr><th>sqlite</th><td><code>{sqlite_path}</code> ({sqlite_bytes} bytes)</td></tr>
  </table>
  <h2>Sidecar files</h2>
  <ul>{sidecar_list}</ul>
</div>
<a class="btn" href="/admin/export">Back to Export</a>"#,
                dump_dir = html_escape(&settings.dump_directory),
                ts = html_escape(&dump.timestamp),
                redb_path = html_escape(&dump.redb_path.display().to_string()),
                redb_bytes = dump.redb_bytes,
                sqlite_path = html_escape(&dump.sqlite_path.display().to_string()),
                sqlite_bytes = dump.sqlite_bytes,
            );

            html_response(
                StatusCode::OK,
                wrap_page("Dump Complete", &settings.theme, Nav::Export, &content),
            )
        },
        Err(err) => {
            let content = format!(
                r#"<h1>Dump Failed</h1>
<div class="flash error">
  <strong>Error:</strong> {err}
</div>
<a class="btn secondary" href="/admin/export">Back to Export</a>"#,
                err = html_escape(&err.to_string()),
            );
            html_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                wrap_page("Dump Failed", &settings.theme, Nav::Export, &content),
            )
        },
    }
}

// ---------------------------------------------------------------------------
// Audit log export handler
// ---------------------------------------------------------------------------

/// `POST /admin/export-audit` -- Export the `audit_log` table as
/// Markdown, CSV, JSON, and SARIF 2.1 files with matching hash sidecars.
///
/// Every format is optional — the form's four checkboxes drive the
/// [`AuditExportOptions`] struct. If the user unticks every box the
/// handler reports a friendly "nothing selected" message instead of
/// erroring.
pub async fn audit_export_execute(
    state: AppState,
    parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let settings = state.settings();

    // Parse the four checkboxes. A form parse error is non-fatal — we
    // fall back to exporting all four formats (matches the defaults the
    // form ships pre-checked with).
    let opts = match parse_form_body(&parts) {
        Ok(form) => AuditExportOptions {
            markdown: form_checkbox(&form, "audit_markdown"),
            csv: form_checkbox(&form, "audit_csv"),
            json: form_checkbox(&form, "audit_json"),
            sarif: form_checkbox(&form, "audit_sarif"),
        },
        Err(_) => AuditExportOptions::default(),
    };

    if !(opts.markdown || opts.csv || opts.json || opts.sarif) {
        let content = r#"<h1>Audit Log Export</h1>
<div class="flash error">
  No output format was selected. Please tick at least one of Markdown,
  CSV, JSON, or SARIF.
</div>
<a class="btn secondary" href="/admin/export">Back to Export</a>"#
            .to_owned();
        return html_response(
            StatusCode::BAD_REQUEST,
            wrap_page("Audit Log Export", &settings.theme, Nav::Export, &content),
        );
    }

    let out_dir = Path::new(&settings.export_directory).to_path_buf();
    let result = export_audit_log(state.db_pool(), &out_dir, &settings, &opts);

    match result {
        Ok(res) => {
            let _ = state.db_pool().with_conn(|conn| {
                csaf_models::audit_log::record(
                    conn,
                    "export",
                    "audit-log",
                    None,
                    Some(&format!(
                        "rows={}, payloads={}, sidecars={}",
                        res.rows,
                        res.written.len(),
                        res.sidecars.len()
                    )),
                )
            });

            let mut payload_list = String::new();
            for p in &res.written {
                payload_list.push_str(&format!(
                    "<li><code>{}</code></li>",
                    html_escape(&p.display().to_string())
                ));
            }
            let mut sidecar_list = String::new();
            for s in &res.sidecars {
                sidecar_list.push_str(&format!(
                    "<li><code>{}</code></li>",
                    html_escape(&s.display().to_string())
                ));
            }

            let content = format!(
                r#"<h1>Audit Log Export Complete</h1>
<div class="flash success">
  <strong>Results:</strong>
  {rows} audit row(s) exported into {payloads} payload file(s) with {sidecars} sidecar(s),
  timestamp <code>{ts}</code>.
</div>
<div class="card">
  <h2>Payloads</h2>
  <ul>{payload_list}</ul>
  <h2>Sidecars</h2>
  <ul>{sidecar_list}</ul>
</div>
<a class="btn" href="/admin/export">Back to Export</a>"#,
                rows = res.rows,
                payloads = res.written.len(),
                sidecars = res.sidecars.len(),
                ts = html_escape(&res.timestamp),
            );

            html_response(
                StatusCode::OK,
                wrap_page(
                    "Audit Log Export Complete",
                    &settings.theme,
                    Nav::Export,
                    &content,
                ),
            )
        },
        Err(err) => {
            let content = format!(
                r#"<h1>Audit Log Export Failed</h1>
<div class="flash error">
  <strong>Error:</strong> {err}
</div>
<a class="btn secondary" href="/admin/export">Back to Export</a>"#,
                err = html_escape(&err.to_string()),
            );
            html_response(
                StatusCode::INTERNAL_SERVER_ERROR,
                wrap_page(
                    "Audit Log Export Failed",
                    &settings.theme,
                    Nav::Export,
                    &content,
                ),
            )
        },
    }
}
