// components.jsx — VulnerabilityCore shared primitives, icons, and SVG charts.
// All visual styling lives in the host CSS (classes); these stay structural.
const { useState, useEffect, useRef, useMemo } = React;

/* --------------------------------------------------- per-page data fetching */
// Each screen fetches its own page endpoint with this hook → { data, loading, error, refetch }.
// A 401 anywhere routes through the shell's logout handler (window.__vcOn401).
// refetch() re-runs the fetch (wire it to <PageError onRetry={refetch} /> so a
// transient failure can recover without reloading the whole app).
function useApi(fn, deps) {
  const [state, setState] = useState({ data: null, loading: true, error: "" });
  const [nonce, setNonce] = useState(0);
  useEffect(() => {
    // Real cancellation: fn receives an AbortSignal and the effect aborts it on
    // unmount / dep-change, so a stale in-flight request can never setState late.
    const ctrl = new AbortController();
    setState((s) => ({ data: s.data, loading: true, error: "" }));
    Promise.resolve().then(() => fn(ctrl.signal)).then(
      (data) => { if (!ctrl.signal.aborted) setState({ data, loading: false, error: "" }); },
      (e) => {
        if (ctrl.signal.aborted || (e && e.aborted)) return; // superseded / unmounted — ignore
        if (e && e.status === 401 && window.__vcOn401) { window.__vcOn401(); return; }
        setState({ data: null, loading: false, error: (e && e.message) || "Request failed." });
      }
    );
    return () => ctrl.abort();
  }, [...(deps || []), nonce]);
  return { ...state, refetch: () => setNonce((n) => n + 1) };
}

// Shared full-page loading / error states for per-page fetches.
function PageLoading({ label }) {
  return (
    <div style={{ display: "grid", placeItems: "center", minHeight: "60vh", gap: 14, textAlign: "center" }}>
      <span className="btn-spin" style={{ width: 28, height: 28, borderWidth: 3 }} />
      <span style={{ opacity: 0.7 }}>{label || "Loading…"}</span>
    </div>
  );
}
function PageError({ message, onRetry }) {
  return (
    <div className="page">
      <div className="empty">
        <Icon name="alert" size={22} />
        <p>{message || "Something went wrong loading this page."}</p>
        {onRetry && <button className="btn btn-ghost btn-sm" onClick={onRetry}>Retry</button>}
      </div>
    </div>
  );
}

/* ---------------------------------------------------------------- icons */
// Minimal single/dual-path line glyphs for UI affordances only.
const ICONS = {
  dashboard: "M3 3h7v7H3zM14 3h7v4h-7zM14 10h7v11h-7zM3 13h7v8H3z",
  ledger:    "M4 5h16M4 10h16M4 15h16M4 20h10",
  browse:    "M12 3l8 4-8 4-8-4 8-4zM4 12l8 4 8-4M4 17l8 4 8-4",
  search:    "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-3.5-3.5",
  filter:    "M3 5h18l-7 8v5l-4 2v-7z",
  bell:      "M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6M10 21h4",
  chevR:     "M9 6l6 6-6 6",
  chevD:     "M6 9l6 6 6-6",
  sortUp:    "M12 4v16M6 10l6-6 6 6",
  sortDown:  "M12 20V4M6 14l6 6 6-6",
  close:     "M6 6l12 12M18 6L6 18",
  menu:      "M4 6h16M4 12h16M4 18h16",
  check:     "M5 12l5 5 9-11",
  clock:     "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 7v5l3 3",
  ext:       "M14 4h6v6M20 4l-9 9M18 13v6H5V6h6",
  shield:    "M12 3l8 3v6c0 5-4 8-8 9-4-1-8-4-8-9V6z",
  chip:      "M7 7h10v10H7zM9 3v2M15 3v2M9 19v2M15 19v2M3 9h2M3 15h2M19 9h2M19 15h2",
  car:       "M3 13l2-5a3 3 0 0 1 3-2h8a3 3 0 0 1 3 2l2 5v5h-3v-2H6v2H3zM7 16h.01M17 16h.01",
  user:      "M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21a8 8 0 0 1 16 0",
  logout:    "M15 4h4v16h-4M11 8l-4 4 4 4M7 12h9",
  sun:       "M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10zM12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19",
  moon:      "M20 14a8 8 0 0 1-10-10 8 8 0 1 0 10 10z",
  sliders:   "M4 6h10M18 6h2M4 12h2M10 12h10M4 18h14M18 18h2M14 4v4M6 10v4M16 16v4",
  bolt:      "M13 2L4 14h7l-1 8 9-12h-7z",
  pulse:     "M3 12h4l2-6 4 14 2-8h6",
  layers:    "M12 3l8 4-8 4-8-4zM4 12l8 4 8-4M4 16l8 4 8-4",
  target:    "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 11.5a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1z",
  download:  "M12 3v12M7 11l5 5 5-5M5 21h14",
  flag:      "M5 21V4M5 4h11l-2 4 2 4H5",
  link:      "M9 15l6-6M10 6l1-1a4 4 0 0 1 6 6l-1 1M14 18l-1 1a4 4 0 0 1-6-6l1-1",
  dot:       "M12 12a3 3 0 1 0 0-.001",
  arrowR:    "M5 12h14M13 6l6 6-6 6",
  arrowL:    "M19 12H5M11 6l-6 6 6 6",
  plus:      "M12 5v14M5 12h14",
  upload:    "M12 15V4M8 8l4-4 4 4M5 20h14",
  doc:       "M6 4a1 1 0 0 1 1-1h7l4 4v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1zM14 3v5h5",
  pencil:    "M4 20h4L19 9l-4-4L4 16zM14 6l4 4",
  trash:     "M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13",
  package:   "M12 3l8 4v10l-8 4-8-4V7zM4 7l8 4 8-4M12 11v10",
  sparkle:   "M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z",
  grip:      "M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01",
  refresh:   "M20 11a8 8 0 0 0-14-5L3 9M3 4v5h5M4 13a8 8 0 0 0 14 5l3-3M21 20v-5h-5",
  alert:     "M12 4l9 16H3zM12 10v4M12 17h.01",
  xcircle:   "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM9 9l6 6M15 9l-6 6",
};
function Icon({ name, size = 16, className = "", style = {} }) {
  const d = ICONS[name] || "";
  const fill = name === "dot" ? "currentColor" : "none";
  return (
    <svg className={"ic " + className} width={size} height={size} viewBox="0 0 24 24"
      fill={fill} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"
      strokeLinejoin="round" style={style} aria-hidden="true">
      <path d={d} />
    </svg>
  );
}

