// screens/detail.jsx — Vulnerability detail (adaptive: automotive vs IoT) + remediation workflow
const WORKFLOW = ["new", "triaged", "in-progress", "remediated"];

function StatusWorkflow({ status, onSet }) {
  const idx = WORKFLOW.indexOf(status);
  const accepted = status === "accepted";
  return (
    <div className="wf">
      <div className="wf-track">
        {WORKFLOW.map((s, i) => {
          const done = !accepted && i < idx;
          const cur = !accepted && i === idx;
          return (
            <React.Fragment key={s}>
              <button className={"wf-node" + (done ? " is-done" : "") + (cur ? " is-cur" : "")}
                onClick={() => onSet(s)} title={"Set " + STATUS_META[s].label}>
                <span className="wf-dot">{done ? <Icon name="check" size={13} /> : <span className="wf-dot-i" style={{ background: cur ? STATUS_META[s].dot : "transparent" }} />}</span>
                <span className="wf-lab">{STATUS_META[s].label}</span>
              </button>
              {i < WORKFLOW.length - 1 && <span className={"wf-line" + (!accepted && i < idx ? " is-done" : "")} />}
            </React.Fragment>
          );
        })}
      </div>
      <button className={"wf-accept" + (accepted ? " is-on" : "")} onClick={() => onSet(accepted ? "new" : "accepted")}>
        <span className="statuspill-dot" style={{ background: STATUS_META.accepted.dot }} />
        {accepted ? "Risk accepted" : "Accept risk"}
      </button>
    </div>
  );
}

/* A field the feed left blank says so. An empty value used to render as a bare label
   over nothing, which read as a layout fault rather than as missing data. The API
   sends "—" for some unknowns, so that counts as empty too. */
function Field({ label, children, wide, mono }) {
  const empty = children == null || children === false ||
    (typeof children === "string" && (!children.trim() || children.trim() === "—"));
  return (
    <div className={"fld" + (wide ? " fld-wide" : "") + (empty ? " is-empty" : "")}>
      <span className="fld-lab">{label}</span>
      <span className={"fld-val" + (mono && !empty ? " mono" : "")}>{empty ? "Not specified" : children}</span>
    </div>
  );
}

function Section({ icon, title, kicker, children, tone }) {
  return (
    <section className="dsec">
      <div className="dsec-head">
        <span className={"dsec-ic" + (tone ? " dsec-ic-" + tone : "")}><Icon name={icon} size={16} /></span>
        <h3>{title}</h3>
        {kicker && <span className="dsec-kick">{kicker}</span>}
      </div>
      <div className="dsec-body">{children}</div>
    </section>
  );
}

function CvssGauge({ cvss, sev, compact }) {
  const pct = cvss / 10;
  const r = 52, c = Math.PI * r; // semicircle
  return (
    <div className={"gauge" + (compact ? " gauge-sm" : "")} title={"CVSS v3.1 base score " + cvss.toFixed(1) + " (" + sev + ")"}>
      <svg viewBox="0 0 130 78" width="100%">
        <path d="M13 70 A52 52 0 0 1 117 70" fill="none" stroke="var(--line)" strokeWidth="11" strokeLinecap="round" />
        <path d="M13 70 A52 52 0 0 1 117 70" fill="none" stroke={`var(--sev-${sev.toLowerCase()})`} strokeWidth="11" strokeLinecap="round"
          strokeDasharray={`${pct * c} ${c}`} style={{ transition: "stroke-dasharray .7s ease" }} />
      </svg>
      <div className="gauge-center">
        <span className={"gauge-num sev-text-" + sev.toLowerCase()}>{cvss.toFixed(1)}</span>
        <span className="gauge-lab">CVSS v3.1</span>
      </div>
    </div>
  );
}

/* Risk strip — the four numbers that decide how urgent this finding is, directly under
   the title. They used to live in the side rail, which on a phone put them BELOW the
   comments, so the one thing a reader needs first came last. Same idea and the same
   surface as the component page's strip.

   EPSS is a probability [0..1] of exploitation in the wild within 30 days (FIRST.org
   feed), shown as a percentage because 0.0123 means nothing at a glance and 1.23%
   does; the raw score stays in the tooltip. Rows outside the backfill have epss_score
   null and read "Not scored" — never 0%, since a genuine 0.00x score is real signal. */
