#!/usr/bin/env python3
"""SBOM supply chain vulnerability analysis agent.

Parses CycloneDX and SPDX JSON SBOMs, correlates components against the NVD 2.0 API
for known CVEs, builds dependency graphs with networkx, calculates risk scores, and
generates compliance reports.
"""

import os
import sys
import json
import time
import argparse
from pathlib import Path
from datetime import datetime
from collections import defaultdict

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

try:
    import networkx as nx
    HAS_NETWORKX = True
except ImportError:
    HAS_NETWORKX = False

try:
    from packaging.version import Version, InvalidVersion
    HAS_PACKAGING = True
except ImportError:
    HAS_PACKAGING = False

# NVD API 2.0 configuration
NVD_CVE_API = "https://services.nvd.nist.gov/rest/json/cves/2.0"
NVD_CPE_API = "https://services.nvd.nist.gov/rest/json/cpes/2.0"
NVD_RATE_LIMIT_NO_KEY = 6.0   # seconds between requests without API key
NVD_RATE_LIMIT_WITH_KEY = 0.6  # seconds between requests with API key
NVD_RESULTS_PER_PAGE = 50

# CVSS severity thresholds
SEVERITY_THRESHOLDS = {
    "CRITICAL": 9.0,
    "HIGH": 7.0,
    "MEDIUM": 4.0,
    "LOW": 0.1,
}


class SBOMComponent:
    """Represents a single software component extracted from an SBOM."""

    def __init__(self, name, version, purl=None, cpe=None, component_type="library",
                 licenses=None, supplier=None):
        self.name = name
        self.version = version
        self.purl = purl
        self.cpe = cpe
        self.component_type = component_type
        self.licenses = licenses or []
        self.supplier = supplier
        self.cves = []
        self.max_cvss = 0.0
        self.risk_level = "NONE"

    def to_dict(self):
        return {
            "name": self.name,
            "version": self.version,
            "purl": self.purl,
            "cpe": self.cpe,
            "type": self.component_type,
            "licenses": self.licenses,
            "cves": self.cves,
            "max_cvss": self.max_cvss,
            "risk_level": self.risk_level,
        }


def detect_sbom_format(sbom_data):
    """Detect whether the SBOM is CycloneDX or SPDX format."""
    if isinstance(sbom_data, dict):
        if sbom_data.get("bomFormat") == "CycloneDX":
            return "cyclonedx"
        if "spdxVersion" in sbom_data:
            return "spdx"
        if "components" in sbom_data and any(
            "purl" in c for c in sbom_data.get("components", [])
        ):
            return "cyclonedx"
        if "packages" in sbom_data:
            return "spdx"
    return "unknown"


def parse_cyclonedx(sbom_data):
    """Parse CycloneDX JSON SBOM and extract components and dependencies."""
    components = []
    dependencies = {}

    spec_version = sbom_data.get("specVersion", "unknown")
    print(f"  Format: CycloneDX v{spec_version}")

    for comp in sbom_data.get("components", []):
        name = comp.get("name", "unknown")
        version = comp.get("version", "unknown")
        purl = comp.get("purl")
        cpe = None

        # Extract CPE from multiple possible locations
        if "cpe" in comp:
            cpe = comp["cpe"]
        for prop in comp.get("properties", []):
            if prop.get("name") == "syft:cpe23" or "cpe" in prop.get("name", "").lower():
                cpe = prop.get("value")
                break

        # Extract licenses
        licenses = []
        for lic_entry in comp.get("licenses", []):
            lic = lic_entry.get("license", {})
            if "id" in lic:
                licenses.append(lic["id"])
            elif "name" in lic:
                licenses.append(lic["name"])

        component = SBOMComponent(
            name=name,
            version=version,
            purl=purl,
            cpe=cpe,
            component_type=comp.get("type", "library"),
            licenses=licenses,
            supplier=comp.get("supplier", {}).get("name"),
        )
        components.append(component)

    # Parse dependency graph
    for dep_entry in sbom_data.get("dependencies", []):
        ref = dep_entry.get("ref", "")
        depends_on = dep_entry.get("dependsOn", [])
        dependencies[ref] = depends_on

    return components, dependencies