/* ----------------------------------------------------------------- logo */
function Logo({ size = 22, withWord = true, withMark = true }) {
  return (
    <span className="logo" style={{ ["--lz"]: size + "px" }}>
      {withMark && (
        <span className="logo-mark" aria-hidden="true">
          <svg viewBox="0 0 24 24" width={size} height={size}>
            <path d="M12 2l8.5 4.6v8.8L12 22l-8.5-6.6V6.6z" fill="none"
              stroke="var(--accent)" strokeWidth="1.5" strokeLinejoin="round" />
            <path d="M12 8.2l3.6 2v3.6L12 16l-3.6-2.2V10.2z" fill="var(--accent)" opacity="0.9" />
          </svg>
        </span>
      )}
      {withWord && (
        <span className="logo-word">vulnerability<span className="logo-word-2">.core</span></span>
      )}
    </span>
  );
}

/* ------------------------------------------------------------ severity */
// Severity band order. Populated from the backend config (severity_band table) at
// startup via applyAppConfig(); the initial values are the fallback used before
// config arrives or if the fetch fails. Mutated IN PLACE so every module that
// captured this array reference sees the update.
const SEV_ORDER = ["Critical", "High", "Medium", "Low"];
function sevClass(s){ return "sev-" + String(s).toLowerCase().replace(/\s+/g, "-"); }
function SeverityTag({ s, cvss, size = "md" }) {
  return (
    <span className={"sevtag " + sevClass(s) + " sevtag-" + size}>
      <span className="sevtag-dot" />
      <span className="sevtag-label">{s}</span>
      {cvss != null && <span className="sevtag-cvss">{cvss.toFixed(1)}</span>}
    </span>
  );
}

/* CVSS bar (0–10) */
function CvssBar({ cvss, sev }) {
  return (
    <div className="cvssbar" title={"CVSS " + cvss.toFixed(1)}>
      <div className={"cvssbar-fill " + sevClass(sev)} style={{ width: (cvss / 10 * 100) + "%" }} />
    </div>
  );
}