function VulnRiskStrip({ v }) {
  const scored = v.epss_score != null;
  const pct = scored ? v.epss_score * 100 : 0;
  const pctText = !scored ? "" : pct > 0 && pct < 0.01 ? "<0.01%" : pct.toFixed(pct < 1 ? 2 : 1) + "%";
  return (
    // .vd-strip is the size container; the grid is the inner div, because a container
    // query can restyle a container's children but never the container itself.
    <section className="vd-strip" aria-label="Risk summary">
      <div className="vd-strip-in">
      <div className="vd-cell vd-cell-cvss">
        <CvssGauge cvss={v.cvss} sev={v.severity} compact />
      </div>
      <div className="vd-cell vd-cell-epss" title={scored ? "EPSS score " + v.epss_score + " (FIRST.org)" : "This entry is not covered by the EPSS feed."}>
        <div className="vd-cell-top">
          <b className={scored ? "" : "is-none"}>{scored ? pctText : "Not scored"}</b>
          <span>EPSS · chance of exploitation in the next 30 days</span>
        </div>
        <div className="epss-bar" aria-hidden="true">
          {scored && <span style={{ width: Math.max(pct, 1.5) + "%" }} />}
        </div>
      </div>
      <div className="vd-cell vd-cell-cia">
        <span className="vd-cell-lab">CIA impact</span>
        <CIA cia={v.cia} />
      </div>
      <div className="vd-cell vd-cell-impact">
        <span className="vd-cell-lab">Overall impact</span>
        <b className={v.impact && String(v.impact).trim() && v.impact !== "—" ? "" : "is-none"}>
          {v.impact && String(v.impact).trim() && v.impact !== "—" ? v.impact : "Not specified"}
        </b>
      </div>
      </div>
    </section>
  );
}