def parse_spdx(sbom_data):
    """Parse SPDX JSON SBOM and extract components and dependencies."""
    components = []
    dependencies = {}

    spdx_version = sbom_data.get("spdxVersion", "unknown")
    print(f"  Format: SPDX {spdx_version}")

    spdx_id_to_purl = {}

    for pkg in sbom_data.get("packages", []):
        name = pkg.get("name", "unknown")
        version = pkg.get("versionInfo", "unknown")
        spdx_id = pkg.get("SPDXID", "")
        purl = None
        cpe = None

        for ref in pkg.get("externalRefs", []):
            ref_type = ref.get("referenceType", "")
            locator = ref.get("referenceLocator", "")
            if ref_type == "purl" or "purl" in ref_type.lower():
                purl = locator
            elif ref_type == "cpe23Type" or "cpe" in ref_type.lower():
                cpe = locator

        licenses = []
        concluded = pkg.get("licenseConcluded", "NOASSERTION")
        if concluded and concluded != "NOASSERTION":
            licenses.append(concluded)
        declared = pkg.get("licenseDeclared", "NOASSERTION")
        if declared and declared != "NOASSERTION" and declared not in licenses:
            licenses.append(declared)

        component = SBOMComponent(
            name=name,
            version=version,
            purl=purl,
            cpe=cpe,
            component_type="library",
            licenses=licenses,
            supplier=pkg.get("supplier"),
        )
        components.append(component)
        spdx_id_to_purl[spdx_id] = purl or f"{name}@{version}"

    # Parse relationships
    for rel in sbom_data.get("relationships", []):
        rel_type = rel.get("relationshipType", "")
        if rel_type == "DEPENDS_ON":
            parent_id = rel.get("spdxElementId", "")
            child_id = rel.get("relatedSpdxElement", "")
            parent_ref = spdx_id_to_purl.get(parent_id, parent_id)
            child_ref = spdx_id_to_purl.get(child_id, child_id)
            if parent_ref not in dependencies:
                dependencies[parent_ref] = []
            dependencies[parent_ref].append(child_ref)

    return components, dependencies


def parse_sbom(sbom_path):
    """Load and parse an SBOM file, auto-detecting the format."""
    if not os.path.isfile(sbom_path):
        raise FileNotFoundError(f"SBOM file not found: {sbom_path}")

    with open(sbom_path, "r", encoding="utf-8") as f:
        sbom_data = json.load(f)

    fmt = detect_sbom_format(sbom_data)
    print(f"\n[INFO] Parsing SBOM: {sbom_path}")

    if fmt == "cyclonedx":
        return parse_cyclonedx(sbom_data), fmt
    elif fmt == "spdx":
        return parse_spdx(sbom_data), fmt
    else:
        raise ValueError(
            f"Unrecognized SBOM format. Expected CycloneDX or SPDX JSON. "
            f"Keys found: {list(sbom_data.keys())[:10]}"
        )


def query_nvd_by_cpe(cpe_name, api_key=None):
    """Query NVD 2.0 API for CVEs matching a CPE name."""
    if not HAS_REQUESTS:
        return []

    params = {"cpeName": cpe_name, "resultsPerPage": NVD_RESULTS_PER_PAGE}
    headers = {}
    if api_key:
        headers["apiKey"] = api_key

    try:
        resp = requests.get(NVD_CVE_API, params=params, headers=headers, timeout=30)
        if resp.status_code == 403:
            print(f"    [WARN] NVD API rate limited. Waiting...", file=sys.stderr)
            time.sleep(NVD_RATE_LIMIT_NO_KEY * 2)
            resp = requests.get(NVD_CVE_API, params=params, headers=headers, timeout=30)
        resp.raise_for_status()
        return resp.json().get("vulnerabilities", [])
    except requests.RequestException as e:
        print(f"    [WARN] NVD API error for {cpe_name}: {e}", file=sys.stderr)
        return []