/* ------------------------------------------------------------- status */
// Triage states. The backend (triage_status table) is the source of truth; these
// fallbacks match the seed and are used before config loads or if the fetch fails.
// `auto_enriched` is a pipeline-written, non-selectable state — included here so a
// catalog row carrying it never hits an undefined lookup.
const STATUS_FALLBACK = {
  new:            { dot: "var(--st-new)",  label: "New",           selectable: true,  sortOrder: 1 },
  triaged:        { dot: "var(--st-tri)",  label: "Triaged",       selectable: true,  sortOrder: 2 },
  "in-progress":  { dot: "var(--st-prog)", label: "In progress",   selectable: true,  sortOrder: 3 },
  remediated:     { dot: "var(--st-done)", label: "Remediated",    selectable: true,  sortOrder: 4 },
  accepted:       { dot: "var(--st-acc)",  label: "Accepted risk", selectable: true,  sortOrder: 5 },
  auto_enriched:  { dot: "var(--st-auto)", label: "Auto-enriched", selectable: false, sortOrder: 6 },
};
// Backing object mutated in place by applyAppConfig(). Wrapped in a Proxy so an
// unknown status key (e.g. one an admin adds without a UI reload) resolves to a safe
// default instead of throwing on `STATUS_META[x].label` — there are many such
// unguarded reads across the screens.
const _statusBacking = Object.assign({}, STATUS_FALLBACK);
const _statusDefault = { dot: "var(--st-new)", label: "Unknown", selectable: false };
const STATUS_META = new Proxy(_statusBacking, {
  get(t, k) { return (typeof k === "string" && !(k in t)) ? _statusDefault : t[k]; },
});
// Selectable status keys in display order — the states a user may set manually
// (drives the ledger's status menu + filter). Mutated in place on config load.
const STATUS_SELECTABLE = Object.keys(STATUS_FALLBACK).filter(k => STATUS_FALLBACK[k].selectable);

// Apply the backend AppConfig (statuses + severity bands) to the shared registries.
// Called once at startup after the config fetch; safe to call with a partial/empty
// config (it leaves the fallback in place).
function applyAppConfig(cfg) {
  if (cfg && Array.isArray(cfg.statuses) && cfg.statuses.length) {
    const ordered = cfg.statuses.slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
    Object.keys(_statusBacking).forEach(k => delete _statusBacking[k]);
    ordered.forEach(s => {
      _statusBacking[s.key] = {
        dot: s.dot || "var(--st-new)",
        label: s.label || s.key,
        selectable: s.selectable !== false,
        sortOrder: s.sortOrder || 0,
      };
    });
    STATUS_SELECTABLE.length = 0;
    ordered.filter(s => s.selectable !== false).forEach(s => STATUS_SELECTABLE.push(s.key));
  }
  if (cfg && Array.isArray(cfg.severityBands) && cfg.severityBands.length) {
    const names = cfg.severityBands.slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)).map(b => b.name);
    SEV_ORDER.length = 0;
    names.forEach(n => SEV_ORDER.push(n));
  }
}

function StatusPill({ status, onClick, interactive }) {
  const m = STATUS_META[status] || STATUS_META.new;
  const inner = (
    <>
      <span className="statuspill-dot" style={{ background: m.dot }} />
      {m.label}
      {interactive && <Icon name="chevD" size={12} className="statuspill-chev" />}
    </>
  );
  // Render a span when non-interactive so it can sit inside <button> rows
  // (triage queue, entity drill-down) without invalid <button>-in-<button> nesting.
  if (!interactive) return <span className="statuspill">{inner}</span>;
  return (
    <button className="statuspill statuspill-int" onClick={onClick} type="button">
      {inner}
    </button>
  );
}

/* --------------------------------------------------------------- kind */
function KindTag({ kind }) {
  const auto = kind === "automotive";
  return (
    <span className={"kindtag " + (auto ? "kindtag-auto" : "kindtag-iot")}>
      <Icon name={auto ? "car" : "chip"} size={12} />
      {auto ? "Automotive" : "IoT / Embedded"}
    </span>
  );
}

/* --------------------------------------------------------- CIA triad */
function CIA({ cia, compact }) {
  const items = [["C", "Confidentiality", cia.c], ["I", "Integrity", cia.i], ["A", "Availability", cia.a]];
  const lvl = (v) => ({ High: "cia-hi", Medium: "cia-md", Low: "cia-lo" }[v] || "cia-lo");
  return (
    <div className={"cia" + (compact ? " cia-compact" : "")}>
      {items.map(([k, full, v]) => (
        <div key={k} className={"cia-cell " + lvl(v)} title={full + ": " + v}>
          <span className="cia-k">{k}</span>
          {!compact && <span className="cia-v">{v}</span>}
        </div>
      ))}
    </div>
  );
}

/* ====================================================== CHARTS (svg) */

