/* Eno.Health Console — Knowledge base: Exposome registry + mappings.
   Exposome factors are grouped under Exposome Systems (parent: null), mirroring
   Phenotype Systems. An Administrator manages the definitions and the four
   mapping surfaces: wearable/sensor signals, phenotypes, genetics and biomarkers. */
const X = window.ENO;

function exposomeDomainPill(domain) {
  return h(Pill, { tone: X.exposomeDomainTone(domain), soft: true }, X.exposomeDomainLabel(domain));
}

/* Owns the editable copy of the exposome registry and the mapping API. */
function useExposomeData() {
  const [factors, setFactors] = useState(() => X.EXPOSOME_FACTORS.map((e) => ({
    ...e, signals: [...e.signals], phenotypes: [...e.phenotypes], genes: [...e.genes], biomarkers: [...e.biomarkers], refs: (e.refs || []).map((r) => ({ ...r })),
  })));
  const [genes, setGenes] = useState(() => X.EXPOSOME_GENES.map((g) => ({ ...g })));
  const toast = useToast();

  const api = {
    patch: (id, patch) => setFactors((xs) => xs.map((e) => e.id === id ? { ...e, ...patch } : e)),
    toggle: (id, key, val) => setFactors((xs) => xs.map((e) => e.id === id
      ? { ...e, [key]: e[key].includes(val) ? e[key].filter((v) => v !== val) : [...e[key], val] } : e)),
    add: (factor) => { setFactors((xs) => [factor, ...xs]); toast((factor.parent ? "Exposome factor" : "Exposome system") + " “" + factor.label + "” created", "ok"); },
    remove: (id) => {
      const f = factors.find((e) => e.id === id);
      const kids = factors.filter((e) => e.parent === id);
      // promote any children to top-level systems, then drop the factor
      setFactors((xs) => xs.filter((e) => e.id !== id).map((e) => e.parent === id ? { ...e, parent: null } : e));
      const kidNote = kids.length ? " · " + kids.length + " factor" + (kids.length === 1 ? "" : "s") + " promoted to top-level systems" : "";
      toast("“" + (f ? f.label : id) + "” deleted" + kidNote);
    },
    addGene: (g) => { setGenes((xs) => [g, ...xs]); toast("Gene " + g.symbol + " added to registry", "ok"); },
  };
  return { factors, genes, api };
}