def query_nvd_by_keyword(keyword, api_key=None):
    """Query NVD 2.0 API for CVEs matching a keyword search."""
    if not HAS_REQUESTS:
        return []

    params = {"keywordSearch": keyword, "resultsPerPage": NVD_RESULTS_PER_PAGE}
    headers = {}
    if api_key:
        headers["apiKey"] = api_key

    try:
        resp = requests.get(NVD_CVE_API, params=params, headers=headers, timeout=30)
        resp.raise_for_status()
        return resp.json().get("vulnerabilities", [])
    except requests.RequestException as e:
        print(f"    [WARN] NVD keyword search error for '{keyword}': {e}", file=sys.stderr)
        return []


def extract_cve_info(vuln_entry):
    """Extract structured CVE information from an NVD API response entry."""
    cve_data = vuln_entry.get("cve", {})
    cve_id = cve_data.get("id", "UNKNOWN")

    # Extract CVSS score (prefer v3.1, fallback to v3.0, then v2.0)
    cvss_score = 0.0
    cvss_version = "none"
    metrics = cve_data.get("metrics", {})

    for version_key in ["cvssMetricV31", "cvssMetricV30", "cvssMetricV2"]:
        metric_list = metrics.get(version_key, [])
        if metric_list:
            cvss_data = metric_list[0].get("cvssData", {})
            cvss_score = cvss_data.get("baseScore", 0.0)
            cvss_version = cvss_data.get("version", version_key)
            break

    # Extract description
    descriptions = cve_data.get("descriptions", [])
    description = ""
    for desc in descriptions:
        if desc.get("lang") == "en":
            description = desc.get("value", "")
            break

    # Determine severity
    severity = "LOW"
    for level, threshold in sorted(SEVERITY_THRESHOLDS.items(),
                                    key=lambda x: x[1], reverse=True):
        if cvss_score >= threshold:
            severity = level
            break

    # Check for known exploited (CISA KEV indicator)
    is_kev = False
    for ref in cve_data.get("references", []):
        if "cisa.gov" in ref.get("url", "").lower():
            is_kev = True
            break

    return {
        "cve_id": cve_id,
        "cvss_score": cvss_score,
        "cvss_version": cvss_version,
        "severity": severity,
        "description": description[:300],
        "is_kev": is_kev,
        "published": cve_data.get("published", ""),
    }


def correlate_cves(components, api_key=None, skip_nvd=False):
    """Correlate all SBOM components against NVD for known vulnerabilities."""
    rate_limit = NVD_RATE_LIMIT_WITH_KEY if api_key else NVD_RATE_LIMIT_NO_KEY
    total = len(components)
    vuln_count = 0

    print(f"\n[INFO] Correlating {total} components against NVD CVE database...")
    if not api_key:
        print(f"  [NOTE] No NVD API key. Rate limited to 1 request per {rate_limit}s.")
        print(f"  Get a free key at: https://nvd.nist.gov/developers/request-an-api-key")

    if skip_nvd:
        print(f"  [NOTE] NVD queries skipped (--skip-nvd flag). Using offline mode.")
        return components

    for idx, comp in enumerate(components):
        print(f"  [{idx+1}/{total}] {comp.name}@{comp.version}...", end="", flush=True)

        vulns = []
        # Try CPE-based search first (most precise)
        if comp.cpe:
            vulns = query_nvd_by_cpe(comp.cpe, api_key)

        # Fallback to keyword search if no CPE or no results
        if not vulns:
            keyword = f"{comp.name} {comp.version}"
            vulns = query_nvd_by_keyword(keyword, api_key)

        # Process results
        for v in vulns:
            cve_info = extract_cve_info(v)
            if cve_info["cvss_score"] > 0:
                comp.cves.append(cve_info)
                if cve_info["cvss_score"] > comp.max_cvss:
                    comp.max_cvss = cve_info["cvss_score"]

        # Assign risk level
        if comp.max_cvss >= SEVERITY_THRESHOLDS["CRITICAL"]:
            comp.risk_level = "CRITICAL"
        elif comp.max_cvss >= SEVERITY_THRESHOLDS["HIGH"]:
            comp.risk_level = "HIGH"
        elif comp.max_cvss >= SEVERITY_THRESHOLDS["MEDIUM"]:
            comp.risk_level = "MEDIUM"
        elif comp.max_cvss > 0:
            comp.risk_level = "LOW"

        cve_count = len(comp.cves)
        vuln_count += cve_count
        status = f" {cve_count} CVEs (max CVSS: {comp.max_cvss})" if cve_count else " clean"
        print(status)

        # Rate limiting
        if idx < total - 1:
            time.sleep(rate_limit)

    print(f"\n[INFO] Correlation complete. Found {vuln_count} total CVEs across all components.")
    return components