/* Donut — severity distribution */
function Donut({ data, total, size = 168, thickness = 22 }) {
  const r = (size - thickness) / 2;
  const c = 2 * Math.PI * r;
  let offset = 0;
  const cx = size / 2;
  // `total` is the real count shown in the centre (may be 0); use a non-zero
  // denominator only for the arc geometry so an empty set renders a clean ring.
  const denom = total || 1;
  return (
    <div className="donut-wrap">
      <svg width={size} height={size} className="donut">
        <circle cx={cx} cy={cx} r={r} fill="none" stroke="var(--line)" strokeWidth={thickness} opacity="0.5" />
        {data.map((d, i) => {
          const frac = d.value / denom;
          const dash = frac * c;
          const el = (
            <circle key={i} cx={cx} cy={cx} r={r} fill="none"
              stroke={d.color} strokeWidth={thickness}
              strokeDasharray={`${dash} ${c - dash}`}
              strokeDashoffset={-offset}
              transform={`rotate(-90 ${cx} ${cx})`}
              style={{ transition: "stroke-dasharray .6s ease" }} />
          );
          offset += dash;
          return el;
        })}
      </svg>
      <div className="donut-center">
        <span className="donut-num">{total}</span>
        <span className="donut-lab">tracked</span>
      </div>
    </div>
  );
}

/* Area trend — total vulnerabilities ingested over weeks */
function TrendChart({ trend, width = 560, height = 180 }) {
  const pad = { l: 8, r: 8, t: 14, b: 22 };
  const w = width - pad.l - pad.r, h = height - pad.t - pad.b;
  const tot = (d) => d.auto + d.iot;
  const max = Math.max(...trend.map(tot)) * 1.1;
  const x = (i) => pad.l + (i / (trend.length - 1)) * w;
  const y = (v) => pad.t + h - (v / max) * h;
  const areaPath = (key, base) => {
    const top = trend.map((d, i) => `${x(i)},${y(base(d) + d[key])}`);
    const bot = trend.map((d, i) => `${x(i)},${y(base(d))}`).reverse();
    return `M${top.join(" L")} L${bot.join(" L")} Z`;
  };
  const linePath = (fn) => "M" + trend.map((d, i) => `${x(i)},${y(fn(d))}`).join(" L");
  return (
    <svg width="100%" viewBox={`0 0 ${width} ${height}`} className="trend" preserveAspectRatio="none">
      {[0.25, 0.5, 0.75, 1].map((g, i) => (
        <line key={i} x1={pad.l} x2={width - pad.r} y1={pad.t + h - g * h} y2={pad.t + h - g * h}
          stroke="var(--line)" strokeWidth="1" opacity="0.5" />
      ))}
      <path d={`M${trend.map((d, i) => `${x(i)},${y(tot(d))}`).join(" L")} L${x(trend.length - 1)},${pad.t + h} L${x(0)},${pad.t + h} Z`} fill="var(--trend-line)" opacity="0.28" />
      <path d={linePath(tot)} fill="none" stroke="var(--trend-line)" strokeWidth="2.5" />
      {trend.map((d, i) => (
        i % 2 === 0 ? <text key={i} x={x(i)} y={height - 6} className="trend-x">{d.week}</text> : null
      ))}
    </svg>
  );
}

/* Horizontal bars — top ECUs */
function BarChart({ data, accent = "var(--viz)" }) {
  if (!data || !data.length) {
    return (
      <div className="card-empty">
        <Icon name="layers" size={22} />
        <span className="card-empty-title">No components yet</span>
        <span className="card-empty-sub">Upload an SBOM to see your most affected ECUs.</span>
      </div>
    );
  }
  const max = Math.max(...data.map(d => d.count));
  return (
    <div className="barchart">
      {data.map((d, i) => (
        <div className="barrow" key={i}>
          <span className="barrow-lab">{d.name}</span>
          <div className="barrow-track">
            <div className="barrow-fill" style={{ width: (d.count / max * 100) + "%", background: accent }} />
          </div>
          <span className="barrow-val">{d.count}</span>
        </div>
      ))}
    </div>
  );
}

/* Sparkline */
function Sparkline({ values, width = 88, height = 28, color = "var(--accent)" }) {
  const max = Math.max(...values), min = Math.min(...values);
  const x = (i) => (i / (values.length - 1)) * width;
  const y = (v) => height - ((v - min) / (max - min || 1)) * (height - 4) - 2;
  const d = "M" + values.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" L");
  return (
    <svg width={width} height={height} className="spark">
      <path d={d} fill="none" stroke={color} strokeWidth="1.6" strokeLinecap="round" />
      <circle cx={x(values.length - 1)} cy={y(values[values.length - 1])} r="2.2" fill={color} />
    </svg>
  );
}

Object.assign(window, {
  Icon, Logo, SeverityTag, CvssBar, StatusPill, KindTag, CIA,
  Donut, TrendChart, BarChart, Sparkline,
  SEV_ORDER, STATUS_META, STATUS_SELECTABLE, sevClass, applyAppConfig,
});
