// wizard_components.jsx — "Create component" workflow.
// Five steps: Identity → SBOM → Refine (advanced editor) → Match → Review.
// Renders in two layout directions selected by the `layout` prop:
//   • "rail"    — left vertical step rail, focused single-panel content
//   • "stepper" — top horizontal stepper + live build-up summary sidebar
// Both share the same step bodies; only the surrounding chrome differs.

const WIZ_STEPS = [
  { id: "identity", label: "Identity",  icon: "package", sub: "Name & type" },
  { id: "sbom",     label: "SBOM/HBOM", icon: "doc",     sub: "Upload CycloneDX / SPDX"},
  { id: "refine",   label: "Refine",    icon: "pencil",  sub: "Edit components", optional: true },
  { id: "match",    label: "Match",     icon: "link",    sub: "Supplier & OEM" },
  { id: "review",   label: "Review",    icon: "sparkle", sub: "Matched CVEs" },
];

function catOf(id) { return window.VC.COMPONENT_CATEGORIES.find(c => c.id === id); }

/* ====================================================== step: Identity */
function IdentityStep({ name, setName, category, setCategory }) {
  return (
    <div className="wizform">
      <div className="wizform-head">
        <h2>Define the component</h2>
        <p>Give the asset a name your team will recognise, then classify what kind of component it is.</p>
      </div>
      <div className="field">
        <label className="field-lab">Component name</label>
        <div className="field-wrap">
          <Icon name="package" size={16} className="field-ic" />
          <input value={name} onChange={e => setName(e.target.value)} autoFocus
            placeholder="e.g. Telematics Control Unit — Connectivity FW" />
        </div>
      </div>
      <div className="field">
        <label className="field-lab">Category / type</label>
        <div className="catgrid">
          {window.VC.COMPONENT_CATEGORIES.map(c => (
            <button key={c.id} type="button" className={"catcard" + (category === c.id ? " is-on" : "")}
              onClick={() => setCategory(c.id)}>
              <span className="catcard-ic"><Icon name={c.icon} size={18} /></span>
              <span className="catcard-lab">{c.label}</span>
              <span className="catcard-hint">{c.hint}</span>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ===================================================== CSV row parsing */
// Minimal RFC-4180-ish CSV parser: handles quoted fields, escaped quotes ("")
// and embedded commas / newlines. Returns an array of string-cell rows.
function parseCsvRows(text) {
  const rows = [];
  let row = [], field = "", inQ = false;
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (inQ) {
      if (ch === '"') {
        if (text[i + 1] === '"') { field += '"'; i++; }
        else inQ = false;
      } else field += ch;
      continue;
    }
    if (ch === '"') inQ = true;
    else if (ch === ',') { row.push(field); field = ""; }
    else if (ch === '\r') { /* ignore */ }
    else if (ch === '\n') { row.push(field); rows.push(row); row = []; field = ""; }
    else field += ch;
  }
  if (field.length || row.length) { row.push(field); rows.push(row); }
  return rows.filter(r => r.some(c => c.trim() !== ""));
}

/* ========================================================= SPDX parsing */
// SPDX 2.2 / 2.3 JSON support. We accept SPDX on upload but convert its
// `packages[]` into exactly the same internal, CycloneDX-shaped lib records the
// rest of the pipeline expects ({name, version, type, purl, supplier}). Nothing
// downstream — matching, the SBOM editor, preview, save — needs to know or care
// that the upload was SPDX; CycloneDX stays the internal representation.
const SPDX_PURPOSE_TO_TYPE = {
  APPLICATION: "application",
  FRAMEWORK: "framework",
  LIBRARY: "library",
  "OPERATING-SYSTEM": "operating-system",
  DEVICE: "device",
  FIRMWARE: "firmware",
  CONTAINER: "application",
  FILE: "library",
  SOURCE: "library",
  ARCHIVE: "library",
  INSTALL: "application",
  OTHER: "library",
};

// Does this parsed JSON object look like an SPDX document (vs CycloneDX)?
function looksLikeSpdx(json) {
  if (!json || typeof json !== "object") return false;
  if (typeof json.spdxVersion === "string" && /spdx/i.test(json.spdxVersion)) return true;
  // Fallback for tools that omit spdxVersion: a packages[] array with the SPDX
  // document marker and no CycloneDX components[] array.
  return Array.isArray(json.packages) && !Array.isArray(json.components) &&
    (json.SPDXID === "SPDXRef-DOCUMENT" || !!json.spdxVersion || !!json.documentNamespace);
}

// SPDX actor field: "Organization: wolfSSL Inc." / "Person: Jane" / "Tool: syft-1.0"
// / "NOASSERTION" -> the bare display name (or "" when unset/NOASSERTION).
function spdxActor(s) {
  if (!s || typeof s !== "string") return "";
  const v = s.trim();
  if (!v || v === "NOASSERTION" || v === "NONE") return "";
  const m = v.match(/^(?:Organization|Person|Tool)\s*:\s*(.+)$/i);
  return (m ? m[1] : v).trim();
}
// SPDX uses the sentinels NOASSERTION / NONE where a value is unknown — treat as blank.
function spdxClean(s) {
  const v = (s == null ? "" : String(s)).trim();
  return (!v || v === "NOASSERTION" || v === "NONE") ? "" : v;
}

function parseSpdxJson(json, filename, raw) {
  const pkgs = Array.isArray(json.packages) ? json.packages : [];

  // The document's primary subject is the package it DESCRIBES — pull it out so it
  // becomes the "primary component" (mirroring CycloneDX's metadata.component)
  // instead of being matched as one of its own dependencies.
  const docId = json.SPDXID || "SPDXRef-DOCUMENT";
  const rels = Array.isArray(json.relationships) ? json.relationships : [];
  let primaryId = null;
  for (const r of rels) {
    if (!r) continue;
    const t = String(r.relationshipType || "").toUpperCase();
    if (t === "DESCRIBES" && r.spdxElementId === docId) { primaryId = r.relatedSpdxElement; break; }
    if (t === "DESCRIBED_BY" && r.relatedSpdxElement === docId) { primaryId = r.spdxElementId; break; }
  }
  const primaryPkg = primaryId ? pkgs.find(p => p && p.SPDXID === primaryId) : null;

  // purl lives in externalRefs (referenceType "purl", category PACKAGE-MANAGER).
  const purlOf = (p) => {
    const refs = Array.isArray(p.externalRefs) ? p.externalRefs : [];
    const ref = refs.find(x => x && /purl/i.test(String(x.referenceType || "")));
    return ref ? String(ref.referenceLocator || "").trim() : "";
  };
  const toLib = (p) => ({
    name: spdxClean(p.name),
    version: spdxClean(p.versionInfo),
    type: SPDX_PURPOSE_TO_TYPE[String(p.primaryPackagePurpose || "").toUpperCase()] || "library",
    purl: purlOf(p),
    supplier: spdxActor(p.supplier) || spdxActor(p.originator) || "—",
  });

  const libs = pkgs.filter(p => p && p !== primaryPkg).map(toLib).filter(l => l.name);

  const ci = json.creationInfo || {};
  const creators = Array.isArray(ci.creators) ? ci.creators.map(String) : [];
  const toolCreator = creators.find(c => /^tool\s*:/i.test(c));
  const tool = toolCreator ? spdxActor(toolCreator) : (creators.length ? spdxActor(creators[0]) : "unknown tool");

  const primaryComponent = primaryPkg ? {
    name: spdxClean(primaryPkg.name) || json.name || filename || "Uploaded SBOM",
    version: spdxClean(primaryPkg.versionInfo),
    supplier: spdxActor(primaryPkg.supplier) || spdxActor(primaryPkg.originator) || "—",
    type: SPDX_PURPOSE_TO_TYPE[String(primaryPkg.primaryPackagePurpose || "").toUpperCase()] || "application",
  } : {
    name: json.name || filename || "Uploaded SBOM", version: "", supplier: "—", type: "application",
  };

  const meta = {
    format: "SPDX",
    specVersion: String(json.spdxVersion || "SPDX").replace(/^SPDX-?/i, "") || "2.x",
    serialNumber: json.documentNamespace || json.SPDXID || "(no namespace)",
    metadata: {
      timestamp: ci.created || new Date().toISOString(),
      tool: tool || "unknown tool",
      component: primaryComponent,
    },
  };
  return { meta, libs, raw };
}

/* ===================================================== CycloneDX parsing */
// Parse a CycloneDX SBOM (JSON or best-effort XML), an SPDX JSON document, or a
// flat CSV into a common {meta, libs, raw}. `meta.format` records which one it was.
function parseSbom(text, filename) {
  text = text || "";
  const trimmed = text.trim();
  const looksXml = trimmed.startsWith("<") || /\.xml$/i.test(filename || "");
  const looksCsv = /\.csv$/i.test(filename || "") ||
    (!looksXml && !trimmed.startsWith("{") && !trimmed.startsWith("[") &&
      /,/.test((trimmed.split(/\r?\n/)[0]) || ""));

  const supplierOf = (c) => {
    const s = c.supplier;
    if (s && typeof s === "object") return s.name || "—";
    return s || c.publisher || c.author || "—";
  };

  // CSV — a flat component list with a header row (name,version,type,purl,supplier).
  if (looksCsv) {
    const rows = parseCsvRows(trimmed);
    if (rows.length < 2) throw new Error("CSV needs a header row plus at least one component row.");
    // Strip ALL punctuation so headers like "Package URL (purl)" and "Supplier / Author"
    // still resolve to their columns.
    const norm = (s) => String(s || "").trim().toLowerCase().replace(/[^a-z0-9]/g, "");
    const NAME_KEYS = ["name", "component", "componentname", "library", "package"];
    // Real-world BoM exports often carry several title/metadata preamble rows before the
    // real column header, so scan for the first row that actually names a component
    // column instead of assuming row 0.
    let headerIdx = rows.findIndex(r => r.map(norm).some(c => NAME_KEYS.includes(c)));
    if (headerIdx === -1) headerIdx = 0;
    const header = rows[headerIdx].map(norm);
    const idx = (...names) => {
      for (const n of names) { const i = header.indexOf(n); if (i !== -1) return i; }                 // exact match wins
      for (const n of names) { const i = header.findIndex(h => h.includes(n)); if (i !== -1) return i; } // then "contains"
      return -1;
    };
    const iName = idx("name", "component", "library", "package");
    const iVer = idx("version", "ver");
    const iType = idx("type", "componenttype");
    const iPurl = idx("purl", "packageurl");
    const iSup = idx("supplier", "vendor", "publisher", "author", "manufacturer");
    if (iName === -1)
      throw new Error("CSV must include a 'name'/'component' column. Found: " + header.filter(Boolean).join(", "));
    const cell = (r, i) => (i !== -1 && r[i] != null ? String(r[i]).trim() : "");
    const libs = rows.slice(headerIdx + 1).map(r => ({
      name: cell(r, iName), version: cell(r, iVer),
      type: cell(r, iType) || "library", purl: cell(r, iPurl),
      supplier: cell(r, iSup) || "—",
    })).filter(l => l.name);
    const meta = {
      format: "CSV",
      specVersion: "CSV",
      serialNumber: "(no serial)",
      metadata: {
        timestamp: new Date().toISOString(),
        tool: "CSV import",
        component: { name: filename || "Uploaded CSV", version: "", supplier: "—", type: "application" },
      },
    };
    return { meta, libs, raw: trimmed };
  }

  if (!looksXml) {
    let json;
    try { json = JSON.parse(trimmed); }
    catch (e) { throw new Error("Not valid JSON — upload a CycloneDX or SPDX .json (or CycloneDX .xml) file."); }

    // SPDX (JSON) — convert to the internal CycloneDX-shaped records and return.
    if (looksLikeSpdx(json)) {
      const parsed = parseSpdxJson(json, filename, trimmed);
      if (!parsed.libs.length) throw new Error("No packages found in this SPDX document.");
      return parsed;
    }

    const comps = Array.isArray(json.components) ? json.components
      : Array.isArray(json) ? json : [];
    const libs = comps.map(c => ({
      name: c.name || "", version: (c.version != null ? String(c.version) : ""),
      type: c.type || "library", purl: c.purl || "", supplier: supplierOf(c),
    })).filter(l => l.name);
    const md = json.metadata || {};
    const tool = Array.isArray(md.tools) ? (md.tools[0] && (md.tools[0].name || md.tools[0].vendor)) : (md.tools && md.tools.components && md.tools.components[0] && md.tools.components[0].name);
    const meta = {
      format: "CycloneDX",
      specVersion: json.specVersion || "1.x",
      serialNumber: json.serialNumber || "(no serial)",
      metadata: {
        timestamp: md.timestamp || new Date().toISOString(),
        tool: tool || "unknown tool",
        component: md.component
          ? { ...md.component, supplier: supplierOf(md.component) }
          : { name: filename || "Uploaded SBOM", version: "", supplier: "—", type: "application" },
      },
    };
    return { meta, libs, raw: trimmed };
  }

  // XML (best-effort)
  const doc = new DOMParser().parseFromString(trimmed, "application/xml");
  if (doc.querySelector("parsererror")) throw new Error("Invalid XML — could not parse the CycloneDX document.");
  const text1 = (el, sel) => { const n = el.querySelector(sel); return n ? n.textContent.trim() : ""; };
  const compEls = [...doc.querySelectorAll("components > component")];
  const libs = compEls.map(el => ({
    name: text1(el, "name"), version: text1(el, "version"),
    type: el.getAttribute("type") || "library", purl: text1(el, "purl"),
    supplier: text1(el, "supplier > name") || "—",
  })).filter(l => l.name);
  const root = doc.documentElement;
  const meta = {
    format: "CycloneDX",
    specVersion: (root.getAttribute("xmlns") || "").match(/\/(\d+\.\d+)$/)?.[1] || "1.x",
    serialNumber: root.getAttribute("serialNumber") || "(no serial)",
    metadata: {
      timestamp: text1(doc, "metadata > timestamp") || new Date().toISOString(),
      tool: text1(doc, "metadata tools > tool > name") || "unknown tool",
      component: {
        name: text1(doc, "metadata > component > name") || filename || "Uploaded SBOM",
        version: text1(doc, "metadata > component > version"),
        supplier: text1(doc, "metadata > component > supplier > name") || "—",
      },
    },
  };
  return { meta, libs, raw: trimmed };
}

/* ========================================================== step: SBOM */
function UploadStep({ sbom, libs, matches, matchLoading, onParsed, onClear }) {
  const [parsing, setParsing] = useState(false);
  const [drag, setDrag] = useState(false);
  const [raw, setRaw] = useState(false);
  const [err, setErr] = useState("");
  const inputRef = useRef(null);

  function handleFile(file) {
    if (!file) return;
    setErr(""); setParsing(true);
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const parsed = parseSbom(String(reader.result || ""), file.name);
        if (!parsed.libs.length) throw new Error("No components found in this SBOM.");
        setParsing(false);
        onParsed({ ...parsed, filename: file.name });
      } catch (e) { setParsing(false); setErr(e.message || "Could not parse this file."); }
    };
    reader.onerror = () => { setParsing(false); setErr("Could not read the file."); };
    reader.readAsText(file);
  }

  if (parsing) {
    return (
      <div className="wizform">
        <div className="drop is-parsing">
          <span className="btn-spin drop-spin" />
          <strong>Parsing SBOM document…</strong>
          <span className="drop-sub">Validating schema · extracting components · normalising PURLs</span>
        </div>
      </div>
    );
  }

  if (!sbom) {
    return (
      <div className="wizform">
        <div className="wizform-head">
          <h2>Upload a software bill of materials</h2>
          <p>VulnerabilityCore reads the component inventory from a CycloneDX or SPDX SBOM and matches each library against the live CVE ledger.</p>
        </div>
        <input ref={inputRef} type="file" accept=".json,.xml,.cdx,.spdx,.spdx.json,.csv,application/json,text/xml,application/xml,text/csv"
          style={{ display: "none" }} onChange={e => { handleFile(e.target.files && e.target.files[0]); e.target.value = ""; }} />
        <div className={"drop" + (drag ? " is-drag" : "")}
          onDragOver={e => { e.preventDefault(); setDrag(true); }}
          onDragLeave={() => setDrag(false)}
          onDrop={e => { e.preventDefault(); setDrag(false); handleFile(e.dataTransfer.files && e.dataTransfer.files[0]); }}
          onClick={() => inputRef.current && inputRef.current.click()} style={{ cursor: "pointer" }}>
          <span className="drop-ic"><Icon name="upload" size={26} /></span>
          <strong>Drag a CycloneDX, SPDX or CSV file here, or click to browse</strong>
          <span className="drop-sub">Accepts <b>.cdx.json</b>, <b>.cdx.xml</b> (CycloneDX 1.4 – 1.5) · <b>.spdx.json</b> (SPDX 2.2 – 2.3) · or a <b>.csv</b> component list</span>
          <div className="drop-actions">
            <button className="btn btn-accent" onClick={e => { e.stopPropagation(); inputRef.current && inputRef.current.click(); }}><Icon name="upload" size={14} />Choose file</button>
          </div>
        </div>
        {err && <div className="login-err" style={{ marginTop: 12 }}><Icon name="bell" size={14} />{err}</div>}
      </div>
    );
  }

  const m = sbom.meta;
  matches = matches || [];
  return (
    <div className="wizform">
      <div className="wizform-head wizform-head-row">
        <div>
          <h2>SBOM parsed</h2>
          <p>{sbom.filename}</p>
        </div>
        <button className="btn btn-sm" onClick={onClear}><Icon name="refresh" size={14} />Replace file</button>
      </div>

      <div className="sbomsum">
        <div className="sbomsum-stat"><b>{libs.length}</b><span>components</span></div>
        <div className="sbomsum-stat"><b className="mono">{m.specVersion}</b><span>{m.format || "CycloneDX"}</span></div>
        <div className="sbomsum-stat"><b className={matches.length ? "is-warn" : ""}>{matchLoading ? "…" : matches.length}</b><span>CVE matches</span></div>
        <div className="sbomsum-stat sbomsum-stat-wide"><b>{m.metadata.component.name}</b><span>primary component{m.metadata.component.version ? " · v" + m.metadata.component.version : ""}</span></div>
      </div>

      <div className="sbom-metaline">
        <span><Icon name="shield" size={13} />Schema valid</span>
        <span className="mono">{m.serialNumber}</span>
        <span>Generated {String(m.metadata.timestamp).slice(0, 10)} · {m.metadata.tool}</span>
      </div>

      <div className="liblist">
        <div className="liblist-head"><span>Inventory preview</span>
          <button className="liblist-raw" onClick={() => setRaw(r => !r)}>{raw ? "Hide source" : "View source"}</button>
        </div>
        {raw ? (
          <pre className="sbom-raw mono">{sbom.raw}</pre>
        ) : (
          libs.slice(0, 7).map((l, i) => {
            const cves = window.VC.libVulnCount(l);
            return (
              <div className="librow" key={i}>
                <span className="librow-name mono">{l.name}<span className="librow-ver"> @{l.version}</span></span>
                <span className="librow-type">{l.type}</span>
                {cves > 0
                  ? <span className="librow-cve"><Icon name="alert" size={12} />{cves}</span>
                  : <span className="librow-ok"><Icon name="check" size={12} /></span>}
              </div>
            );
          })
        )}
        {!raw && libs.length > 7 && <div className="liblist-more">+{libs.length - 7} more components</div>}
      </div>
    </div>
  );
}

/* ======================================================== step: Refine */
function EditStep({ libs, setLibs }) {
  return (
    <div className="wizform">
      <div className="wizform-head wizform-head-row">
        <div>
          <h2>Refine the inventory <span className="wiz-advanced">Advanced</span></h2>
          <p>Correct mis-parsed names, fix versions, or add components the scanner missed. Flagged rows reference a tracked CVE.</p>
        </div>
      </div>
      <SbomEditor value={libs} onChange={setLibs} embedded />
    </div>
  );
}

/* ========================================================= step: Match */
function MatchStep({ supplier, setSupplier, brands, setBrands, suggested }) {
  function toggleBrand(b) { setBrands(brands.includes(b) ? brands.filter(x => x !== b) : [...brands, b]); }

  // Quick-pick options = suppliers that genuinely have components (from /api/meta),
  // plus the supplier parsed from THIS SBOM, deduped. Anything not listed can still
  // be typed in the free-text box below.
  const supplierOpts = [];
  const _seenSup = new Set();
  (window.VC.SUPPLIERS || []).concat(suggested ? [suggested] : []).forEach(s => {
    if (s && s !== "—" && !_seenSup.has(s)) { _seenSup.add(s); supplierOpts.push(s); }
  });

  return (
    <div className="wizform">
      <div className="wizform-head">
        <h2>Match to supplier &amp; OEM</h2>
        <p>Attribute the component to the supplier that builds it and the vehicle OEMs it ships in — this is how it surfaces in the entity pivots.</p>
      </div>

      <div className="field">
        <label className="field-lab">Supplier <span className="field-req">required</span></label>
        <div className="field-wrap">
          <Icon name="package" size={16} className="field-ic" />
          <input value={supplier} onChange={e => setSupplier(e.target.value)}
            placeholder="Type the supplier that builds this component" />
        </div>
        {supplierOpts.length > 0 && (
          <div className="suppgrid" style={{ marginTop: 10 }}>
            {supplierOpts.map(s => (
              <button key={s} type="button" className={"suppcard" + (supplier === s ? " is-on" : "")} onClick={() => setSupplier(s)}>
                <span className="suppcard-av">{s.slice(0, 2).toUpperCase()}</span>
                <span className="suppcard-name">{s}</span>
                {s === suggested && <span className="suppcard-sug"><Icon name="sparkle" size={11} />from SBOM</span>}
              </button>
            ))}
          </div>
        )}
      </div>

      <div className="field">
        <label className="field-lab">OEM <span className="field-opt">{brands.length} selected</span></label>
        <div className="brandchips">
          {window.VC.BRANDS.map(b => (
            <button key={b} type="button" className={"chip chip-brand" + (brands.includes(b) ? " is-on" : "")} onClick={() => toggleBrand(b)}>
              <Icon name={brands.includes(b) ? "check" : "car"} size={13} />{b}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ======================================================== step: Review */
function ReviewStep({ name, category, supplier, brands, libs, matches }) {
  const sev = window.VC.matchSeverity(matches);
  const cat = catOf(category);
  return (
    <div className="wizform">
      <div className={"rev-hero" + (matches.length ? "" : " is-clean")}>
        <div className="rev-hero-l">
          <span className="rev-hero-ic"><Icon name="sparkle" size={20} /></span>
          <div>
            <div className="rev-hero-num">{matches.length}<span> matching {matches.length === 1 ? "vulnerability" : "vulnerabilities"} found</span></div>
            <p className="rev-hero-sub">Correlated {libs.length} SBOM components against the live CVE ledger (server-side).</p>
          </div>
        </div>
        <div className="rev-hero-sev">
          {SEV_ORDER.map(s => sev[s] > 0 && (
            <span key={s} className={"rev-sevchip " + sevClass(s)}><span className="rev-sevchip-dot" />{sev[s]} {s}</span>
          ))}
        </div>
      </div>

      <div className="rev-cols">
        <section className="rev-matches">
          <div className="rev-sec-head"><h3>Matched vulnerabilities</h3><span className="card-sub">{matches.length} via shared libraries</span></div>
          {matches.length === 0 ? (
            <div className="empty"><Icon name="check" size={22} /><p>No tracked CVEs match this SBOM. The component will still be monitored as new CVEs arrive.</p></div>
          ) : (
            <div className="rev-list">
              {matches.map(({ vuln, lib }) => (
                <div className="rev-row" key={vuln.id}>
                  <SeverityTag s={vuln.severity} cvss={vuln.cvss} size="sm" />
                  <div className="rev-row-main">
                    <div className="rev-row-top">
                      <span className="rev-row-id mono">{vuln.id}</span>
                      <span className="rev-row-via">via <b className="mono">{lib.name}@{lib.version}</b></span>
                    </div>
                    <span className="rev-row-desc">{vuln.description}</span>
                  </div>
                </div>
              ))}
            </div>
          )}
        </section>

        <aside className="rev-summary">
          <div className="rev-sec-head"><h3>Component</h3></div>
          <div className="rev-card">
            <div className="rev-card-name">{name || "Untitled component"}</div>
            {cat && <span className="kindtag kindtag-auto rev-card-cat"><Icon name={cat.icon} size={12} />{cat.label}</span>}
            <div className="rev-meta">
              <div className="metaline"><span>Supplier</span><b>{supplier || "—"}</b></div>
              <div className="metaline"><span>Components</span><b className="mono">{libs.length}</b></div>
            </div>
            <div className="rev-brands-lab">OEM</div>
            <div className="rev-brands">
              {brands.length ? brands.map(b => <span className="reg" key={b}>{b}</span>) : <span className="card-sub">None selected</span>}
            </div>
          </div>
        </aside>
      </div>
    </div>
  );
}

/* ===================================================== live summary (stepper) */
function WizSummary({ name, category, sbom, libs, supplier, brands, matches }) {
  const cat = catOf(category);
  return (
    <aside className="wiz-summary">
      <div className="wiz-summary-head">Component draft</div>
      <div className="wiz-sum-row"><span>Name</span><b>{name || "—"}</b></div>
      <div className="wiz-sum-row"><span>Type</span><b>{cat ? cat.label : "—"}</b></div>
      <div className="wiz-sum-row"><span>SBOM</span><b>{sbom ? `${libs.length} components` : "—"}</b></div>
      <div className="wiz-sum-row"><span>Supplier</span><b>{supplier || "—"}</b></div>
      <div className="wiz-sum-row"><span>OEM</span><b>{brands.length || "—"}</b></div>
      <div className="wiz-sum-div" />
      <div className="wiz-sum-match">
        <span className="wiz-sum-match-num">{sbom ? matches.length : "—"}</span>
        <span className="wiz-sum-match-lab">matching {matches.length === 1 ? "CVE" : "CVEs"}</span>
      </div>
    </aside>
  );
}

/* ========================================================= wizard shell */
function NewComponentWizard({ layout, onCancel, onCreate }) {
  const [step, setStep] = useState(0);
  const [name, setName] = useState("");
  const [category, setCategory] = useState("");
  const [sbom, setSbom] = useState(null);
  const [libs, setLibs] = useState([]);
  const [supplier, setSupplier] = useState("");
  const [brands, setBrands] = useState([]);

  // Matching now runs SERVER-SIDE: the full CVE catalog is no longer shipped to the
  // browser (data isolation), so the old client-side window.VC.correlateSbom() would
  // always find 0. We POST the parsed libraries to /api/components/preview, which runs
  // the real matching pipeline against the catalog and rolls it back (nothing is saved).
  const [matches, setMatches] = useState([]);
  const [matchLoading, setMatchLoading] = useState(false);
  useEffect(() => {
    if (!sbom || !libs.length) { setMatches([]); setMatchLoading(false); return; }
    let cancelled = false;
    setMatchLoading(true);
    const payload = {
      name: name.trim() || "preview",
      libs: libs.map(({ _id, ...l }) => ({ ...l, supplier: !l.supplier || l.supplier === "—" ? null : l.supplier })),
    };
    window.VCApi.previewMatches(payload)
      .then((ms) => { if (!cancelled) setMatches(Array.isArray(ms) ? ms : []); })
      .catch(() => { if (!cancelled) setMatches([]); })
      .finally(() => { if (!cancelled) setMatchLoading(false); });
    return () => { cancelled = true; };
  }, [sbom, libs]);

  function loadSbom(parsed) {
    const withIds = parsed.libs.map((c, i) => ({ ...c, _id: "l" + i }));
    setSbom({ meta: parsed.meta, raw: parsed.raw, filename: parsed.filename,
      format: /\.xml$/i.test(parsed.filename) ? "xml" : "json" });
    setLibs(withIds);
    const sup = parsed.meta && parsed.meta.metadata && parsed.meta.metadata.component && parsed.meta.metadata.component.supplier;
    if (!supplier && sup && sup !== "—") setSupplier(sup);
  }
  function clearSbom() { setSbom(null); setLibs([]); }

  const unlocked = (i) => [
    true,
    !!(name.trim() && category),
    !!sbom, !!sbom,
    !!(sbom && supplier),
  ][i];

  function goTo(i) { if (unlocked(i)) { setStep(i); window.scrollTo(0, 0); } }

  function create() {
    onCreate({
      id: "cmp-" + Math.random().toString(36).slice(2, 7),
      name: name.trim() || "Untitled component", category, supplier, brands,
      sbomName: sbom.filename,
      spec: sbom.meta.format === "CSV" ? "CSV" : `${sbom.meta.format || "CycloneDX"} ${sbom.meta.specVersion}`,
      libs: libs.map(({ _id, ...l }) => ({ ...l, supplier: !l.supplier || l.supplier === "—" ? null : l.supplier })),
      createdAt: new Date().toISOString().slice(0, 10),
    });
  }

  const stepId = WIZ_STEPS[step].id;
  let body = null;
  if (stepId === "identity") body = <IdentityStep {...{ name, setName, category, setCategory }} />;
  else if (stepId === "sbom") body = <UploadStep sbom={sbom} libs={libs} matches={matches} matchLoading={matchLoading} onParsed={loadSbom} onClear={clearSbom} />;
  else if (stepId === "refine") body = <EditStep libs={libs} setLibs={setLibs} />;
  else if (stepId === "match") body = <MatchStep {...{ supplier, setSupplier, brands, setBrands }} suggested={sbom?.meta?.metadata?.component?.supplier} />;
  else body = <ReviewStep {...{ name, category, supplier, brands, libs, matches }} />;

  // step-aware footer ------------------------------------------------------
  let footer;
  if (stepId === "identity") footer = (
    <><button className="btn btn-ghost" onClick={onCancel}>Cancel</button><span className="wiz-foot-sp" />
      <button className="btn btn-accent" disabled={!unlocked(1)} onClick={() => goTo(1)}>Continue<Icon name="arrowR" size={15} /></button></>
  );
  else if (stepId === "sbom") footer = (
    <><button className="btn btn-ghost" onClick={() => goTo(0)}><Icon name="arrowL" size={15} />Back</button><span className="wiz-foot-sp" />
      <button className="btn" disabled={!sbom} onClick={() => goTo(2)}><Icon name="pencil" size={14} />Open SBOM editor</button>
      <button className="btn btn-accent" disabled={!sbom} onClick={() => goTo(3)}>Continue to matching<Icon name="arrowR" size={15} /></button></>
  );
  else if (stepId === "refine") footer = (
    <><button className="btn btn-ghost" onClick={() => goTo(1)}><Icon name="arrowL" size={15} />Back</button><span className="wiz-foot-sp" />
      <button className="btn btn-accent" onClick={() => goTo(3)}>Continue to matching<Icon name="arrowR" size={15} /></button></>
  );
  else if (stepId === "match") footer = (
    <><button className="btn btn-ghost" onClick={() => goTo(1)}><Icon name="arrowL" size={15} />Back</button><span className="wiz-foot-sp" />
      <button className="btn btn-accent" disabled={!supplier} onClick={() => goTo(4)}>Review matches<Icon name="arrowR" size={15} /></button></>
  );
  else footer = (
    <><button className="btn btn-ghost" onClick={() => goTo(3)}><Icon name="arrowL" size={15} />Back</button><span className="wiz-foot-sp" />
      <button className="btn btn-accent" onClick={create}><Icon name="check" size={15} />Create component</button></>
  );

  const steps = (
    WIZ_STEPS.map((s, i) => {
      const state = i < step ? "done" : i === step ? "cur" : "todo";
      return (
        <button key={s.id} className={"wizstep is-" + state + (unlocked(i) ? " is-unlocked" : "")}
          onClick={() => goTo(i)} disabled={!unlocked(i)}>
          <span className="wizstep-dot">{i < step ? <Icon name="check" size={14} /> : <Icon name={s.icon} size={14} />}</span>
          <span className="wizstep-tx"><span className="wizstep-lab">{s.label}{s.optional && <em> · optional</em>}</span><span className="wizstep-sub">{s.sub}</span></span>
        </button>
      );
    })
  );

  if (layout === "stepper") {
    return (
      <div className="page wiz wiz-stepper-layout">
        <div className="wiz-top">
          <div><span className="eyebrow">Workflow</span><h1 className="page-title">New component</h1></div>
          <button className="btn btn-ghost btn-sm" onClick={onCancel}><Icon name="close" size={14} />Cancel</button>
        </div>
        <div className="wiz-stepper">{steps}</div>
        <div className="wiz-body-stepper">
          <div className="wiz-panel">{body}<div className="wiz-foot">{footer}</div></div>
          <WizSummary {...{ name, category, sbom, libs, supplier, brands, matches }} />
        </div>
      </div>
    );
  }

  return (
    <div className="page wiz wiz-rail-layout">
      <div className="wiz-top">
        <div><span className="eyebrow">Workflow</span><h1 className="page-title">New component</h1></div>
        <button className="btn btn-ghost btn-sm" onClick={onCancel}><Icon name="close" size={14} />Cancel</button>
      </div>
      <div className="wiz-body-rail">
        <div className="wiz-rail">{steps}</div>
        <div className="wiz-panel">{body}<div className="wiz-foot">{footer}</div></div>
      </div>
    </div>
  );
}

window.NewComponentWizard = NewComponentWizard;