def build_dependency_graph(components, dependencies):
    """Build a directed dependency graph using networkx."""
    if not HAS_NETWORKX:
        print("[WARN] networkx not installed. Dependency graph analysis skipped.", file=sys.stderr)
        return None

    G = nx.DiGraph()

    # Build lookup for components by purl or name@version
    comp_lookup = {}
    for comp in components:
        ref = comp.purl or f"{comp.name}@{comp.version}"
        G.add_node(ref, name=comp.name, version=comp.version,
                   max_cvss=comp.max_cvss, risk_level=comp.risk_level,
                   cve_count=len(comp.cves))
        comp_lookup[ref] = comp

    # Add edges from dependency relationships
    for parent_ref, children in dependencies.items():
        if parent_ref not in G:
            G.add_node(parent_ref)
        for child_ref in children:
            if child_ref not in G:
                G.add_node(child_ref)
            G.add_edge(parent_ref, child_ref)

    return G


def analyze_dependency_graph(G):
    """Analyze the dependency graph for risk metrics."""
    if G is None or len(G.nodes) == 0:
        return {}

    analysis = {
        "total_nodes": G.number_of_nodes(),
        "total_edges": G.number_of_edges(),
        "is_dag": nx.is_directed_acyclic_graph(G),
    }

    # Find most depended-on components (highest in-degree)
    in_degrees = sorted(G.in_degree(), key=lambda x: x[1], reverse=True)
    analysis["most_depended_on"] = [
        {"ref": node, "dependents": deg, **G.nodes[node]}
        for node, deg in in_degrees[:10] if deg > 0
    ]

    # Find root nodes (no incoming edges - likely the application itself)
    roots = [n for n, d in G.in_degree() if d == 0]
    analysis["root_components"] = len(roots)

    # Find leaf nodes (no outgoing edges - no dependencies)
    leaves = [n for n, d in G.out_degree() if d == 0]
    analysis["leaf_components"] = len(leaves)

    # Calculate longest dependency chain
    if analysis["is_dag"] and len(G.nodes) > 0:
        try:
            longest_path = nx.dag_longest_path(G)
            analysis["deepest_chain_length"] = len(longest_path)
            analysis["deepest_chain"] = longest_path
        except nx.NetworkXError:
            analysis["deepest_chain_length"] = 0

    # Identify vulnerable components with high in-degree (blast radius)
    high_risk_hubs = []
    for node, deg in in_degrees:
        node_data = G.nodes.get(node, {})
        if node_data.get("max_cvss", 0) >= SEVERITY_THRESHOLDS["HIGH"] and deg > 0:
            high_risk_hubs.append({
                "ref": node,
                "dependents": deg,
                "max_cvss": node_data.get("max_cvss", 0),
                "risk_level": node_data.get("risk_level", "UNKNOWN"),
            })
    analysis["high_risk_hubs"] = high_risk_hubs

    # Betweenness centrality for bottleneck identification
    if len(G.nodes) > 1:
        centrality = nx.betweenness_centrality(G)
        top_central = sorted(centrality.items(), key=lambda x: x[1], reverse=True)[:5]
        analysis["bottleneck_components"] = [
            {"ref": node, "centrality": round(cent, 4)} for node, cent in top_central if cent > 0
        ]

    return analysis


