//! Firmware SBOM via tramiton.
//!
//! Phase 2 (full, the default): drive a **reproducible build** with tramiton's
//! `NixBackend` — `analyze` → `seal_and_build` → a sealed lock whose libraries
//! are pinned and whose firmware artifact carries a content hash — then render
//! the SBOM from the lock plus deep binary SCA of pre-compiled inputs. This is
//! the complete bill of materials (toolchain + every fetched library + the
//! firmware image), the same one `tramiton sbom` produces.
//!
//! Phase 1 fallback (analysis-only): when no nix backend is available or the
//! build fails, fall back to the resolvable libraries + toolchain from the build
//! plan alone (no build). A scan therefore always yields *something*, and a nix
//! that can't run in the deployment never breaks a scan.

use std::path::Path;

use compliance_core::models::{SbomEntry, TargetType};
use tramiton_repro::ReproBackend;
use tramiton_sbom::ComponentKind;

/// Whether firmware SBOM applies to this target family.
pub fn is_firmware_target(target_type: TargetType) -> bool {
    matches!(
        target_type,
        TargetType::FirmwareBareMetal | TargetType::FirmwareRtos | TargetType::EmbeddedLinuxYocto
    )
}

/// Build SBOM entries for a firmware target from its source tree. Prefers a full
/// reproducible build (sealed lock); falls back to analysis-only. Returns an
/// empty vector when tramiton cannot even form a build plan.
pub async fn firmware_sbom_entries(path: &Path, repo_id: &str) -> Vec<SbomEntry> {
    let p = path.to_path_buf();
    let repo = repo_id.to_string();
    // The whole analyze → seal → build → render sequence is blocking (it shells
    // out to nix), so keep it off the async runtime. Bound it: a firmware build
    // that hangs must not wedge the scan (the orphaned task is abandoned).
    let handle = tokio::task::spawn_blocking(move || build_sbom_blocking(&p, &repo));
    match tokio::time::timeout(std::time::Duration::from_secs(900), handle).await {
        Ok(Ok(entries)) => entries,
        Ok(Err(e)) => {
            tracing::warn!(repo_id, error = %e, "Firmware SBOM: task join error");
            Vec::new()
        }
        Err(_) => {
            tracing::warn!(repo_id, "Firmware SBOM: build exceeded 15m; skipping");
            Vec::new()
        }
    }
}

fn build_sbom_blocking(path: &Path, repo_id: &str) -> Vec<SbomEntry> {
    let repo = tramiton_core::Repo::new(path);
    let plan = match tramiton_core::provider::analyze(&repo) {
        Ok(Some(bp)) => bp,
        Ok(None) => return Vec::new(),
        Err(e) => {
            tracing::warn!(repo_id, error = %e, "Firmware SBOM: tramiton analyze failed");
            return Vec::new();
        }
    };

    // Phase 2: reproducible build → sealed lock → complete SBOM.
    if let Some(backend) = tramiton_repro::NixBackend::detect() {
        match tramiton_repro::seal_and_build(&backend, &plan, path) {
            Ok(lock) => {
                let mut sbom = tramiton_sbom::Sbom::from_lock(&lock, repo_id);
                // Deep binary SCA of any pre-compiled inputs in the tree.
                sbom.components.extend(tramiton_sbom::binary::scan(path));
                let entries = sbom_to_entries(&sbom, repo_id);
                tracing::info!(
                    repo_id,
                    backend = backend.name(),
                    count = entries.len(),
                    "Firmware SBOM: sealed reproducible build"
                );
                return entries;
            }
            Err(e) => {
                tracing::warn!(repo_id, error = %e, "Firmware SBOM: reproducible build failed; falling back to analysis-only")
            }
        }
    } else {
        tracing::info!(
            repo_id,
            "Firmware SBOM: no nix backend available; analysis-only SBOM"
        );
    }

    // Phase 1 fallback: analysis-only (toolchain + resolvable libraries).
    analysis_entries(&plan, repo_id)
}

/// Map a rendered [`tramiton_sbom::Sbom`] (primary firmware + components) into
/// our [`SbomEntry`] rows. Source-file (`File`) components are dropped — they are
/// build inputs, not a dependency inventory.
fn sbom_to_entries(sbom: &tramiton_sbom::Sbom, repo_id: &str) -> Vec<SbomEntry> {
    let mut entries = Vec::new();
    if let Some(primary) = &sbom.primary {
        entries.push(component_to_entry(primary, repo_id));
    }
    for c in &sbom.components {
        if matches!(c.kind, ComponentKind::File) {
            continue;
        }
        entries.push(component_to_entry(c, repo_id));
    }
    entries
}

fn component_to_entry(c: &tramiton_sbom::Component, repo_id: &str) -> SbomEntry {
    let manager = match c.kind {
        ComponentKind::Firmware => "firmware",
        ComponentKind::Library => "library",
        ComponentKind::Toolchain => "toolchain",
        ComponentKind::File => "file",
    };
    let mut entry = SbomEntry::new(
        repo_id.to_string(),
        c.name.clone(),
        c.version.clone().unwrap_or_default(),
        manager.to_string(),
    );
    entry.purl = c.source.clone();
    entry
}

/// Analysis-only components from the build plan: the cross-toolchain plus the
/// resolvable fetched libraries, without a build.
fn analysis_entries(bp: &tramiton_core::BuildPlan, repo_id: &str) -> Vec<SbomEntry> {
    let mut entries = Vec::new();
    if let Some(id) = bp.toolchain.id.clone() {
        let version = bp.toolchain.version.clone().unwrap_or_default();
        entries.push(SbomEntry::new(
            repo_id.to_string(),
            id,
            version,
            "toolchain".to_string(),
        ));
    }
    for lib in tramiton_repro::lock::libraries_from_inputs(&bp.inputs) {
        let mut entry = SbomEntry::new(
            repo_id.to_string(),
            lib.name,
            lib.revision,
            "library".to_string(),
        );
        entry.purl = lib.source;
        entries.push(entry);
    }
    entries
}