/* ======================================================================== */
/* EXPOSOME — main view                                                      */
/* ======================================================================== */
function ExposomeView() {
  const { factors, genes, api } = useExposomeData();
  const toast = useToast();
  const [q, setQ] = useState("");
  const [domain, setDomain] = useState("all");
  const [sel, setSel] = useState(null);
  const [creating, setCreating] = useState(null);   // { parent } seed for new factor/system
  const [removing, setRemoving] = useState(null);
  const [ctx, setCtx] = useState(null);             // { pos, factor } right-click menu
  const [exporting, setExporting] = useState(false);
  const [collapsed, setCollapsed] = useState(() => new Set());
  const toggleCollapse = (id) => setCollapsed((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });

  const childrenOf = (id) => factors.filter((e) => e.parent === id);
  const matches = (e) => (domain === "all" || e.domain === domain) &&
    (!q || (e.label + " " + e.category + " " + e.id).toLowerCase().includes(q.toLowerCase()));

  // Display order: each system followed by its visible children (unless collapsed).
  const visible = [];
  factors.filter((e) => !e.parent).forEach((top) => {
    const kids = childrenOf(top.id);
    const topMatch = matches(top);
    const kidMatch = kids.filter(matches);
    if (topMatch || kidMatch.length) {
      const isCollapsed = collapsed.has(top.id) && !q;
      visible.push({ e: top, depth: 0, collapsed: isCollapsed });
      if (!isCollapsed) (topMatch ? kids : kidMatch).forEach((k) => visible.push({ e: k, depth: 1 }));
    }
  });

  const systemCount = factors.filter((e) => !e.parent).length;
  const factorCount = factors.filter((e) => e.parent).length;
  const selFactor = factors.find((e) => e.id === sel);
  const mappedSensors = factors.filter((e) => e.parent && e.signals.length).length;
  const mappedBio = factors.filter((e) => e.parent && e.biomarkers.length).length;

  const head = ["Exposome factor", "Domain", "Category", "Sensors", "Phenotypes", "Genes", "Biomarkers", "Status"];
  const mapCell = (n) => n ? h("span", { className: "mono" }, n) : h("span", { className: "dim" }, "0");

  return h("div", { className: "view" },
    h(PageHead, { title: "Exposome",
      sub: "The environmental exposures Eno tracks alongside the genome — grouped into systems and mapped to sensors, phenotypes, genes and biomarkers" }),

    h("div", { className: "ref-banner" }, h(Icon, { name: "subtree", size: 16, className: "dim" }),
      h("div", null, "Exposome factors are grouped under ", h("strong", null, "Exposome Systems"), " — e.g. ", h("strong", null, "Particulate Matter"),
        " under ", h("strong", null, "Air Quality"), ". Each factor is mapped to the ", h("strong", null, "sensor signals"), " that observe it, the ",
        h("strong", null, "phenotypes"), " it modulates, the ", h("strong", null, "genes"), " that govern susceptibility, and the ",
        h("strong", null, "biomarkers"), " that register its effect.")),

    h("div", { className: "stat-grid stat-grid-4" },
      h(Card, { pad: false }, h(Stat, { label: "Exposome systems", value: systemCount, icon: "subtree" })),
      h(Card, { pad: false }, h(Stat, { label: "Exposome factors", value: factorCount, icon: "globe" })),
      h(Card, { pad: false }, h(Stat, { label: "Sensor-mapped", value: mappedSensors, tone: "ok", icon: "activity" })),
      h(Card, { pad: false }, h(Stat, { label: "Biomarker-mapped", value: mappedBio, icon: "database" }))),

    h(Card, { pad: false, title: X.EXPOSOME_DB.name, subtitle: "Curated by " + X.EXPOSOME_DB.curatedBy + " · v" + X.EXPOSOME_DB.version,
      actions: h(React.Fragment, null,
        h(Button, { variant: "default", size: "sm", icon: "download", onClick: () => setExporting(true) }, "Export"),
        h(Button, { variant: "primary", size: "sm", icon: "plus", onClick: () => setCreating({ parent: "" }) }, "New exposome")) },
      h("div", { className: "toolbar" },
        h("div", { className: "search" }, h(Icon, { name: "search", size: 16 }),
          h("input", { className: "search-input", placeholder: "Search factor, category or identifier…", value: q, onChange: (e) => setQ(e.target.value) })),
        h("span", { className: "toolbar-count" }, visible.length, " of ", factors.length)),
      h("div", { className: "table-wrap flush" },
        h("table", { className: "table" },
          h("thead", null, h("tr", null, head.map((c, i) => h("th", { key: c, className: i >= 3 && i <= 6 ? "ta-c" : "" }, c)))),
          h("tbody", null,
            visible.map(({ e, depth, collapsed: col }) => {
              const kids = childrenOf(e.id);
              return h("tr", { key: e.id, className: "row-click" + (depth ? " pheno-child" : ""), onClick: () => setSel(e.id),
                  onContextMenu: (ev) => { ev.preventDefault(); setCtx({ pos: { x: ev.clientX, y: ev.clientY }, factor: e }); } },
                h("td", null, h("div", { className: "pheno-cell" + (depth ? " is-sub" : "") },
                  depth ? h("span", { className: "pheno-branch", "aria-hidden": "true" }) : null,
                  h("div", { className: "pheno-cell-main" },
                    h("span", { className: "pheno-name" },
                      !depth && kids.length
                        ? h("button", { className: "pheno-disclose", title: col ? "Expand" : "Collapse",
                            onClick: (ev) => { ev.stopPropagation(); toggleCollapse(e.id); } },
                            h(Icon, { name: col ? "chevronR" : "chevron", size: 14 }))
                        : null,
                      e.label,
                      !depth && kids.length ? h("span", { className: "pheno-subbadge" }, kids.length, " factor", kids.length === 1 ? "" : "s") : null),
                    e.desc ? h("span", { className: "pheno-desc" }, e.desc) : null))),
                h("td", null, exposomeDomainPill(e.domain)),
                h("td", { className: "dim" }, e.category),
                h("td", { className: "ta-c" }, depth ? mapCell(e.signals.length) : h("span", { className: "dim" }, "—")),
                h("td", { className: "ta-c" }, depth ? mapCell(e.phenotypes.length) : h("span", { className: "dim" }, "—")),
                h("td", { className: "ta-c" }, depth ? mapCell(e.genes.length) : h("span", { className: "dim" }, "—")),
                h("td", { className: "ta-c" }, depth ? mapCell(e.biomarkers.length) : h("span", { className: "dim" }, "—")),
                h("td", null, e.status === "active" ? h(Pill, { tone: "ok", soft: true }, "active") : h(Pill, { tone: "neutral", soft: true }, "draft")));
            }),
            !visible.length && h("tr", null, h("td", { colSpan: 8 }, h(Empty, { icon: "scan", title: "No exposome factors", sub: "Adjust your search or domain filter." }))))))),

    h(ExposomeDrawer, { factor: selFactor, factors, genes, api, onClose: () => setSel(null), onDelete: (e) => { setSel(null); setRemoving(e); } }),
    ctx && h(window.ContextMenu, { pos: ctx.pos, onClose: () => setCtx(null),
      items: [
        { label: ctx.factor.status === "active" ? "Unpublish" : "Publish", icon: ctx.factor.status === "active" ? "clock" : "check",
          onClick: () => { const pub = ctx.factor.status !== "active"; api.patch(ctx.factor.id, { status: pub ? "active" : "draft" });
            toast("“" + ctx.factor.label + "” " + (pub ? "published — now live" : "unpublished — moved to draft"), "ok"); } },
        { label: "Export…", icon: "download", onClick: () => toast("Exporting “" + ctx.factor.label + "” definition (mock)") },
        { sep: true },
        { label: ctx.factor.parent ? "Delete factor" : "Delete system", icon: "trash", danger: true,
          onClick: () => { setSel(null); setRemoving(ctx.factor); } },
      ] }),
    h(NewExposomeModal, { seed: creating, existing: factors, onClose: () => setCreating(null), onCreate: (e) => { api.add(e); setCreating(null); setSel(e.id); } }),
    h(ExportExposomeModal, { open: exporting, factors, onClose: () => setExporting(false) }),
    h(DeleteExposomeModal, { factor: removing, children: removing ? childrenOf(removing.id) : [], onClose: () => setRemoving(null), onConfirm: () => { api.remove(removing.id); setRemoving(null); } }));
}

