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

//! `sarif-viewer` binary: a localhost web viewer plus `validate` / `export`
//! command-line subcommands for SARIF 2.1.0 and 2.2 (draft) documents.

#![forbid(unsafe_code)]

use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;

use clap::{Args, Parser, Subcommand};
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;

use sarif_viewer::app_state::AppState;
use sarif_viewer::export::{self, ExportFormat};
use sarif_viewer::routes::build_viewer_router;
use sarif_viewer::sidecar::SidecarHashes;
use sarif_viewer::store::SarifStore;
use sarif_viewer::{import, router, server, tls, validation};

/// Default loopback bind address.
const DEFAULT_BIND: &str = "127.0.0.1";
/// Default HTTPS (TCP) listen port.
const DEFAULT_PORT: u16 = 8580;

/// Offline viewer, validator and multi-format exporter for SARIF 2.1.0 / 2.2.
#[derive(Parser, Debug)]
#[command(name = "sarif-viewer", version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Subcommand, Debug)]
enum Command {
    /// Start the localhost web viewer (default).
    Serve(ServeArgs),
    /// Schema-validate a SARIF file and print the report (exit 1 if invalid).
    Validate(ValidateArgs),
    /// Validate then export a SARIF file to SARIF/Markdown/ODT/PDF.
    Export(ExportArgs),
}

#[derive(Args, Debug)]
struct ServeArgs {
    /// Bind address (loopback by default).
    #[arg(long, env = "SARIF_BIND", default_value = DEFAULT_BIND)]
    bind: String,
    /// HTTPS (TCP) listen port — HTTP/1.1 + HTTP/2 over TLS 1.3.
    #[arg(long, env = "SARIF_PORT", default_value_t = DEFAULT_PORT)]
    port: u16,
    /// QUIC (UDP) port for HTTP/3 [default: HTTPS port + 1].
    #[arg(long, env = "SARIF_QUIC_PORT")]
    quic_port: Option<u16>,
    /// Disable the HTTP/3 (QUIC) listener.
    #[arg(long)]
    no_quic: bool,
    /// Export-menu target directory.
    #[arg(long, env = "SARIF_EXPORT_DIR")]
    export_dir: Option<String>,
    /// Pre-load a SARIF file or directory at start-up (repeatable).
    #[arg(long = "import", value_name = "PATH")]
    imports: Vec<PathBuf>,
    /// Do not load the built-in sample logs.
    #[arg(long)]
    no_samples: bool,
}

#[derive(Args, Debug)]
struct ValidateArgs {
    /// The SARIF file to validate.
    file: PathBuf,
}

#[derive(Args, Debug)]
struct ExportArgs {
    /// The SARIF file to export.
    file: PathBuf,
    /// Output format(s): sarif, markdown, odt, pdf (repeatable; default: all).
    #[arg(long = "format", value_name = "FMT")]
    formats: Vec<String>,
    /// Output directory (created if missing).
    #[arg(long, default_value = "./sarif_export")]
    out_dir: String,
    /// Do not write checksum sidecars.
    #[arg(long)]
    no_sidecars: bool,
    /// Overwrite existing output files.
    #[arg(long)]
    overwrite: bool,
}

/// A per-user default export directory, falling back to `./sarif_export`.
fn default_export_dir() -> String {
    #[cfg(windows)]
    let home = std::env::var("USERPROFILE").ok();
    #[cfg(not(windows))]
    let home = std::env::var("HOME").ok();
    home.filter(|h| !h.trim().is_empty()).map_or_else(
        || "./sarif_export".to_owned(),
        |home| {
            Path::new(&home)
                .join("sarif-viewer")
                .join("export")
                .to_string_lossy()
                .into_owned()
        },
    )
}

fn main() -> ExitCode {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .init();

    let cli = Cli::parse();
    match cli.command {
        Some(Command::Validate(args)) => cmd_validate(&args.file),
        Some(Command::Export(args)) => cmd_export(&args),
        Some(Command::Serve(args)) => run_serve(args),
        None => run_serve(ServeArgs {
            bind: DEFAULT_BIND.to_owned(),
            port: DEFAULT_PORT,
            quic_port: None,
            no_quic: false,
            export_dir: None,
            imports: Vec::new(),
            no_samples: false,
        }),
    }
}