function Detail({ v: initialV, statusOf, setStatus, assigneesOf, toggleAssignee, clearAssignees,
  jiraOf, jiraReady, createJiraIssue, unlinkJiraIssue,
  deadlineOf, setDeadline, onBack, onOpen, backLabel, sbomHits = [] }) {
  const [assignOpen, setAssignOpen] = useState(false);
  // Administrator action — see the ledger, and ForbidNonAdmin server-side.
  const canAssign = canAssignOwners();
  // Owned here rather than in the chip: the create is deliberately not optimistic, so
  // something has to hold "in flight" for the round trip to Jira.
  const [jiraBusy, setJiraBusy] = useState(false);
  // The detail page owns its own endpoint: one record + its related items by id.
  // The clicked record is already a full catalog object, so it renders immediately
  // while /api/detail/{id} fills in the "related" list.
  const { data } = useApi((signal) => window.VCApi.detail(initialV.id, signal), [initialV.id]);
  const v = (data && data.vuln) || initialV;
  const related = (data && data.related) || [];
  const st = statusOf(v);
  const jiraLink = jiraOf ? jiraOf(v) : null;
  const auto = v.kind === "automotive";
  // Enrichment completeness = share of analysis fields actually populated (dynamic).
  const enrich = useMemo(() => {
    const fields = [v.attack_path, v.damage_scenario, v.countermeasures, v.tools_used,
      v.feasibility, v.types_of_attack, v.level_of_attack, v.impact,
      v.cia && (v.cia.c === "High" || v.cia.i === "High" || v.cia.a === "High") ? "y" : ""];
    const filled = fields.filter(x => x && String(x).trim() && String(x).trim() !== "—").length;
    return Math.round(filled / fields.length * 100);
  }, [v]);

  // The same SBOM entry usually appears in several ECU builds (one row per build),
  // so collapse to one line per distinct name+version and list the builds it sits in.
  // Sorted by name for a stable order — the incoming array follows component load
  // order, which is not meaningful to the reader.
  const sbomRows = useMemo(() => {
    const byLib = new Map();
    (sbomHits || []).forEach(({ comp, lib }) => {
      if (!lib || !lib.name) return;
      const key = lib.name + "@" + (lib.version || "");
      let row = byLib.get(key);
      if (!row) byLib.set(key, (row = { key, name: lib.name, version: lib.version || "", builds: [] }));
      // comp.name usually ALREADY carries the version ("01_Appl-build 1.0"), so a
      // naive join renders "01_Appl-build 1.0 1.0". Same guard applySbomUpdate uses.
      const cname = (comp && comp.name) || "";
      const cver = (comp && comp.ecuVersion) || "";
      const build = cver && !cname.endsWith(" " + cver) ? (cname + " " + cver).trim() : cname.trim();
      if (build && row.builds.indexOf(build) === -1) row.builds.push(build);
    });
    return Array.from(byLib.values()).sort((a, b) => a.name.localeCompare(b.name));
  }, [sbomHits]);

  // The name(s) THIS customer uses, for the "Affected component" line. Falls back to
  // the advisory's own string when the match came from outside the loaded component
  // set (a deep link opened before components finish loading, or a catalog-only view).
  const sbomNames = sbomRows.map(r => r.name).join(", ");

  // Header tags, each with its label: bare values ("Amazon Web Services") left the
  // reader guessing whether a tag was the vendor, the supplier or the ECU. A value
  // the feed does not have is left out rather than drawn as an empty tag (the
  // supplier was an icon with nothing beside it); "—" is the API's "unknown".
  const known = (x) => x != null && String(x).trim() !== "" && String(x).trim() !== "—";
  const headTags = [
    [auto ? "ECU" : "Product", auto ? v.ecu : v.product_name],
    [v.interface ? "Interface" : "Protocol", v.interface || v.protocol],
    ["Supplier", v.supplier],
  ].filter(([, val]) => known(val));
  // The analysis grids are two columns; an odd field count left a blank grey cell
  // beside the last one, so that field takes the full row instead.
  const vendorAsPublished = sbomNames && v.affected_component && sbomNames !== v.affected_component;

  return (
    <div className="page detail vuln-detail">
      <button className="back" onClick={onBack}><Icon name="chevR" size={15} style={{ transform: "rotate(180deg)" }} />{backLabel || "Ledger"}</button>

      {/* comp-head: the component page's header treatment (labelled tags, title wrap,
          kicker line), reused so the two detail pages read as one product. */}
      <div className="detail-head">
        <div className="detail-head-main comp-head">
          <div className="detail-id-row">
            <SeverityTag s={v.severity} cvss={v.cvss} />
            <span className="detail-src">via {v.source}</span>
            {known(v.published) && <span className="detail-src comp-src-upd"><span className="comp-src-dot">· </span>published {v.published}</span>}
          </div>
          <h1 className="detail-id mono">{v.id}</h1>
          <p className="detail-desc">{v.description}</p>
          {headTags.length > 0 && (
            <div className="detail-tags">
              {headTags.map(([k, val]) => (
                <span className="dtag" key={k} title={k + ": " + val}>
                  <span className="comp-tag-k">{k}</span><span className="comp-tag-v">{val}</span>
                </span>
              ))}
            </div>
          )}
        </div>
      </div>

      <VulnRiskStrip v={v} />

      {/* Workflow bar, in two rows: WHERE the finding is (the status track and Accept
          risk), then WHO and WHEN (deadline, owners, the Jira issue) and the export.
          It used to be one row that wrapped wherever the space ran out, which put the
          actions under the track at some widths and off the screen on a phone. */}
      <div className="card wf-card vd-wf">
        <div className="wf-card-l">
          <span className="wf-card-lab">Remediation status</span>
          <StatusWorkflow status={st} onSet={(s) => setStatus(v.id, s)} />
        </div>
        <div className="wf-card-r">
          <span className="wf-card-lab">Tracking</span>
          {/* Both the deadline and the assignees are set in the same dialog — the
              date is a property of the remediation, the people are who's doing it. */}
          <DuePill deadline={deadlineOf ? deadlineOf(v) : null} status={st}
            interactive={canAssign} onClick={canAssign ? () => setAssignOpen(true) : undefined} />
          <AssigneePill assignments={assigneesOf ? assigneesOf(v) : []}
            interactive={canAssign} onClick={canAssign ? () => setAssignOpen(true) : undefined} />
          {canAssign && assignOpen &&
            <AssigneeModal
              vulnId={v.id}
              current={assigneesOf ? assigneesOf(v) : []}
              onToggle={(u, on) => toggleAssignee(v.id, u.userId, on)}
              onClearAll={() => clearAssignees(v.id)}
              deadline={deadlineOf ? deadlineOf(v) : null}
              status={st}
              onSetDeadline={setDeadline}
              onClose={() => setAssignOpen(false)} />}
          {/* Jira: the issue key once filed, a File-issue action while the company has
              a working connection, and nothing at all when it does not. Unlink sits
              beside the key rather than inside it — the chip's job is to open Jira. */}
          <JiraChip
            link={jiraLink}
            ready={jiraReady}
            busy={jiraBusy}
            onCreate={async () => {
              if (!createJiraIssue) return;
              setJiraBusy(true);
              await createJiraIssue(v.id);
              setJiraBusy(false);
            }} />
          {/* With Jira off for the company the key stays as history, and the server
              refuses to unlink it until Jira is back. */}
          {jiraLink && unlinkJiraIssue && FEATURES.jira &&
            <button className="btn btn-ghost btn-sm" title="Detach this issue. The Jira ticket is not deleted."
              onClick={() => unlinkJiraIssue(v.id)}>Unlink</button>}
          <span className="vd-wf-sp" />
          <button className="btn btn-ghost btn-sm" title="Download this finding and its deadline as JSON"
            onClick={() => window.VCExport.json(`${v.id}-evidence.json`, { ...v, deadline: deadlineOf ? deadlineOf(v) : null })}><Icon name="download" size={14} />Evidence</button>
        </div>
      </div>

      <div className="detail-grid">
        {/* main column */}
        <div className="detail-main">
          <Section icon="target" title="Attack analysis" kicker="AI-enriched">
            <p className="dsec-lead">{v.attack_path}</p>
            <div className="fldgrid">
              <Field label="Attack type">{v.types_of_attack}</Field>
              <Field label="Access level">{v.level_of_attack}</Field>
              {auto && <Field label="Tooling">{v.tools_used}</Field>}
              <Field label="Feasibility" wide={!auto}>{v.feasibility}</Field>
            </div>
          </Section>

          <Section icon="bolt" title="Damage scenario" kicker="real-world consequence" tone="danger">
            <p className="dsec-lead dsec-danger">{v.damage_scenario}</p>
          </Section>

          {auto ? (
            <Section icon="car" title="Affected components" kicker="automotive">
              <div className="fldgrid">
                <Field label="ECU">{v.ecu}</Field>
                <Field label="Interface">{v.interface}</Field>
                <Field label="Vehicle models" wide>{v.model_name}</Field>
                <Field label="Supplier">{v.supplier}</Field>
                <Field label="OEMs affected">{(v.brands || []).join(", ")}</Field>
              </div>
            </Section>
          ) : (
            <Section icon="chip" title="Affected components" kicker="IoT / embedded">
              <div className="fldgrid">
                <Field label="Vendor">{v.vendor}</Field>
                <Field label="Product">{v.product_name}</Field>
                <Field label="Protocol">{v.protocol}</Field>
                <Field label="Firmware">{v.firmware_version}</Field>
                {/* The customer's own name for the thing, not the vendor's internal
                    one. CVE-2021-27468 publishes "AosService.rem service"; the SBOM
                    that matched it calls the entry "Service". Showing the vendor
                    string here made findings untraceable to the build they came from. */}
                <Field label="Affected component" mono>{sbomNames || v.affected_component}</Field>
                {vendorAsPublished && (
                  <Field label="As published by vendor">{v.affected_component}</Field>
                )}
                <Field label="Library" mono>{v.library_name}</Field>
                <Field label="Supplier">{v.supplier}</Field>
                <Field label="OEMs affected" wide={!!vendorAsPublished}>{(v.brands || []).join(", ")}</Field>
              </div>
            </Section>
          )}

          {/* Why this finding is on this tenant's ledger at all. Rendered for both
              kinds: the automotive branch above has no component field of its own, so
              without this there is nothing on the page tying a CVE to the build it
              came from. Hidden entirely when the components are not loaded yet rather
              than showing an empty shell. */}
          {sbomRows.length > 0 && (
            <Section icon="package" title="In your SBOM"
              kicker={sbomRows.length === 1 ? "1 matched entry" : sbomRows.length + " matched entries"}>
              <div className="fldgrid">
                {sbomRows.map((r, i) => (
                  <Field key={r.key} label={r.builds.join(", ") || "ECU build"} mono
                    wide={sbomRows.length % 2 === 1 && i === sbomRows.length - 1}>
                    {r.name}{r.version ? " " + r.version : ""}
                  </Field>
                ))}
              </div>
            </Section>
          )}

          <Section icon="shield" title="Recommended countermeasures" kicker="mitigation" tone="ok">
            <ul className="cmlist">
              {(v.countermeasures || "").split(/;\s*/).map(s => s.trim()).filter(Boolean).map((c, i) => (
                <li key={i}><Icon name="check" size={14} className="cmlist-ic" /><span>{(c.charAt(0).toUpperCase() + c.slice(1)).replace(/\.$/, "")}</span></li>
              ))}
              {!(v.countermeasures || "").trim() && <li className="card-sub">No countermeasures recorded for this entry.</li>}
            </ul>
          </Section>

          {/* Shared team discussion on THIS finding, server-side, so it is the same
              thread for everyone in the organisation on every device. Scoped to the
              vulnerability rather than the component: a CVE matched by four ECUs is
              one conversation, not four.

              Hidden entirely when a superadmin has switched comments off. That is
              not the enforcement — the server refuses a post regardless, which is what
              covers a session whose cached config predates the change. */}
          <CommentsSection componentId={v.id} scope="vulnerabilities" off={!FEATURES.comments} />
        </div>

        {/* sidebar. The CVSS / EPSS / CIA card that used to open it is the risk strip
            under the header now. */}
        <aside className="detail-side" aria-label="Finding summary">
          <div className="card side-card">
            <span className="side-title">Classification</span>
            <div className="metaline"><span>Source feed</span><b>{v.source}</b></div>
            <div className="metaline"><span>Published</span><b className="mono">{v.published}</b></div>
            <div className="metaline"><span>Enrichment</span><b className="conf"><span className="conf-bar"><span style={{ width: enrich + "%" }} /></span>{enrich}%</b></div>
          </div>

          <div className="card side-card">
            <span className="side-title">Compliance evidence</span>
            <p className="side-note">This record supports monitoring obligations under:</p>
            <div className="reglist">
              {["UNECE R155", "ISO/SAE 21434", "AIS-189 CSMS", "EU CRA"].map(r => (
                <span className="reg" key={r}>{r}</span>
              ))}
            </div>
            <button className="btn btn-ghost btn-block btn-sm" onClick={() => {
            const dl = deadlineOf ? deadlineOf(v) : null;
            // Stamp the derived state next to the raw date: the record then says what
            // the tool asserted on the day it was produced, instead of leaving a bare
            // date whose meaning depends on when someone happens to read the file.
            const dm = window.dueMeta ? window.dueMeta(dl, st) : null;
            window.VCExport.json(`${v.id}-audit.json`, {
              ...v, status: st,
              deadline: dl,
              deadlineState: dm && dm.state !== "none" ? { state: dm.state, daysToDue: dm.days, asOf: new Date().toISOString().slice(0, 10) } : null,
              standards: ["UNECE R155", "ISO/SAE 21434", "AIS-189 CSMS", "EU CRA"],
              exportedFrom: "vulnerabilitycore",
            });
          }}><Icon name="download" size={14} />Export audit record</button>
          </div>

          {/* Supporting documents filed against THIS finding — the supplier advisory,
              the retest report, a screenshot of the fix. Same store and same rules as
              the component evidence card (replace, never delete), scoped to the CVE. */}
          <EvidenceCard componentId={v.id} scope="vulnerabilities" />

          {related.length > 0 && (
            <div className="card side-card">
              <span className="side-title">Related <em>· {(v.kind === "automotive" ? v.ecu : v.product_name) || v.interface || v.protocol}</em></span>
              {related.map(r => (
                <button className="relrow" key={r.id} onClick={() => onOpen(r)}>
                  <SeverityTag s={r.severity} size="sm" />
                  <span className="relrow-id mono">{r.id}</span>
                  <Icon name="chevR" size={14} />
                </button>
              ))}
            </div>
          )}
        </aside>
      </div>
    </div>
  );
}
window.Detail = Detail;
