// data-showcase.jsx — dimensions tree for /data (English-primary; Chinese supplementary)

const { useState, useEffect, useMemo, useRef } = React;

/** Catalogue panel names — English gloss for CSV category headers */
const LAB_PANEL_EN = {
  "CBC (血液常規檢查)": "Complete blood count",
  "GLUF(血糖測試)": "Glucose metabolism",
  "HBT(乙肝組合)": "Hepatitis panel",
  "LIPID 血脂檢查": "Lipid profile",
  "一般檢查": "Vitals & general measures",
  "心臟病風險篩檢": "Cardiac risk screening",
  "性病篩檢 S.T.D.": "STI screening",
  "甲狀腺功能篩檢": "Thyroid function",
  "組織發炎篩檢": "Inflammatory markers",
  "肝膽功能檢查": "Liver & biliary chemistry",
  "腎功能檢查": "Renal panel",
  "腫瘤標記檢查": "Tumour markers",
  "鈣磷鎂鐵檢查": "Ca / PO₄ / Mg / Fe",
};

function splitSuggestionBullets(text) {
  if (!text || typeof text !== "string") return [];
  return text.split(/[;\n；]+/).map((s) => s.trim()).filter(Boolean);
}

function severityGlossEn(severity) {
  switch (severity) {
    case "panic":
      return "Urgent band — same-day escalation pathway (panic limits).";
    case "ref_high":
      return "Above reference range — GP / specialist routing per catalogue.";
    case "ref_low":
      return "Below reference range — monitoring or referral per catalogue.";
    case "positive":
      return "Qualitative positive — confirmatory or referral pathway.";
    case "negative":
      return "Qualitative negative — screening / exclusion pathway.";
    case "compound":
      return "Compound predicate (multiple tests / AND logic).";
    default:
      return "Operator-defined band — traceable to catalogue Key × Result.";
  }
}

function SeverityDot({ severity }) {
  const cls =
    severity === "panic"
      ? "dim-dot dim-dot--panic"
      : severity === "ref_high" || severity === "ref_low"
        ? "dim-dot dim-dot--ref"
        : severity === "positive" || severity === "negative"
          ? "dim-dot dim-dot--flag"
          : severity === "compound"
            ? "dim-dot dim-dot--compound"
            : "dim-dot dim-dot--other";
  return <span className={cls} aria-hidden />;
}

function CodeChips({ codes }) {
  if (!codes || !codes.length) return null;
  return (
    <span className="dim-codes">
      {codes.map((c) => (
        <span key={c} className="dim-code">
          ({c})
        </span>
      ))}
    </span>
  );
}

function SupZh({ children, className }) {
  if (!children) return null;
  return <span className={"dim-sup-zh" + (className ? " " + className : "")}>{children}</span>;
}

function BranchHead({ id, label, subtitle, sourceNote }) {
  return (
    <header className="dim-branch-head">
      <h2 id={id}>
        <span className="dim-branch-label">{label.en}</span>
        <SupZh>{label.tc}</SupZh>
      </h2>
      <p className="dim-branch-sub">{subtitle.en}</p>
      <SupZh>{subtitle.tc}</SupZh>
      {sourceNote ? <p className="dim-source-note">{sourceNote.en}</p> : null}
      {sourceNote?.tc ? <SupZh>{sourceNote.tc}</SupZh> : null}
    </header>
  );
}