/* ======================================================================== */
/* DRAWER — definition + four mapping surfaces                               */
/* ======================================================================== */
function ExposomeDrawer({ factor, factors, genes, api, onClose, onDelete }) {
  const toast = useToast();
  const [customGene, setCustomGene] = useState("");
  if (!factor) return h(Drawer, { open: false, onClose });

  const f = factor;
  const isSystem = !f.parent;
  const hasChildren = factors.some((e) => e.parent === f.id);
  const systemOpts = factors.filter((e) => !e.parent && e.id !== f.id);

  const addCustomGene = () => {
    const sym = customGene.trim().toUpperCase();
    if (!sym) return;
    if (!genes.find((g) => g.id === sym)) api.addGene({ id: sym, symbol: sym, name: "Custom gene", category: "Custom" });
    api.toggle(f.id, "genes", sym);
    setCustomGene("");
  };

  const mappingSections = h(React.Fragment, null,
    // 1 — sensor / wearable signals
    h("div", { className: "refsec" },
      h("p", { className: "refsec-title" }, "Wearable / sensor signal mappings"),
      h("p", { className: "field-hint", style: { margin: "-3px 0 4px" } }, "Device streams that observe this exposure in real time."),
      h("div", { className: "chip-set" },
        X.SIGNAL_TYPES.map((s) => {
          const on = f.signals.includes(s.key);
          return h("button", { key: s.key, type: "button", className: "chip-tog" + (on ? " on" : ""), onClick: () => api.toggle(f.id, "signals", s.key) },
            h(Icon, { name: on ? "check" : "plus", size: 13 }), s.label,
            h("span", { className: "dim", style: { fontSize: "11px" } }, s.unit));
        }))),

    // 2 — phenotypes
    h("div", { className: "refsec" },
      h("div", { className: "refsec-head" },
        h("p", { className: "refsec-title" }, "Phenotype mappings"),
        h("span", { className: "toolbar-count" }, f.phenotypes.length, " linked")),
      h("p", { className: "field-hint", style: { margin: "-3px 0 7px" } }, "Functional-Medicine phenotypes this exposure modulates."),
      h(window.LinkCombo, { all: X.FM_PHENOTYPES.map((p) => ({ id: p.id, label: p.label, meta: p.parent ? X.phenoLabel(p.parent) : "Phenotype System", search: p.label + " " + p.id + " " + (p.parent ? X.phenoLabel(p.parent) : "") })),
        selected: f.phenotypes, placeholder: "Search phenotype to map…", emptyText: "No phenotypes mapped yet.", noMatchText: "No matching phenotype",
        onAdd: (id) => api.toggle(f.id, "phenotypes", id), onRemove: (id) => api.toggle(f.id, "phenotypes", id) })),

    // 3 — genetics
    h("div", { className: "refsec" },
      h("div", { className: "refsec-head" },
        h("p", { className: "refsec-title" }, "Genetics mappings"),
        h("span", { className: "toolbar-count" }, f.genes.length, " linked")),
      h("p", { className: "field-hint", style: { margin: "-3px 0 7px" } }, "Genes whose variants alter susceptibility to this exposure."),
      h(window.LinkCombo, { all: genes.map((g) => ({ id: g.id, label: g.symbol, meta: g.name + " · " + g.category, search: g.symbol + " " + g.name + " " + g.category })),
        selected: f.genes, mono: true, placeholder: "Search gene by symbol or name…", emptyText: "No genes mapped yet.", noMatchText: "No matching gene",
        onAdd: (id) => api.toggle(f.id, "genes", id), onRemove: (id) => api.toggle(f.id, "genes", id) }),
      h("div", { className: "link-add" },
        h("input", { className: "input mono", placeholder: "Custom gene symbol, e.g. NRF2", value: customGene, onChange: (e) => setCustomGene(e.target.value) }),
        h(Button, { variant: "default", icon: "plus", disabled: !customGene.trim(), onClick: addCustomGene }, "Add custom"))),

    // 4 — biomarkers
    h("div", { className: "refsec" },
      h("div", { className: "refsec-head" },
        h("p", { className: "refsec-title" }, "Biomarker mappings"),
        h("span", { className: "toolbar-count" }, f.biomarkers.length, " linked")),
      h("p", { className: "field-hint", style: { margin: "-3px 0 7px" } }, "Lab biomarkers that register this exposure's biological effect."),
      h(window.LinkCombo, { all: X.BIOMARKERS.map((b) => ({ id: b.id, label: b.name, meta: (b.specimen || "") + (b.unit ? " · " + b.unit : ""), search: b.name + " " + (b.specimen || "") + " " + (b.unit || "") })),
        selected: f.biomarkers, placeholder: "Search biomarker to map…", emptyText: "No biomarkers mapped yet.", noMatchText: "No matching biomarker",
        onAdd: (id) => api.toggle(f.id, "biomarkers", id), onRemove: (id) => api.toggle(f.id, "biomarkers", id) })));

  return h(Drawer, { open: true, onClose, width: 760, title: f.label,
    sub: h(React.Fragment, null,
      isSystem ? h(Pill, { tone: "neutral", soft: true }, "System") : exposomeDomainPill(f.domain),
      h("span", { className: "dim" }, isSystem ? "Exposome System" : f.category, f.unit && f.unit !== "—" ? h(React.Fragment, null, " · ", h(Mono, null, f.unit)) : null)),
    footer: h("div", { className: "drawer-actions" },
      h(Button, { variant: "primary", icon: "check", onClick: () => { toast((isSystem ? "Exposome system" : "Exposome factor") + " saved", "ok"); onClose(); } }, "Save changes"),
      h(Button, { variant: "danger-ghost", icon: "trash", onClick: () => onDelete(f) }, isSystem ? "Delete system" : "Delete factor")) },
    h("div", { className: "refbox" },
      isSystem && hasChildren
        ? h("div", { className: "ref-banner" }, h(Icon, { name: "subtree", size: 16, className: "dim" }),
            h("div", null, h("strong", null, "“" + f.label + "”"), " is an Exposome System grouping ", h("strong", null, factors.filter((e) => e.parent === f.id).length, " factor", factors.filter((e) => e.parent === f.id).length === 1 ? "" : "s"), ". Mappings are normally managed on the individual factors beneath it."))
        : null,

      !isSystem && h("div", { className: "td-stats", style: { border: "1px solid var(--line)", borderRadius: "var(--r)", overflow: "hidden" } },
        h("div", { className: "td-stat" }, h("span", { className: "td-num" }, f.signals.length), h("span", { className: "dim" }, "sensor signals")),
        h("div", { className: "td-stat" }, h("span", { className: "td-num" }, f.phenotypes.length), h("span", { className: "dim" }, "phenotypes")),
        h("div", { className: "td-stat" }, h("span", { className: "td-num" }, f.genes.length), h("span", { className: "dim" }, "genes")),
        h("div", { className: "td-stat" }, h("span", { className: "td-num" }, f.biomarkers.length), h("span", { className: "dim" }, "biomarkers"))),

      // definition
      h("div", { className: "form-grid", style: { display: "grid", gridTemplateColumns: isSystem ? "1fr 1fr" : "1fr 1fr 1fr", gap: "14px" } },
        h(Field, { label: "Domain" },
          h(Select, { value: f.domain, onChange: (v) => api.patch(f.id, { domain: v }), options: X.EXPOSOME_DOMAINS.map((d) => ({ value: d.id, label: d.label })) })),
        h(Field, { label: "Category" },
          h(Select, { value: f.category, onChange: (v) => api.patch(f.id, { category: v }),
            options: X.EXPOSOME_CATEGORIES.includes(f.category) ? X.EXPOSOME_CATEGORIES : [f.category, ...X.EXPOSOME_CATEGORIES] })),
        !isSystem && h(Field, { label: "Unit of measure" },
          h("input", { className: "input mono", value: f.unit, onChange: (e) => api.patch(f.id, { unit: e.target.value }) }))),

      h(Field, { label: "Exposome system",
        hint: hasChildren ? "This factor has its own factors, so it stays a top-level system." : "Optionally group this factor under a system (e.g. Particulate Matter under Air Quality)" },
        h(Select, { value: hasChildren ? "" : (f.parent || ""), disabled: hasChildren, onChange: (v) => api.patch(f.id, { parent: v || null }),
          options: [{ value: "", label: "None — top-level Exposome System" }].concat(systemOpts.map((s) => ({ value: s.id, label: s.label }))) })),

      h(Field, { label: "Definition", hint: "What this exposure is and how it acts on the body" },
        h("textarea", { className: "input", rows: 3, value: f.desc, style: { resize: "vertical", lineHeight: 1.5, fontFamily: "inherit" },
          onChange: (e) => api.patch(f.id, { desc: e.target.value }) })),

      mappingSections,

      // references
      f.refs && f.refs.length ? h("div", { className: "refsec" },
        h("p", { className: "refsec-title" }, "References"),
        h("div", { className: "link-list" }, f.refs.map((r, i) => h("a", { className: "link-item", key: i, href: r.url, target: "_blank", rel: "noreferrer", style: { textDecoration: "none" } },
          h("div", { className: "link-item-main" }, h("span", { className: "link-t" }, r.title), h("span", { className: "link-s" }, r.url)),
          h(Icon, { name: "external", size: 14, className: "dim" }))))) : null));
}