def check_license_compliance(components):
    """Check for potentially problematic licenses in SBOM components."""
    copyleft_licenses = {
        "GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later",
        "GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later",
        "AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later",
        "LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later",
        "LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later",
        "MPL-2.0", "EUPL-1.2", "CPAL-1.0", "OSL-3.0",
    }

    findings = {
        "copyleft_components": [],
        "unknown_license_components": [],
        "license_distribution": defaultdict(int),
    }

    for comp in components:
        if not comp.licenses or comp.licenses == ["NOASSERTION"]:
            findings["unknown_license_components"].append(
                {"name": comp.name, "version": comp.version}
            )
        for lic in comp.licenses:
            findings["license_distribution"][lic] += 1
            if lic in copyleft_licenses:
                findings["copyleft_components"].append({
                    "name": comp.name,
                    "version": comp.version,
                    "license": lic,
                })

    findings["license_distribution"] = dict(findings["license_distribution"])
    return findings


def generate_report(components, dependencies, graph_analysis, license_info,
                    sbom_path, sbom_format, output_path=None):
    """Generate a comprehensive vulnerability analysis report."""
    # Aggregate statistics
    vuln_components = [c for c in components if c.cves]
    total_cves = sum(len(c.cves) for c in components)
    severity_counts = defaultdict(lambda: {"components": 0, "cves": 0})

    for comp in components:
        if comp.risk_level != "NONE":
            severity_counts[comp.risk_level]["components"] += 1
            severity_counts[comp.risk_level]["cves"] += len(comp.cves)

    report_lines = []
    report_lines.append("=" * 60)
    report_lines.append("SBOM VULNERABILITY ANALYSIS REPORT")
    report_lines.append("=" * 60)
    report_lines.append(f"SBOM File:          {sbom_path}")
    report_lines.append(f"Format:             {sbom_format}")
    report_lines.append(f"Analysis Date:      {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}")
    report_lines.append(f"Total Components:   {len(components)}")

    direct_deps = len(dependencies)
    transitive = len(components) - direct_deps if direct_deps < len(components) else 0
    report_lines.append(f"Dependencies:       {len(dependencies)} direct, ~{transitive} transitive")
    report_lines.append("")

    report_lines.append("VULNERABILITY SUMMARY")
    report_lines.append("-" * 40)
    for level in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
        counts = severity_counts.get(level, {"components": 0, "cves": 0})
        report_lines.append(
            f"  {level:10s}: {counts['components']:3d} components / {counts['cves']:3d} CVEs"
        )
    report_lines.append(f"  {'TOTAL':10s}: {len(vuln_components):3d} components / {total_cves:3d} CVEs")
    report_lines.append("")

    # Critical and high findings detail
    critical_high = sorted(
        [c for c in components if c.risk_level in ("CRITICAL", "HIGH")],
        key=lambda c: c.max_cvss, reverse=True
    )

    if critical_high:
        report_lines.append("CRITICAL & HIGH FINDINGS")
        report_lines.append("-" * 40)
        for i, comp in enumerate(critical_high[:20], 1):
            report_lines.append(f"\n  {i}. {comp.name}@{comp.version} [{comp.risk_level}]")
            for cve in sorted(comp.cves, key=lambda c: c["cvss_score"], reverse=True)[:5]:
                kev_flag = " [CISA KEV]" if cve.get("is_kev") else ""
                report_lines.append(
                    f"     {cve['cve_id']} (CVSS {cve['cvss_score']:.1f}){kev_flag}"
                )
                if cve["description"]:
                    desc_short = cve["description"][:120]
                    report_lines.append(f"       {desc_short}...")

    # Dependency graph analysis
    if graph_analysis:
        report_lines.append("")
        report_lines.append("DEPENDENCY GRAPH ANALYSIS")
        report_lines.append("-" * 40)
        report_lines.append(f"  Nodes: {graph_analysis.get('total_nodes', 0)}")
        report_lines.append(f"  Edges: {graph_analysis.get('total_edges', 0)}")
        report_lines.append(f"  DAG: {graph_analysis.get('is_dag', 'N/A')}")
        chain_len = graph_analysis.get("deepest_chain_length", 0)
        if chain_len:
            report_lines.append(f"  Deepest dependency chain: {chain_len} levels")

        hubs = graph_analysis.get("high_risk_hubs", [])
        if hubs:
            report_lines.append(f"\n  HIGH-RISK HUBS (vulnerable + many dependents):")
            for hub in hubs[:5]:
                report_lines.append(
                    f"    {hub['ref']}: {hub['dependents']} dependents, "
                    f"CVSS {hub['max_cvss']:.1f} [{hub['risk_level']}]"
                )

    # License compliance
    if license_info:
        report_lines.append("")
        report_lines.append("LICENSE COMPLIANCE")
        report_lines.append("-" * 40)
        copyleft = license_info.get("copyleft_components", [])
        unknown = license_info.get("unknown_license_components", [])
        report_lines.append(f"  Copyleft licenses found: {len(copyleft)}")
        for cl in copyleft[:10]:
            report_lines.append(f"    {cl['name']}@{cl['version']}: {cl['license']}")
        report_lines.append(f"  Unknown/missing licenses: {len(unknown)}")

    report_text = "\n".join(report_lines)
    print(f"\n{report_text}")

    # Build JSON result
    result = {
        "sbom_file": sbom_path,
        "sbom_format": sbom_format,
        "analysis_timestamp": datetime.utcnow().isoformat(),
        "summary": {
            "total_components": len(components),
            "vulnerable_components": len(vuln_components),
            "total_cves": total_cves,
            "severity_counts": dict(severity_counts),
        },
        "components": [c.to_dict() for c in components],
        "dependency_graph": graph_analysis or {},
        "license_compliance": license_info or {},
    }

    if output_path:
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(result, f, indent=2, default=str)
        print(f"\n[OK] Full report saved to {output_path}")

    return result