/// `validate` subcommand.
fn cmd_validate(file: &Path) -> ExitCode {
    // CLI-supplied file to validate (operator-chosen path). See skills/rust-path-security.md.
    let bytes = match std::fs::read(file) { // allow-std-fs: operator-chosen CLI path
        Ok(bytes) => bytes,
        Err(error) => {
            eprintln!("error: cannot read {}: {error}", file.display());
            return ExitCode::FAILURE;
        }
    };
    let doc: serde_json::Value = match serde_json::from_slice(&bytes) {
        Ok(doc) => doc,
        Err(error) => {
            eprintln!("error: {} is not valid JSON: {error}", file.display());
            return ExitCode::FAILURE;
        }
    };
    let report = match validation::validate(&doc, true) {
        Ok(report) => report,
        Err(error) => {
            eprintln!("error: {error}");
            return ExitCode::FAILURE;
        }
    };

    println!("{}: {}", file.display(), report.summary());
    print_errors("structural", &report.structural);
    print_errors("schema", &report.schema_errors);
    if let (Some(label), Some(errors)) = (report.cross_label, &report.cross_errors) {
        if errors.is_empty() {
            println!("cross-check ({label}): clean");
        } else {
            println!("cross-check ({label}): {} advisory issue(s)", errors.len());
        }
    }
    if report.is_valid() {
        ExitCode::SUCCESS
    } else {
        ExitCode::FAILURE
    }
}

/// Print a capped list of validation errors under a heading.
fn print_errors(label: &str, errors: &[String]) {
    if errors.is_empty() {
        return;
    }
    println!("  {label} ({} issue(s)):", errors.len());
    for error in errors.iter().take(40) {
        println!("    - {error}");
    }
    if errors.len() > 40 {
        println!("    … and {} more", errors.len().saturating_sub(40));
    }
}

/// `export` subcommand.
fn cmd_export(args: &ExportArgs) -> ExitCode {
    let formats = parse_formats(&args.formats);
    let store = SarifStore::new();
    // CLI-supplied file to export (operator-chosen path). See skills/rust-path-security.md.
    let bytes = match std::fs::read(&args.file) { // allow-std-fs: operator-chosen CLI path
        Ok(bytes) => bytes,
        Err(error) => {
            eprintln!("error: cannot read {}: {error}", args.file.display());
            return ExitCode::FAILURE;
        }
    };
    let source = args
        .file
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("report")
        .to_owned();
    // load_bytes runs the schema-validation gate.
    let log = match import::load_bytes(&source, &bytes, &store) {
        Ok(log) => log,
        Err(error) => {
            eprintln!("error: {error}");
            return ExitCode::FAILURE;
        }
    };

    let artifacts = match export::build_artifacts(&log, &formats) {
        Ok(artifacts) => artifacts,
        Err(error) => {
            eprintln!("error: {error}");
            return ExitCode::FAILURE;
        }
    };
    let dir = match export::open_export_dir(&args.out_dir) {
        Ok(dir) => dir,
        Err(error) => {
            eprintln!("error: cannot open {}: {error}", args.out_dir);
            return ExitCode::FAILURE;
        }
    };
    let sidecars = if args.no_sidecars {
        SidecarHashes::none()
    } else {
        SidecarHashes::all()
    };
    match export::write_artifacts(&dir, &artifacts, &sidecars, args.overwrite) {
        Ok(written) => {
            println!("Exported {} file(s) to {}:", written.len(), args.out_dir);
            for item in &written {
                println!("  {} ({} bytes, {} sidecar(s))", item.file, item.bytes, item.sidecars.len());
            }
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("error: {error}");
            ExitCode::FAILURE
        }
    }
}

/// Parse `--format` tokens into formats; empty selection means all formats.
fn parse_formats(tokens: &[String]) -> Vec<ExportFormat> {
    if tokens.is_empty() {
        return ExportFormat::ALL.to_vec();
    }
    let mut formats: Vec<ExportFormat> = tokens
        .iter()
        .filter_map(|t| ExportFormat::from_token(t))
        .collect();
    formats.dedup();
    if formats.is_empty() {
        ExportFormat::ALL.to_vec()
    } else {
        formats
    }
}