/* ======================================================================== */
/* MODALS — new factor/system · delete                                       */
/* ======================================================================== */
function NewExposomeModal({ seed, existing, onClose, onCreate }) {
  const open = !!seed;
  // seed.parent === null → creating a System; "" → creating a factor (parent chosen below)
  const [label, setLabel] = useState("");
  const [parent, setParent] = useState("");
  const [domain, setDomain] = useState("external-specific");
  const [category, setCategory] = useState("");
  const [unit, setUnit] = useState("");
  const [desc, setDesc] = useState("");
  const [status, setStatus] = useState("draft");
  const seedSystem = seed && seed.parent === null;
  useEffect(() => { if (open) { setLabel(""); setParent(seedSystem ? "" : ""); setDomain("external-specific"); setCategory("Air quality"); setUnit(""); setDesc(""); setStatus("draft"); } }, [open]);
  if (!open) return null;
  const systemOpts = existing.filter((e) => !e.parent);
  const dup = existing.some((e) => e.label.trim().toLowerCase() === label.trim().toLowerCase());
  const slug = (seedSystem ? "exs_" : "ef_") + label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "").slice(0, 24);
  const valid = label.trim() && !dup;
  const submit = () => {
    if (!valid) return;
    onCreate({ id: slug, parent: seedSystem ? null : (parent || null), label: label.trim(), domain,
      category: category.trim() || (seedSystem ? "System" : "Uncategorized"), unit: seedSystem ? "—" : (unit.trim() || "—"),
      status, desc: desc.trim(), signals: [], phenotypes: [], genes: [], biomarkers: [], refs: [] });
  };
  return h(Modal, { open, onClose, title: seedSystem ? "New exposome system" : "New exposome factor", width: 580,
    footer: h(React.Fragment, null,
      h(Button, { variant: "ghost", onClick: onClose }, "Cancel"),
      h(Button, { variant: "primary", icon: "plus", disabled: !valid, onClick: submit }, seedSystem ? "Create system" : "Create factor")) },
    h("div", { className: "mform" },
      h(Field, { label: seedSystem ? "System name" : "Factor name",
        hint: dup ? "An entry with this name already exists." : (seedSystem ? "A grouping of exposures, e.g. “Air Quality”" : "The exposure, e.g. “Ambient Ozone”") },
        h("input", { className: "input", autoFocus: true, value: label, placeholder: seedSystem ? "e.g. Air Quality" : "e.g. Ambient Ozone", onChange: (e) => setLabel(e.target.value) })),
      !seedSystem && h(Field, { label: "Exposome system", hint: "Group this factor under an existing system (optional)" },
        h(Select, { value: parent, onChange: setParent,
          options: [{ value: "", label: "None — create as top-level system" }].concat(systemOpts.map((s) => ({ value: s.id, label: s.label }))) })),
      h("div", { className: "mform-2" },
        h(Field, { label: "Domain" },
          h(Select, { value: domain, onChange: setDomain, options: X.EXPOSOME_DOMAINS.map((d) => ({ value: d.id, label: d.label })) })),
        h(Field, { label: "Category" },
          h(Select, { value: category, onChange: setCategory, options: X.EXPOSOME_CATEGORIES }))),
      !seedSystem && h("div", { className: "mform-2" },
        h(Field, { label: "Unit of measure" },
          h("input", { className: "input mono", value: unit, placeholder: "e.g. µg/m³", onChange: (e) => setUnit(e.target.value) })),
        h(Field, { label: "Status" },
          h(Select, { value: status, onChange: setStatus, options: [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }] }))),
      seedSystem && h(Field, { label: "Status" },
        h(Select, { value: status, onChange: setStatus, options: [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }] })),
      h(Field, { label: "Definition" },
        h("textarea", { className: "input", rows: 3, value: desc, placeholder: seedSystem ? "Describe what this system groups…" : "Describe the exposure and how it affects the body…",
          style: { resize: "vertical", lineHeight: 1.5, fontFamily: "inherit" }, onChange: (e) => setDesc(e.target.value) })),
      label.trim() && h("p", { className: "mform-note" }, h(Icon, { name: "hash", size: 14 }),
        "Identifier: ", h("span", { className: "mono" }, slug)),
      !seedSystem && h("p", { className: "mform-note" }, h(Icon, { name: "sparkles", size: 14 }),
        "Map sensors, phenotypes, genes and biomarkers in the next step — the factor opens for editing right after you create it.")));
}