def analyze_sbom(sbom_path, api_key=None, output_path=None, skip_nvd=False):
    """Full SBOM analysis pipeline: parse, correlate CVEs, graph analysis, report."""
    (components, dependencies), sbom_format = parse_sbom(sbom_path)
    print(f"  Components: {len(components)}")
    print(f"  Dependency entries: {len(dependencies)}")

    # Correlate with NVD
    components = correlate_cves(components, api_key=api_key, skip_nvd=skip_nvd)

    # Build and analyze dependency graph
    G = build_dependency_graph(components, dependencies)
    graph_analysis = analyze_dependency_graph(G)

    # License compliance check
    license_info = check_license_compliance(components)

    # Generate report
    result = generate_report(
        components, dependencies, graph_analysis, license_info,
        sbom_path, sbom_format, output_path
    )

    return result


def compare_sboms(sbom_path_old, sbom_path_new, api_key=None):
    """Compare two SBOMs to identify added, removed, and changed components."""
    (comps_old, _), _ = parse_sbom(sbom_path_old)
    (comps_new, _), _ = parse_sbom(sbom_path_new)

    old_set = {f"{c.name}@{c.version}" for c in comps_old}
    new_set = {f"{c.name}@{c.version}" for c in comps_new}
    old_names = {c.name for c in comps_old}
    new_names = {c.name for c in comps_new}

    added = new_set - old_set
    removed = old_set - new_set

    # Version changes: same name, different version
    old_versions = {c.name: c.version for c in comps_old}
    new_versions = {c.name: c.version for c in comps_new}
    version_changes = []
    for name in old_names & new_names:
        if old_versions[name] != new_versions[name]:
            version_changes.append({
                "name": name,
                "old_version": old_versions[name],
                "new_version": new_versions[name],
            })

    print(f"\n{'='*60}")
    print(f"SBOM DIFF REPORT")
    print(f"{'='*60}")
    print(f"Old: {sbom_path_old} ({len(comps_old)} components)")
    print(f"New: {sbom_path_new} ({len(comps_new)} components)")
    print(f"\nAdded:   {len(added)} components")
    for a in sorted(added):
        print(f"  + {a}")
    print(f"\nRemoved: {len(removed)} components")
    for r in sorted(removed):
        print(f"  - {r}")
    print(f"\nVersion Changes: {len(version_changes)}")
    for vc in version_changes:
        print(f"  ~ {vc['name']}: {vc['old_version']} -> {vc['new_version']}")

    return {"added": sorted(added), "removed": sorted(removed),
            "version_changes": version_changes}