/// `serve` subcommand: build state and run the web viewer.
fn run_serve(args: ServeArgs) -> ExitCode {
    let runtime = match tokio::runtime::Builder::new_multi_thread().enable_all().build() {
        Ok(runtime) => runtime,
        Err(error) => {
            eprintln!("error: cannot start async runtime: {error}");
            return ExitCode::FAILURE;
        }
    };
    runtime.block_on(async move { serve(args).await })
}

/// Async body of the `serve` subcommand: HTTPS (TLS 1.3) over TCP plus, unless
/// disabled, HTTP/3 over QUIC.
async fn serve(args: ServeArgs) -> ExitCode {
    if let Err(error) = tls::install_crypto_provider() {
        eprintln!("error: {error}");
        return ExitCode::FAILURE;
    }

    let store = SarifStore::new();
    if !args.no_samples {
        let loaded = sarif_viewer::load_samples(&store);
        tracing::info!(loaded, "embedded sample logs loaded");
    }
    for path in &args.imports {
        let outcome = import::import_path(path, &store);
        tracing::info!(
            path = %path.display(),
            ok = outcome.ok_count(),
            err = outcome.err_count(),
            "preloaded path"
        );
        for (source, error) in &outcome.errors {
            tracing::warn!(%source, %error, "preload rejected a file");
        }
    }

    let export_dir = args.export_dir.unwrap_or_else(default_export_dir);
    let quic_port = args.quic_port.unwrap_or(args.port.saturating_add(1));
    let listen = format!("{}:{}", args.bind, args.port);
    let state = AppState::new(store, export_dir, listen.clone());

    // Advertise HTTP/3 via Alt-Svc on every response (before the router serves).
    if !args.no_quic {
        router::set_alt_svc_port(quic_port);
    }

    let router = match build_viewer_router(state) {
        Ok(router) => router,
        Err(error) => {
            eprintln!("error: cannot build router: {error}");
            return ExitCode::FAILURE;
        }
    };

    // One self-signed TLS 1.3 cert backs both listeners.
    let (certs, key) = match tls::generate_self_signed_cert() {
        Ok(pair) => pair,
        Err(error) => {
            eprintln!("error: cannot generate TLS certificate: {error}");
            return ExitCode::FAILURE;
        }
    };

    // HTTPS / TCP acceptor (HTTP/1.1 + HTTP/2).
    let tcp_config = match tls::build_tls13_server_config(
        certs.clone(),
        key.clone_key(),
        vec![b"h2".to_vec(), b"http/1.1".to_vec()],
    ) {
        Ok(config) => config,
        Err(error) => {
            eprintln!("error: cannot build TLS config: {error}");
            return ExitCode::FAILURE;
        }
    };
    let acceptor = TlsAcceptor::from(Arc::new(tcp_config));
    let listener = match TcpListener::bind(&listen).await {
        Ok(listener) => listener,
        Err(error) => {
            eprintln!("error: cannot bind {listen}: {error}");
            return ExitCode::FAILURE;
        }
    };

    // HTTP/3 / QUIC listener (UDP) on its own task.
    if args.no_quic {
        tracing::info!("HTTP/3 (QUIC) disabled by --no-quic");
    } else {
        let quic_listen = format!("{}:{quic_port}", args.bind);
        match quic_listen.parse::<SocketAddr>() {
            Ok(quic_addr) => {
                let quic_router = router.clone();
                tokio::spawn(async move {
                    if let Err(error) = server::run_quic(quic_addr, quic_router, certs, key).await {
                        tracing::error!(%error, "QUIC/HTTP3 server failed");
                    }
                });
                println!("  HTTP/3 (QUIC) on udp://{quic_listen}");
                tracing::info!(%quic_listen, "QUIC/HTTP3 listener spawned (UDP, TLS 1.3)");
            }
            Err(error) => {
                eprintln!("error: invalid QUIC address {quic_listen}: {error}");
                return ExitCode::FAILURE;
            }
        }
    }

    tracing::info!("SARIF Viewer listening on https://{listen} (TLS 1.3; Ctrl-C to stop)");
    println!("SARIF Viewer running at https://{listen}  (TLS 1.3, self-signed; Ctrl-C to stop)");
    server::run_tls(listener, acceptor, router).await;
    ExitCode::SUCCESS
}