function DeleteExposomeModal({ factor, children, onClose, onConfirm }) {
  if (!factor) return null;
  const isSystem = !factor.parent;
  const kids = children || [];
  const total = factor.signals.length + factor.phenotypes.length + factor.genes.length + factor.biomarkers.length;
  return h(Modal, { open: true, onClose, title: isSystem ? "Delete exposome system" : "Delete exposome factor", width: 540,
    footer: h(React.Fragment, null,
      h(Button, { variant: "ghost", onClick: onClose }, "Cancel"),
      h(Button, { variant: "danger", icon: "trash", onClick: onConfirm }, isSystem ? "Delete system" : "Delete factor")) },
    h("div", { className: "mform" },
      kids.length ? h("div", { className: "warn-banner soft" }, h(Icon, { name: "subtree", size: 16 }),
        h("div", null, h("strong", null, "“" + factor.label + "”"), " is an Exposome System with ", h("strong", null, kids.length, " factor", kids.length === 1 ? "" : "s"), " (", kids.map((k) => k.label).join(", "), "). ", kids.length === 1 ? "It" : "They", " will be promoted to top-level systems.")) : null,
      h("p", { style: { margin: 0 } }, "Delete the ", isSystem ? "exposome system" : "exposome factor", " ", h("strong", null, "“" + factor.label + "”"), "?"),
      total
        ? h("div", { className: "warn-banner" }, h(Icon, { name: "alert", size: 16 }),
            h("div", null, "This removes ", h("strong", null, total, " mapping", total === 1 ? "" : "s"), " to sensors, phenotypes, genes and biomarkers. The mapped entities themselves are not affected."))
        : h("p", { className: "mform-note" }, h(Icon, { name: "info", size: 14 }), "This entry has no mappings of its own, so nothing else is affected.")));
}