def main():
    parser = argparse.ArgumentParser(
        description="SBOM Supply Chain Vulnerability Analysis Agent"
    )
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # Analyze SBOM
    analyze_parser = subparsers.add_parser("analyze", help="Analyze an SBOM for vulnerabilities")
    analyze_parser.add_argument("sbom_path", help="Path to SBOM file (CycloneDX or SPDX JSON)")
    analyze_parser.add_argument("--api-key", help="NVD API key for higher rate limits")
    analyze_parser.add_argument("--output", "-o", help="Save full report to JSON file")
    analyze_parser.add_argument("--skip-nvd", action="store_true",
                                help="Skip NVD API queries (offline mode)")

    # Compare two SBOMs
    diff_parser = subparsers.add_parser("diff", help="Compare two SBOMs for changes")
    diff_parser.add_argument("old_sbom", help="Path to old/baseline SBOM")
    diff_parser.add_argument("new_sbom", help="Path to new/current SBOM")
    diff_parser.add_argument("--api-key", help="NVD API key")

    # Parse only (no NVD queries)
    parse_parser = subparsers.add_parser("parse", help="Parse SBOM and list components")
    parse_parser.add_argument("sbom_path", help="Path to SBOM file")
    parse_parser.add_argument("--output", "-o", help="Save component list to JSON")

    # License check
    license_parser = subparsers.add_parser("licenses", help="Check license compliance")
    license_parser.add_argument("sbom_path", help="Path to SBOM file")

    args = parser.parse_args()

    if args.command == "analyze":
        if not HAS_REQUESTS:
            print("[ERROR] requests library required. Install: pip install requests",
                  file=sys.stderr)
            sys.exit(1)
        api_key = args.api_key or os.environ.get("NVD_API_KEY")
        analyze_sbom(args.sbom_path, api_key=api_key, output_path=args.output,
                     skip_nvd=args.skip_nvd)

    elif args.command == "diff":
        compare_sboms(args.old_sbom, args.new_sbom, api_key=args.api_key)

    elif args.command == "parse":
        (components, dependencies), fmt = parse_sbom(args.sbom_path)
        print(f"\n  Total components: {len(components)}")
        for comp in components:
            print(f"    {comp.name}@{comp.version} [{comp.component_type}] "
                  f"licenses={comp.licenses}")
        if args.output:
            data = {"format": fmt, "component_count": len(components),
                    "components": [c.to_dict() for c in components]}
            with open(args.output, "w") as f:
                json.dump(data, f, indent=2)
            print(f"\n[OK] Component list saved to {args.output}")

    elif args.command == "licenses":
        (components, _), _ = parse_sbom(args.sbom_path)
        info = check_license_compliance(components)
        print(f"\nLicense Distribution:")
        for lic, count in sorted(info["license_distribution"].items(),
                                  key=lambda x: x[1], reverse=True):
            print(f"  {lic}: {count}")
        if info["copyleft_components"]:
            print(f"\nCopyleft Components ({len(info['copyleft_components'])}):")
            for cl in info["copyleft_components"]:
                print(f"  {cl['name']}@{cl['version']}: {cl['license']}")
        if info["unknown_license_components"]:
            print(f"\nUnknown License ({len(info['unknown_license_components'])}):")
            for ul in info["unknown_license_components"]:
                print(f"  {ul['name']}@{ul['version']}")

    else:
        parser.print_help()


if __name__ == "__main__":
    main()