function LabBranch({ branch }) {
  return (
    <section className={"dim-branch dim-branch--" + branch.status} aria-labelledby="dim-lab-h">
      <BranchHead
        id="dim-lab-h"
        label={branch.label}
        subtitle={branch.subtitle}
        sourceNote={branch.sourceNote}
      />
      <div className="dim-tree dim-tree--lab">
        {branch.categories.map((cat) => (
          <details key={cat.name} className="dim-details" open>
            <summary className="dim-details-sum">
              <span className="dim-sum-primary">{LAB_PANEL_EN[cat.name] || "Laboratory panel"}</span>
              <SupZh>{cat.name}</SupZh>
            </summary>
            <div className="dim-details-body">
              <div className="dim-comments-grid">
                {cat.comments.map((com) => (
                  <div key={com.name} className="dim-comment-block">
                    <div className="dim-comment-head">
                      <span className="dim-comment-en">Clinical subgroup</span>
                      <SupZh>{com.name}</SupZh>
                    </div>
                    <div className="dim-rule-cards">
                      {com.rules.map((rule, ri) => (
                        <article
                          key={ri}
                          className={"dim-rule-card dim-rule-card--" + rule.severity}
                        >
                          <div className="dim-rule-row dim-rule-row--clause">
                            <SeverityDot severity={rule.severity} />
                            <span className="dim-clause">
                              <strong>{rule.key}</strong>
                              <span className="dim-clause-sep">:</span> {rule.result}
                            </span>
                            {rule.multi ? (
                              <span className="dim-multi-tag">Multi-condition</span>
                            ) : null}
                            <CodeChips codes={rule.codes} />
                          </div>
                          <p className="dim-gloss-en">{severityGlossEn(rule.severity)}</p>
                          <div className="dim-catalogue-block">
                            <div className="dim-catalogue-label">Catalogue wording (source language)</div>
                            <ul className="dim-catalogue-bullets">
                              {splitSuggestionBullets(rule.suggestion).map((b, bi) => (
                                <li key={bi}>{b}</li>
                              ))}
                            </ul>
                          </div>
                        </article>
                      ))}
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </details>
        ))}
      </div>
    </section>
  );
}

function SurveyBranch({ branch }) {
  return (
    <section className={"dim-branch dim-branch--" + branch.status} aria-labelledby="dim-survey-h">
      <BranchHead
        id="dim-survey-h"
        label={branch.label}
        subtitle={branch.subtitle}
        sourceNote={branch.sourceNote}
      />
      <div className="dim-tree dim-tree--survey">
        {branch.sections.map((sec, si) => (
          <details key={si} className="dim-details" open>
            <summary className="dim-details-sum">
              <span className="dim-sum-primary">{sec.name.en}</span>
              <SupZh>{sec.name.tc}</SupZh>
            </summary>
            <div className="dim-details-body">
              <div className="dim-survey-grid">
                {sec.rules.map((r, ri) => (
                  <div key={ri} className="dim-survey-card">
                    <div className="dim-rule-id">{r.ruleId}</div>
                    <div className="dim-survey-block">
                      <span className="dim-field-label">Predicate</span>
                      <p className="dim-field-en">{r.predicate.en}</p>
                      <SupZh>{r.predicate.tc}</SupZh>
                    </div>
                    <div className="dim-survey-block">
                      <span className="dim-field-label">Offered pathway</span>
                      <p className="dim-field-en">{r.package.en}</p>
                      <SupZh>{r.package.tc}</SupZh>
                    </div>
                    <div className="dim-survey-block">
                      <span className="dim-field-label">Clinical routing hint</span>
                      <p className="dim-field-en">{r.actionHint.en}</p>
                      <SupZh>{r.actionHint.tc}</SupZh>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </details>
        ))}
      </div>
    </section>
  );
}

function ImagingBranch({ branch }) {
  return (
    <section className={"dim-branch dim-branch--" + branch.status} aria-labelledby="dim-img-h">
      <header className="dim-branch-head">
        <h2 id="dim-img-h">
          <span className="dim-branch-label">{branch.label.en}</span>
          <SupZh>{branch.label.tc}</SupZh>
          <span className="dim-badge dim-badge--illus">
            Illustrative <span className="dim-badge-sub">示意</span>
          </span>
        </h2>
        <p className="dim-branch-sub">{branch.subtitle.en}</p>
        <SupZh>{branch.subtitle.tc}</SupZh>
      </header>
      <div className="dim-img-grid">
        {branch.modalities.map((mod, mi) => (
          <div key={mi} className="dim-img-card">
            <div className="dim-img-mod">
              <span className="dim-img-mod-en">{mod.name.en}</span>
              <SupZh>{mod.name.tc}</SupZh>
            </div>
            <ul className="dim-img-findings">
              {mod.findings.map((f, fi) => (
                <li key={fi} className="dim-img-finding">
                  <div className="dim-img-find-en">{f.label.en}</div>
                  <SupZh>{f.label.tc}</SupZh>
                  <div className="dim-img-action-en">{f.action.en}</div>
                  <SupZh>{f.action.tc}</SupZh>
                </li>
              ))}
            </ul>
          </div>
        ))}
      </div>
    </section>
  );
}

function ECCapabilityBranch({ branch }) {
  return (
    <section className={"dim-branch dim-branch--" + branch.status} aria-labelledby="dim-ec-h">
      <header className="dim-branch-head">
        <h2 id="dim-ec-h">
          <span className="dim-branch-label">{branch.label.en}</span>
          <SupZh>{branch.label.tc}</SupZh>
          <span className="dim-badge dim-badge--illus">
            Illustrative <span className="dim-badge-sub">示意</span>
          </span>
        </h2>
        <p className="dim-branch-sub">{branch.subtitle.en}</p>
        <SupZh>{branch.subtitle.tc}</SupZh>
      </header>
      <div className="dim-ec-pillars">
        {branch.pillars.map((p, pi) => (
          <details key={pi} className="dim-details dim-details--pillar" open>
            <summary className="dim-details-sum">
              <span className="dim-sum-primary">{p.name.en}</span>
              <SupZh>{p.name.tc}</SupZh>
            </summary>
            <div className="dim-details-body">
              <ul className="dim-ec-scenarios">
                {p.scenarios.map((s, si) => (
                  <li key={si} className="dim-ec-scenario">
                    <div className="dim-ec-block">
                      <span className="dim-ec-k">Signal</span>
                      <div className="dim-ec-en">{s.signal.en}</div>
                      <SupZh>{s.signal.tc}</SupZh>
                    </div>
                    <div className="dim-ec-block">
                      <span className="dim-ec-k">Route</span>
                      <div className="dim-ec-en">{s.route.en}</div>
                      <SupZh>{s.route.tc}</SupZh>
                    </div>
                  </li>
                ))}
              </ul>
            </div>
          </details>
        ))}
      </div>
    </section>
  );
}

function PlannedBranch({ branch }) {
  return (
    <section className={"dim-branch dim-branch--" + branch.status} aria-labelledby="dim-plan-h">
      <header className="dim-branch-head">
        <h2 id="dim-plan-h">
          <span className="dim-branch-label">{branch.label.en}</span>
          <SupZh>{branch.label.tc}</SupZh>
          <span className="dim-badge dim-badge--plan">
            Planned <span className="dim-badge-sub">規劃中</span>
          </span>
        </h2>
        <p className="dim-branch-sub">{branch.subtitle.en}</p>
        <SupZh>{branch.subtitle.tc}</SupZh>
      </header>
      <ul className="dim-planned-list">
        {branch.items.map((it, ii) => (
          <li key={ii} className="dim-planned-li">
            <div className="dim-planned-name-en">{it.name.en}</div>
            <SupZh>{it.name.tc}</SupZh>
            <div className="dim-planned-note">{it.note.en}</div>
            <SupZh>{it.note?.tc}</SupZh>
          </li>
        ))}
      </ul>
    </section>
  );
}

function TotalsStrip({ totals }) {
  if (!totals) return null;
  const items = [
    ["Lab rules", totals.labRules],
    ["Lab categories", totals.labCategories],
    ["Survey rules", totals.surveyRules],
    ["Multi-condition rows", totals.multiConditionRows],
    ["Illustrative modalities", totals.modalitiesIllustrative],
    ["EC pillars", totals.ecPillars],
  ];
  return (
    <div className="dim-totals">
      {items.map(([k, v]) => (
        <div key={k} className="dim-total-chip">
          <span className="dim-total-n">{v}</span>
          <span className="dim-total-l">{k}</span>
        </div>
      ))}
    </div>
  );
}

function pathPrefixMatch(rowPath, prefix) {
  if (!rowPath || rowPath.length < prefix.length) return false;
  for (let i = 0; i < prefix.length; i++) {
    if (rowPath[i] !== prefix[i]) return false;
  }
  return true;
}

/** Outline-tree lines: vertical continues while descendants remain under the same prefix. */
function attachOutlineTreeLines(rows) {
  return rows.map((row, i) => {
    const d = row.depth;
    const verticalContinues = [];
    for (let k = 0; k < d - 1; k++) {
      const prefix = row.path.slice(0, k + 1);
      let lastIdx = i;
      for (let j = i; j < rows.length; j++) {
        if (pathPrefixMatch(rows[j].path, prefix)) lastIdx = j;
      }
      verticalContinues.push(lastIdx > i);
    }
    return { ...row, verticalContinues };
  });
}

/** Flat rows for tree-table (outline / file-tree style — not node–edge graph). */
function buildDimensionTreeRows(rootsById) {
  const rows = [];
  const lab = rootsById.lab;
  if (lab) {
    rows.push({
      depth: 0,
      kind: "root",
      path: ["root:lab"],
      primary: lab.label.en,
      zh: lab.label.tc,
      status: lab.status,
    });
    const cats = lab.categories;
    for (let ci = 0; ci < cats.length; ci++) {
      const cat = cats[ci];
      const catEn = LAB_PANEL_EN[cat.name] || "Laboratory panel";
      const catPath = ["root:lab", "c:" + ci];
      rows.push({
        depth: 1,
        kind: "category",
        path: catPath,
        isLastSibling: ci === cats.length - 1,
        primary: catEn,
        zh: cat.name,
      });
      const coms = cat.comments;
      for (let cj = 0; cj < coms.length; cj++) {
        const com = coms[cj];
        const comPath = catPath.concat("m:" + cj);
        rows.push({
          depth: 2,
          kind: "subgroup",
          path: comPath,
          isLastSibling: cj === coms.length - 1,
          primary: com.name,
        });
        const rules = com.rules;
        for (let rk = 0; rk < rules.length; rk++) {
          const rule = rules[rk];
          rows.push({
            depth: 3,
            kind: "lab-rule",
            path: comPath.concat("r:" + rk),
            isLastSibling: rk === rules.length - 1,
            primary: `${rule.key}: ${rule.result}`,
            detail: rule.suggestion,
            severity: rule.severity,
            codes: rule.codes,
            multi: rule.multi,
          });
        }
      }
    }
  }
  const survey = rootsById.survey;
  if (survey) {
    rows.push({
      depth: 0,
      kind: "root",
      path: ["root:survey"],
      primary: survey.label.en,
      zh: survey.label.tc,
      status: survey.status,
    });
    const secs = survey.sections;
    for (let si = 0; si < secs.length; si++) {
      const sec = secs[si];
      const secPath = ["root:survey", "s:" + si];
      rows.push({
        depth: 1,
        kind: "section",
        path: secPath,
        isLastSibling: si === secs.length - 1,
        primary: sec.name.en,
        zh: sec.name.tc,
      });
      const rules = sec.rules;
      for (let ri = 0; ri < rules.length; ri++) {
        const r = rules[ri];
        rows.push({
          depth: 2,
          kind: "survey-rule",
          path: secPath.concat("q:" + ri),
          isLastSibling: ri === rules.length - 1,
          primary: r.ruleId,
          zh: r.predicate.tc,
          detail: r.predicate.en,
          route: r.package?.en,
          hint: r.actionHint?.en,
        });
      }
    }
  }
  const imaging = rootsById.imaging;
  if (imaging) {
    rows.push({
      depth: 0,
      kind: "root",
      path: ["root:img"],
      primary: imaging.label.en,
      zh: imaging.label.tc,
      status: imaging.status,
      illustrative: true,
    });
    const mods = imaging.modalities;
    for (let mi = 0; mi < mods.length; mi++) {
      const mod = mods[mi];
      const modPath = ["root:img", "mod:" + mi];
      rows.push({
        depth: 1,
        kind: "modality",
        path: modPath,
        isLastSibling: mi === mods.length - 1,
        primary: mod.name.en,
        zh: mod.name.tc,
      });
      const findings = mod.findings;
      for (let fi = 0; fi < findings.length; fi++) {
        const f = findings[fi];
        rows.push({
          depth: 2,
          kind: "finding",
          path: modPath.concat("f:" + fi),
          isLastSibling: fi === findings.length - 1,
          primary: f.label.en,
          zh: f.label.tc,
          detail: f.action?.en,
        });
      }
    }
  }
  const ec = rootsById.ec_capability;
  if (ec) {
    rows.push({
      depth: 0,
      kind: "root",
      path: ["root:ec"],
      primary: ec.label.en,
      zh: ec.label.tc,
      status: ec.status,
      illustrative: true,
    });
    const pillars = ec.pillars;
    for (let pi = 0; pi < pillars.length; pi++) {
      const p = pillars[pi];
      const pilPath = ["root:ec", "p:" + pi];
      rows.push({
        depth: 1,
        kind: "pillar",
        path: pilPath,
        isLastSibling: pi === pillars.length - 1,
        primary: p.name.en,
        zh: p.name.tc,
      });
      const scenarios = p.scenarios;
      for (let si = 0; si < scenarios.length; si++) {
        const s = scenarios[si];
        rows.push({
          depth: 2,
          kind: "scenario",
          path: pilPath.concat("x:" + si),
          isLastSibling: si === scenarios.length - 1,
          primary: s.signal.en,
          zh: s.signal.tc,
          detail: s.route?.en,
        });
      }
    }
  }
  const planned = rootsById.planned;
  if (planned) {
    rows.push({
      depth: 0,
      kind: "root",
      path: ["root:plan"],
      primary: planned.label.en,
      zh: planned.label.tc,
      status: planned.status,
      plannedFlag: true,
    });
    const items = planned.items;
    for (let ii = 0; ii < items.length; ii++) {
      const it = items[ii];
      rows.push({
        depth: 1,
        kind: "planned-item",
        path: ["root:plan", "i:" + ii],
        isLastSibling: ii === items.length - 1,
        primary: it.name.en,
        zh: it.name.tc,
        detail: it.note?.en,
      });
    }
  }
  return attachOutlineTreeLines(rows);
}

function truncateGraphLabel(s, maxLen) {
  if (!s || typeof s !== "string") return "";
  if (s.length <= maxLen) return s;
  return s.slice(0, Math.max(0, maxLen - 1)) + "…";
}

/** 2-line wrap for graph labels (keeps text under the dot, not in the next column). */
function wrapGraphLabelLines(s, perLine, maxLines) {
  if (!s || typeof s !== "string") return [""];
  const out = [];
  let rest = s.trim();
  while (rest.length && out.length < maxLines) {
    if (rest.length <= perLine) {
      out.push(rest);
      return out;
    }
    let cut = rest.lastIndexOf(" ", perLine);
    if (cut <= 4) cut = perLine;
    out.push(rest.slice(0, cut).trim());
    rest = rest.slice(cut).trim();
  }
  if (rest.length && out.length) {
    out[out.length - 1] = truncateGraphLabel(out[out.length - 1] + " " + rest, perLine + 12);
  }
  return out.length ? out : [""];
}

/** Nested tree for D3 layout — nodes and edges (SVG graph view). */
function buildGraphHierarchy(rootsById) {
  const topChildren = [];

  const lab = rootsById.lab;
  if (lab) {
    const labKids = [];
    for (let ci = 0; ci < lab.categories.length; ci++) {
      const cat = lab.categories[ci];
      const catEn = LAB_PANEL_EN[cat.name] || "Laboratory panel";
      const catKids = [];
      for (let cj = 0; cj < cat.comments.length; cj++) {
        const com = cat.comments[cj];
        const ruleKids = com.rules.map((rule, rk) => {
          const sug = String(rule.suggestion || "")
            .trim()
            .replace(/\s+/g, " ");
          return {
            id: "g:lab:" + ci + ":" + cj + ":r" + rk,
            kind: "rule",
            label: truncateGraphLabel(rule.key + ": " + rule.result, 36),
            fullLabel: rule.key + ": " + rule.result,
            fullAction: sug || undefined,
            actionLine: sug || undefined,
          };
        });
        catKids.push({
          id: "g:lab:" + ci + ":m" + cj,
          kind: "subgroup",
          label: truncateGraphLabel(com.name, 32),
          fullLabel: com.name,
          children: ruleKids,
        });
      }
      labKids.push({
        id: "g:lab:c" + ci,
        kind: "category",
        label: catEn,
        children: catKids,
      });
    }
    topChildren.push({
      id: "g:lab",
      kind: "domain",
      label: lab.label.en,
      children: labKids,
    });
  }

  const survey = rootsById.survey;
  if (survey) {
    const secKids = [];
    for (let si = 0; si < survey.sections.length; si++) {
      const sec = survey.sections[si];
      const ruleKids = sec.rules.map((r, ri) => {
        const pkg = (r.package && r.package.en) || "";
        const hint = (r.actionHint && r.actionHint.en) || "";
        const route = [pkg, hint].filter(Boolean).join(" · ");
        return {
          id: "g:survey:" + si + ":q" + ri,
          kind: "survey-rule",
          label: r.ruleId,
          fullLabel: (r.predicate && r.predicate.en ? r.ruleId + " · " + r.predicate.en : r.ruleId),
          fullAction: route || undefined,
          actionLine: route || undefined,
        };
      });
      secKids.push({
        id: "g:survey:s" + si,
        kind: "section",
        label: sec.name.en,
        children: ruleKids,
      });
    }
    topChildren.push({
      id: "g:survey",
      kind: "domain",
      label: survey.label.en,
      children: secKids,
    });
  }

  const imaging = rootsById.imaging;
  if (imaging) {
    const modKids = [];
    for (let mi = 0; mi < imaging.modalities.length; mi++) {
      const mod = imaging.modalities[mi];
      const findKids = mod.findings.map((f, fi) => {
        const act = ((f.action && f.action.en) || "").trim();
        return {
          id: "g:img:" + mi + ":f" + fi,
          kind: "finding",
          label: truncateGraphLabel(f.label.en, 34),
          fullLabel: f.label.en,
          fullAction: act || undefined,
          actionLine: act || undefined,
        };
      });
      modKids.push({
        id: "g:img:m" + mi,
        kind: "modality",
        label: mod.name.en,
        children: findKids,
      });
    }
    topChildren.push({
      id: "g:img",
      kind: "domain",
      label: imaging.label.en,
      children: modKids,
    });
  }

  const ec = rootsById.ec_capability;
  if (ec) {
    const pilKids = [];
    for (let pi = 0; pi < ec.pillars.length; pi++) {
      const p = ec.pillars[pi];
      const scenKids = p.scenarios.map((s, si) => {
        const route = ((s.route && s.route.en) || "").trim();
        return {
          id: "g:ec:" + pi + ":x" + si,
          kind: "scenario",
          label: truncateGraphLabel(s.signal.en, 34),
          fullLabel: s.signal.en,
          fullAction: route || undefined,
          actionLine: route || undefined,
        };
      });
      pilKids.push({
        id: "g:ec:p" + pi,
        kind: "pillar",
        label: p.name.en,
        children: scenKids,
      });
    }
    topChildren.push({
      id: "g:ec",
      kind: "domain",
      label: ec.label.en,
      children: pilKids,
    });
  }

  const planned = rootsById.planned;
  if (planned) {
    const itemKids = planned.items.map((it, ii) => {
      const note = ((it.note && it.note.en) || "").trim();
      return {
        id: "g:plan:i" + ii,
        kind: "planned",
        label: truncateGraphLabel(it.name.en, 36),
        fullLabel: it.name.en,
        fullAction: note || undefined,
        actionLine: note || undefined,
      };
    });
    topChildren.push({
      id: "g:plan",
      kind: "domain",
      label: planned.label.en,
      children: itemKids,
    });
  }

  return {
    id: "g:root",
    kind: "root",
    label: "Rule dimensions",
    children: topChildren,
  };
}

function annotateGraphCounts(n) {
  if (!n.children || !n.children.length) {
    n.childCount = 0;
    return;
  }
  n.childCount = n.children.length;
  for (const c of n.children) annotateGraphCounts(c);
}

/** Expand-on-click: only include branches whose ancestor ids are in expandedSet. */
function pruneGraphData(node, expandedSet) {
  const out = {
    id: node.id,
    kind: node.kind,
    label: node.label,
    fullLabel: node.fullLabel,
    childCount: node.childCount != null ? node.childCount : 0,
  };
  if (node.actionLine) out.actionLine = node.actionLine;
  if (node.fullAction) out.fullAction = node.fullAction;
  const sub = node.children;
  if (!sub || !sub.length) return out;
  if (!expandedSet.has(node.id)) {
    out.collapsed = true;
    return out;
  }
  out.children = sub.map((c) => pruneGraphData(c, expandedSet));
  return out;
}

function findGraphNodeById(node, id) {
  if (node.id === id) return node;
  if (!node.children) return null;
  for (const c of node.children) {
    const f = findGraphNodeById(c, id);
    if (f) return f;
  }
  return null;
}

function collectDescendantIdsFrom(node) {
  const ids = [];
  function walk(n) {
    if (!n.children) return;
    for (const c of n.children) {
      ids.push(c.id);
      walk(c);
    }
  }
  walk(node);
  return ids;
}

function graphNodeRadius(kind) {
  switch (kind) {
    case "root":
      return 7;
    case "domain":
      return 6;
    case "category":
    case "section":
      return 5;
    case "subgroup":
    case "modality":
    case "pillar":
      return 4;
    default:
      return 3;
  }
}

function layoutRuleGraph(hierarchyData) {
  if (typeof d3 === "undefined") return null;
  const root = d3.hierarchy(hierarchyData);
  /* D3 tree: x = sibling spread, y = depth. Swap → left→right depth. Large depthSep = room for labels below nodes. */
  const siblingSep = 48;
  const depthSep = 210;
  d3.tree().nodeSize([siblingSep, depthSep])(root);
  root.each((d) => {
    const t = d.x;
    d.x = d.y;
    d.y = t;
  });
  let x0 = Infinity;
  let x1 = -Infinity;
  let y0 = Infinity;
  let y1 = -Infinity;
  root.each((d) => {
    if (d.x < x0) x0 = d.x;
    if (d.x > x1) x1 = d.x;
    if (d.y < y0) y0 = d.y;
    if (d.y > y1) y1 = d.y;
  });
  const pad = 56;
  /* Labels sit under the dot (not to the right); reserve space for 2 lines + fold badge */
  const rightMargin = 40;
  const bottomLabelPad = 120;
  root.each((d) => {
    d.x = d.x - x0 + pad;
    d.y = d.y - y0 + pad;
  });
  const width = x1 - x0 + pad * 2 + rightMargin;
  const height = y1 - y0 + pad * 2 + bottomLabelPad;
  const linkPath = d3
    .linkHorizontal()
    .x((d) => d.x)
    .y((d) => d.y);
  return { root, width, height, linkPath };
}

function RuleDimensionsGraph({ rootsById }) {
  const fullHierarchy = useMemo(() => {
    const data = buildGraphHierarchy(rootsById);
    annotateGraphCounts(data);
    return data;
  }, [rootsById]);

  const [expanded, setExpanded] = useState(() => new Set(["g:root"]));

  const layout = useMemo(() => {
    if (typeof d3 === "undefined" || !fullHierarchy) return null;
    try {
      const pruned = pruneGraphData(fullHierarchy, expanded);
      return layoutRuleGraph(pruned);
    } catch (e) {
      console.warn("RuleDimensionsGraph layout:", e);
      return null;
    }
  }, [fullHierarchy, expanded]);

  const onToggleNode = (id, childCount) => {
    if (!childCount) return;
    setExpanded((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
        const n = findGraphNodeById(fullHierarchy, id);
        if (n) {
          for (const oid of collectDescendantIdsFrom(n)) {
            next.delete(oid);
          }
        }
      } else {
        next.add(id);
      }
      return next;
    });
  };

  const svgRef = useRef(null);
  const layerRef = useRef(null);

  useEffect(() => {
    if (!layout || typeof d3 === "undefined") return;
    const svg = d3.select(svgRef.current);
    const layer = d3.select(layerRef.current);
    const zoom = d3.zoom().scaleExtent([0.08, 2.4]).on("zoom", (ev) => {
      layer.attr("transform", ev.transform);
    });
    svg.call(zoom);
    return () => {
      svg.on(".zoom", null);
    };
  }, [layout]);

  if (!layout) {
    return (
      <div className="dim-graph-fallback">
        <p>Graph layout needs D3 — ensure d3.min.js is loaded.</p>
      </div>
    );
  }

  const { root, width, height, linkPath } = layout;
  const nodes = root.descendants();
  const links = root.links();

  return (
    <div className="dim-graph-wrap">
      <p className="dim-graph-hint">
        Tree expands left → right · click a node to open or close its branch (areas start collapsed) · drag
        to pan · wheel to zoom
      </p>
      <svg
        ref={svgRef}
        className="dim-graph-svg"
        width={width}
        height={height}
        viewBox={"0 0 " + width + " " + height}
        role="img"
        aria-label="Rule dimensions node graph"
      >
        <rect className="dim-graph-bg" width={width} height={height} />
        <g ref={layerRef} className="dim-graph-layer">
          <g className="dim-graph-links">
            {links.map((link, i) => (
              <path key={i} className="dim-graph-link" fill="none" d={linkPath(link)} />
            ))}
          </g>
          <g className="dim-graph-nodes">
            {nodes.map((d, i) => {
              const r = graphNodeRadius(d.data.kind);
              const titleBase = d.data.fullLabel || d.data.label;
              const actionTip = d.data.fullAction;
              const cc = d.data.childCount || 0;
              const isOpen = expanded.has(d.data.id);
              const canToggle = cc > 0;
              const showCollapsed = d.data.collapsed;
              const rawLabel = d.data.label + (showCollapsed && cc > 0 ? " (" + cc + ")" : "");
              const labelLines = wrapGraphLabelLines(rawLabel, 24, 2);
              const hasAction = !!(d.data.actionLine && String(d.data.actionLine).trim().length);
              const actionLines = hasAction
                ? wrapGraphLabelLines(String(d.data.actionLine), 30, 2)
                : [];
              const lastLabelBaseline = r + 12 + (labelLines.length - 1) * 13;
              const actionFirstDy = lastLabelBaseline + 10;
              const titleStr =
                [titleBase, actionTip].filter(Boolean).join(" — ") +
                (canToggle
                  ? isOpen
                    ? " — click to collapse"
                    : " — click to expand " + cc + " children"
                  : "");
              return (
                <g
                  key={d.data.id || "n-" + i}
                  className={
                    "dim-graph-node dim-graph-node--" +
                    d.data.kind +
                    (showCollapsed ? " dim-graph-node--folded" : "") +
                    (canToggle ? " dim-graph-node--click" : "")
                  }
                  style={{ cursor: canToggle ? "pointer" : "default" }}
                  transform={"translate(" + d.x + "," + d.y + ")"}
                  onClick={(ev) => {
                    ev.stopPropagation();
                    onToggleNode(d.data.id, cc);
                  }}
                  onKeyDown={(ev) => {
                    if (canToggle && (ev.key === "Enter" || ev.key === " ")) {
                      ev.preventDefault();
                      onToggleNode(d.data.id, cc);
                    }
                  }}
                  role={canToggle ? "button" : undefined}
                  tabIndex={canToggle ? 0 : undefined}
                  aria-expanded={canToggle ? isOpen : undefined}
                >
                  <title>{titleStr}</title>
                  <circle className="dim-graph-dot" r={r} />
                  <text
                    className="dim-graph-label"
                    textAnchor="middle"
                    style={{ pointerEvents: "none" }}
                  >
                    {labelLines.map((line, li) => (
                      <tspan
                        key={li}
                        x={0}
                        dy={li === 0 ? r + 12 : 13}
                        className={li > 0 ? "dim-graph-label-line2" : undefined}
                      >
                        {line}
                      </tspan>
                    ))}
                  </text>
                  {hasAction ? (
                    <text
                      className="dim-graph-action"
                      textAnchor="middle"
                      style={{ pointerEvents: "none" }}
                    >
                      {actionLines.map((line, li) => (
                        <tspan key={"a-" + li} x={0} dy={li === 0 ? actionFirstDy : 11}>
                          {line}
                        </tspan>
                      ))}
                    </text>
                  ) : null}
                </g>
              );
            })}
          </g>
        </g>
      </svg>
    </div>
  );
}

/** Unicode outline tree (explorer-style hierarchy — not graph edges). */
function TreeOutlineGuides({ row }) {
  if (row.depth === 0) return null;
  const vc = row.verticalContinues || [];
  return (
    <div className="dim-tree-outline" aria-hidden>
      {vc.map((cont, k) => (
        <span
          key={k}
          className={"dim-tree-rail" + (cont ? " dim-tree-rail--pipe" : "")}
        >
          {cont ? "│" : "\u00a0"}
        </span>
      ))}
      <span className="dim-tree-fork">
        <span className="dim-tree-fork-char">{row.isLastSibling ? "└" : "├"}</span>
        <span className="dim-tree-fork-arm">─</span>
      </span>
    </div>
  );
}

function RootStatusTag({ row }) {
  if (row.kind !== "root") return null;
  if (row.illustrative || row.status === "illustrative") {
    return (
      <span className="dim-tree-tag dim-tree-tag--illus" title="Illustrative">
        ILLUS
      </span>
    );
  }
  if (row.plannedFlag || row.status === "planned") {
    return (
      <span className="dim-tree-tag dim-tree-tag--plan" title="Planned">
        PLAN
      </span>
    );
  }
  if (row.status === "live") {
    return (
      <span className="dim-tree-tag dim-tree-tag--live" title="Live catalogue">
        LIVE
      </span>
    );
  }
  return null;
}

function RuleTreeTable({ rows }) {
  return (
    <div className="dim-tree-wrap">
      <table className="dim-tree-table" role="treegrid" aria-label="Rule dimensions tree">
        <thead>
          <tr>
            <th className="dim-tree-th dim-tree-th--tree">Dimension</th>
            <th className="dim-tree-th">Condition · detail</th>
            <th className="dim-tree-th dim-tree-th--meta">Routing · codes</th>
          </tr>
        </thead>
        <tbody>
          {rows.map((row, i) => {
            const branch =
              row.kind === "root" ||
              row.kind === "category" ||
              row.kind === "subgroup" ||
              row.kind === "section" ||
              row.kind === "modality" ||
              row.kind === "pillar";
            let detail = "";
            let meta = null;
            if (row.kind === "lab-rule") {
              detail = row.detail || "";
              meta = (
                <span className="dim-tree-meta-inner">
                  <SeverityDot severity={row.severity} />
                  <span className={"dim-tree-sev dim-tree-sev--" + row.severity}>
                    {String(row.severity).replace(/_/g, " ")}
                  </span>
                  {row.multi ? <span className="dim-tree-multi">multi</span> : null}
                  {row.codes && row.codes.length ? (
                    <span className="dim-tree-codes">
                      {row.codes.map((c) => (
                        <code key={c} className="dim-tree-code">
                          {c}
                        </code>
                      ))}
                    </span>
                  ) : null}
                </span>
              );
            } else if (row.kind === "survey-rule") {
              detail = row.detail || "";
              meta = (
                <span className="dim-tree-meta-inner dim-tree-meta-stack">
                  {row.route ? <span className="dim-tree-route">{row.route}</span> : null}
                  {row.hint ? (
                    <span className="dim-tree-hint">{row.hint}</span>
                  ) : null}
                </span>
              );
            } else if (
              row.kind === "finding" ||
              row.kind === "scenario" ||
              row.kind === "planned-item"
            ) {
              detail = row.detail || "";
            }
            return (
              <tr
                key={i}
                className={
                  "dim-tree-tr" +
                  (branch ? " dim-tree-tr--branch" : " dim-tree-tr--leaf")
                }
              >
                <td className="dim-tree-td dim-tree-td--tree">
                  <div className="dim-tree-cell">
                    <TreeOutlineGuides row={row} />
                    <div className="dim-tree-label-stack">
                      <span className="dim-tree-label">
                        {row.primary}
                        <RootStatusTag row={row} />
                      </span>
                      {row.zh ? (
                        <span className="dim-tree-zh">{row.zh}</span>
                      ) : null}
                    </div>
                  </div>
                </td>
                <td className="dim-tree-td dim-tree-td--detail">
                  {detail ? (
                    <span className="dim-tree-detail" title={detail}>
                      {detail}
                    </span>
                  ) : branch ? (
                    <span className="dim-tree-dash">—</span>
                  ) : null}
                </td>
                <td className="dim-tree-td dim-tree-td--meta">{meta}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

function ViewToggle({ view, onView }) {
  return (
    <div className="dim-view-bar" role="group" aria-label="Layout">
      <button
        type="button"
        className={"dim-view-btn" + (view === "catalogue" ? " dim-view-btn--active" : "")}
        aria-pressed={view === "catalogue"}
        onClick={() => onView("catalogue")}
      >
        Catalogue
      </button>
      <button
        type="button"
        className={"dim-view-btn" + (view === "tree" ? " dim-view-btn--active" : "")}
        aria-pressed={view === "tree"}
        onClick={() => onView("tree")}
      >
        Tree table
      </button>
      <button
        type="button"
        className={"dim-view-btn" + (view === "graph" ? " dim-view-btn--active" : "")}
        aria-pressed={view === "graph"}
        onClick={() => onView("graph")}
      >
        Graph
      </button>
    </div>
  );
}

function DataShowcaseApp() {
  const [doc, setDoc] = useState(null);
  const [err, setErr] = useState(null);
  const [view, setView] = useState("catalogue");

  useEffect(() => {
    const dark = view === "tree" || view === "graph";
    document.body.classList.toggle("data-tree-view", dark);
    return () => document.body.classList.remove("data-tree-view");
  }, [view]);

  useEffect(() => {
    let cancelled = false;
    fetch("data/dimensions.json?v=40")
      .then((r) => {
        if (!r.ok) throw new Error("dimensions.json " + r.status);
        return r.json();
      })
      .then((d) => {
        if (!cancelled) setDoc(d);
      })
      .catch((e) => {
        if (!cancelled) setErr(String(e.message || e));
      });
    return () => {
      cancelled = true;
    };
  }, []);

  const rootsById = useMemo(() => {
    if (!doc || !doc.roots) return {};
    const m = {};
    for (const r of doc.roots) m[r.id] = r;
    return m;
  }, [doc]);

  const treeRows = useMemo(() => buildDimensionTreeRows(rootsById), [rootsById]);

  if (err) {
    return (
      <div className="dim-loading dim-loading--err">
        <p>Could not load dimensions.json — {err}</p>
      </div>
    );
  }

  if (!doc) {
    return (
      <div className="dim-loading">
        <p>Loading dimensions…</p>
      </div>
    );
  }

  const meta = doc.meta || {};

  return (
    <div
      className={
        "dim-shell dim-shell--dense" +
        (view === "tree" || view === "graph" ? " dim-shell--tree-view" : "")
      }
    >
      <header className="dim-hero dim-hero--minimal dim-hero--with-toggle">
        <div className="dim-hero-row">
          <TotalsStrip totals={doc.totals} />
          <ViewToggle view={view} onView={setView} />
        </div>
      </header>

      {view === "catalogue" ? (
        <div className="dim-columns">
          {rootsById.lab ? <LabBranch branch={rootsById.lab} /> : null}
          {rootsById.survey ? <SurveyBranch branch={rootsById.survey} /> : null}
          {rootsById.imaging ? <ImagingBranch branch={rootsById.imaging} /> : null}
          {rootsById.ec_capability ? <ECCapabilityBranch branch={rootsById.ec_capability} /> : null}
          {rootsById.planned ? <PlannedBranch branch={rootsById.planned} /> : null}
        </div>
      ) : view === "tree" ? (
        <RuleTreeTable rows={treeRows} />
      ) : (
        <RuleDimensionsGraph rootsById={rootsById} />
      )}

      <footer
        className={"dim-foot" + (view === "tree" || view === "graph" ? " dim-foot--tree" : "")}
      >
        <p>{meta.footnote?.en}</p>
        <SupZh>{meta.footnote?.tc}</SupZh>
        <a className="dim-foot-link" href="index.html">
          <span className="dim-foot-link-en">Open tablet check-in demo</span>
          <SupZh>開啟自助登記示範</SupZh>
        </a>
      </footer>
    </div>
  );
}

window.DataShowcaseApp = DataShowcaseApp;