/* ======================================================================== */
/* EXPORT — exposome registry to CSV / Excel                                 */
/* ======================================================================== */
const EXPO_EXPORT_COLS = ["Exposome factor", "Identifier", "Type", "Domain", "Category", "Unit", "Status",
  "Sensor signals", "Phenotypes", "Genes", "Biomarkers"];
function expoSignalLabels(keys) {
  return keys.map((k) => { const s = (X.SIGNAL_TYPES || []).find((t) => t.key === k); return s ? s.label : k; });
}
function expoRow(e) {
  return [e.label, e.id, e.parent ? "Factor" : "System", X.exposomeDomainLabel(e.domain), e.category, e.unit, e.status,
    expoSignalLabels(e.signals).join("; "),
    e.phenotypes.map((p) => X.phenoLabel(p)).join("; "),
    e.genes.map((g) => X.geneSymbol(g)).join("; "),
    e.biomarkers.map((b) => X.bmName(b)).join("; ")];
}
function expoDownloadBlob(name, mime, data) {
  const url = URL.createObjectURL(new Blob([data], { type: mime }));
  const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1500);
}
function expoExportCSV(factors) {
  const esc = (v) => { const s = String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
  const lines = [EXPO_EXPORT_COLS.map(esc).join(","), ...factors.map((e) => expoRow(e).map(esc).join(","))];
  expoDownloadBlob("eno-exposome.csv", "text/csv;charset=utf-8", "\uFEFF" + lines.join("\r\n"));
}
function expoExportXLS(factors) {
  const esc = (v) => String(v).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  const th = EXPO_EXPORT_COLS.map((c) => "<th>" + esc(c) + "</th>").join("");
  const trs = factors.map((e) => "<tr>" + expoRow(e).map((v) => "<td>" + esc(v) + "</td>").join("") + "</tr>").join("");
  const html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8"></head><body><table border="1"><thead><tr>' + th + "</tr></thead><tbody>" + trs + "</tbody></table></body></html>";
  expoDownloadBlob("eno-exposome.xls", "application/vnd.ms-excel", html);
}
function ExportExposomeModal({ open, factors, onClose }) {
  const toast = useToast();
  const [fmt, setFmt] = useState("csv");
  useEffect(() => { if (open) setFmt("csv"); }, [open]);
  const run = () => {
    if (fmt === "csv") expoExportCSV(factors); else expoExportXLS(factors);
    toast("Exported " + factors.length + " exposome entries · " + fmt.toUpperCase(), "ok");
    onClose();
  };
  const opt = (key, icon, t, s) => h("button", { type: "button", className: "fmt-opt" + (fmt === key ? " on" : ""), onClick: () => setFmt(key) },
    h(Icon, { name: icon, size: 18 }), h("div", null, h("div", { className: "fmt-opt-t" }, t), h("div", { className: "fmt-opt-s" }, s)));
  return h(Modal, { open, onClose, title: "Export exposome", width: 500,
    footer: h(React.Fragment, null,
      h(Button, { variant: "ghost", onClick: onClose }, "Cancel"),
      h(Button, { variant: "primary", icon: "download", onClick: run }, "Export " + fmt.toUpperCase())) },
    h("div", { className: "mform" },
      h(Field, { label: "Format" },
        h("div", { className: "fmt-opts" },
          opt("csv", "sheet", "CSV", "Comma-separated · opens anywhere"),
          opt("xls", "sheet", "Excel", "Formatted .xls workbook"))),
      h(Field, { label: "Includes " + factors.length + " exposome entries · " + EXPO_EXPORT_COLS.length + " columns" },
        h("div", { className: "export-cols" }, EXPO_EXPORT_COLS.map((c) => h("span", { className: "tag", key: c }, c))))));
}

Object.assign(window, { ExposomeView });
