/* ConductBiology Population genetics: one genotype file to structure, plots, tables, and a
   methods section, in the browser. Spec: docs/specs/2026-10-04-population-genetics-v1-spec.md.
   Engines live in lib/popgen; long jobs run in Web Workers through lib/popgen/pool.mjs. */

const POP_EXAMPLES = [
  { key: "microsat", label: "microsatellite example", file: "example-microsatellites.str", popmap: "example-microsatellites-popmap.txt" },
  { key: "snps", label: "SNP example", file: "example-snps.vcf", popmap: "example-snps-popmap.txt" },
];
const POP_EXAMPLE_BASE = "/population/examples/";
const POP_PRESETS = {
  quick: { label: "Quick", burnin: 5000, iterations: 10000 },
  standard: { label: "Standard", burnin: 20000, iterations: 50000 },
  publication: { label: "Publication", burnin: 100000, iterations: 100000 },
};
const POP_FORMATS = [
  { value: "structure", label: "STRUCTURE" },
  { value: "genepop", label: "Genepop" },
  { value: "genalex", label: "GenAlEx" },
  { value: "table", label: "Table (CSV or spreadsheet)" },
  { value: "vcf", label: "VCF" },
  { value: "ped", label: "PLINK PED" },
];
const POP_ACCEPT = ".str,.stru,.txt,.gen,.genepop,.csv,.tsv,.xlsx,.xls,.vcf,.gz,.ped,.map,.dat";
const POP_DEFAULT_FILTERS = { dropMonomorphic: true, maxLocusMissing: null, maxIndMissing: null, minMaf: null };

function popName(dataset, pop) {
  return typeof pop === "number" ? dataset.pops[pop] : pop;
}

function popFmt(value, digits = 3) {
  if (value === null || value === undefined || !Number.isFinite(value)) return "n/a";
  return value.toFixed(digits);
}

function popPct(value, digits = 1) {
  if (value === null || value === undefined || !Number.isFinite(value)) return "n/a";
  return `${(value * 100).toFixed(digits)}%`;
}

// Reorders Q columns so that column k of the result is column perm[k] of the input.
function popPermuteColumns(Q, K, perm) {
  if (!perm) return Q;
  const N = Q.length / K;
  const out = new Float64Array(Q.length);
  for (let i = 0; i < N; i += 1) for (let k = 0; k < K; k += 1) out[i * K + k] = Q[i * K + perm[k]];
  return out;
}

async function popSvgToPng(svg, width, height, scale = 2) {
  const img = new Image();
  const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = () => reject(new Error("Could not draw the figure.")); img.src = url; });
  const canvas = document.createElement("canvas");
  canvas.width = Math.round(width * scale);
  canvas.height = Math.round(height * scale);
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  return new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
}

function PopulationTool() {
  const engine = useToolEngine("CBPopgen");
  const pool = useToolEngine("CBPopgenPool");
  const savedId = toolSavedParam();
  if (!engine || !pool) return <div className="page-wrap tool-body"><p className="tool-muted">Loading the population genetics engine…</p></div>;
  if (savedId) return <PopSavedResults engine={engine} id={savedId} />;
  return <PopulationWorkspace engine={engine} poolLib={pool} />;
}

// /population/app?saved=<id>: figures, tables, and methods saved to the account.
function PopSavedResults({ engine, id }) {
  const saved = useSavedItem(id);
  const payload = saved.item ? saved.item.payload || {} : {};
  const record = payload.record || null;
  const files = Array.isArray(payload.files) ? payload.files : [];
  const figures = files.filter((f) => f.name.endsWith(".svg"));
  // Per-run and per-K ancestry matrices are many; they are listed for download, not drawn.
  const isRunTable = (f) => /K\d+-(run\d+|mean-aligned)\.csv$/.test(f.name);
  const tables = files.filter((f) => f.name.endsWith(".csv") && !isRunTable(f));
  const runTables = files.filter(isRunTable);
  const methods = files.find((f) => f.name === "methods.md");
  const base = toolSlug(saved.item ? saved.item.title : "", "population-genetics");
  return (
    <main>
      <div className="page-wrap">
        <div className="tool-head">
          <span className="mono" style={{ fontSize: 11, color: "var(--brand-700)", letterSpacing: ".08em", textTransform: "uppercase", fontWeight: 600 }}>ConductBiology Population genetics</span>
          <h1>{saved.item ? saved.item.title : "Saved result"}</h1>
          <p>Figures, tables, and methods saved to your account. To rerun, load the same genotype file in the tool, then the run record.</p>
        </div>
      </div>
      <div className="page-wrap tool-body">
        {saved.status === "loading" ? <p className="tool-muted">Opening the saved result…</p> : null}
        {saved.status === "error" ? (
          <ToolNotice tone="block" title="Could not open this result">
            {saved.message} <a className="btn-link" href="/saved">Back to Saved work</a>
          </ToolNotice>
        ) : null}
        {saved.status === "ready" && !record ? <ToolNotice tone="block" title="This saved item has no run record." /> : null}
        {saved.status === "ready" && record ? (
          <>
            <ToolSavedBanner item={saved.item} module="population" />
            <div className="tool-stack">
              <div className="tool-stats-row" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 12 }}>
                <ToolStat label="Input file" value={<span style={{ fontSize: 14, overflowWrap: "anywhere" }}>{record.input.fileName}</span>} sub={<span className="tool-mono">{record.input.sha256.slice(0, 12)}…</span>} />
                <ToolStat label="Individuals" value={record.data ? record.data.nInd : "n/a"} />
                <ToolStat label="Populations" value={record.data ? record.data.nPop : "n/a"} />
                <ToolStat label="Loci" value={record.data ? record.data.nLoci : "n/a"} />
              </div>
              {figures.map((f) => (
                <ToolPanel key={f.name} title={popFigureTitle(f.name)}
                           actions={<button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={() => toolDownload(f.name.split("/").pop(), f.data, "image/svg+xml")}>SVG</button>}>
                  <div style={{ overflowX: "auto" }}>
                    <img alt={popFigureTitle(f.name)} src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(f.data)}`} style={{ maxWidth: "100%", height: "auto", display: "block" }} />
                  </div>
                </ToolPanel>
              ))}
              {tables.map((f) => <PopSavedTable key={f.name} file={f} />)}
              {runTables.length ? (
                <ToolPanel title="Ancestry matrices by K and run">
                  <details>
                    <summary style={{ cursor: "pointer", padding: "12px 0", minHeight: 44, boxSizing: "border-box", fontSize: 13.5 }}>{runTables.length} CSV files (Q matrices for each run and the aligned mean per K)</summary>
                    <ul style={{ margin: "8px 0 0", padding: 0, listStyle: "none", display: "grid", gap: 2 }}>
                      {runTables.map((f) => (
                        <li key={f.name} style={{ display: "flex", gap: 10, alignItems: "center", fontSize: 13 }}>
                          <span className="tool-mono" style={{ flex: 1 }}>{f.name.split("/").pop()}</span>
                          <button type="button" className="btn btn-link" style={{ fontSize: 13, minHeight: 32 }} onClick={() => toolDownload(f.name.split("/").pop(), f.data, "text/csv")}>Download</button>
                        </li>
                      ))}
                    </ul>
                  </details>
                </ToolPanel>
              ) : null}
              {methods ? (
                <ToolPanel title="Methods">
                  <pre style={{ whiteSpace: "pre-wrap", margin: 0, fontSize: 13, lineHeight: 1.55, fontFamily: "inherit" }}>{methods.data}</pre>
                </ToolPanel>
              ) : null}
              <ToolPanel title="Files">
                <button type="button" className="btn btn-primary btn-tap" onClick={() => toolDownload(`${base}-results.zip`, new Blob([engine.zipFiles(files)], { type: "application/zip" }))}>
                  Download everything (ZIP, {files.length} files)
                </button>
              </ToolPanel>
            </div>
          </>
        ) : null}
      </div>
    </main>
  );
}

function popFigureTitle(name) {
  const base = name.split("/").pop().replace(/\.svg$/, "");
  const known = { "lnpd-by-k": "Mean ln P(D) by K", "cv-error-by-k": "Cross-validation error by K", "delta-k": "ΔK by K", "pca-pc1-pc2": "PCA", "pairwise-fst": "Pairwise FST", "upgma-tree": "UPGMA tree" };
  if (known[base]) return known[base];
  const k = /^ancestry-K(\d+)$/.exec(base);
  return k ? `Ancestry, K = ${k[1]}` : base;
}

// CSV as written by the engine's toCsv (quoted fields may hold commas).
function popCsvRows(text) {
  const rows = [];
  for (const line of String(text).split(/\r?\n/)) {
    if (line === "") continue;
    const cells = [];
    let cell = "";
    let quoted = false;
    for (let i = 0; i < line.length; i += 1) {
      const ch = line[i];
      if (quoted) {
        if (ch === '"' && line[i + 1] === '"') { cell += '"'; i += 1; }
        else if (ch === '"') quoted = false;
        else cell += ch;
      } else if (ch === '"') quoted = true;
      else if (ch === ",") { cells.push(cell); cell = ""; }
      else cell += ch;
    }
    cells.push(cell);
    rows.push(cells);
  }
  return rows;
}

function PopSavedTable({ file }) {
  const rows = popCsvRows(file.data);
  const [header, ...body] = rows;
  const shown = body.slice(0, 50);
  return (
    <ToolPanel title={file.name.split("/").pop()}
               actions={<button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={() => toolDownload(file.name.split("/").pop(), file.data, "text/csv")}>CSV</button>}>
      <div className="table-scroll">
        <table className="tool-table">
          <thead><tr>{(header || []).map((h, i) => <th key={i}>{h}</th>)}</tr></thead>
          <tbody>
            {shown.map((row, r) => (
              <tr key={r}>{row.map((cell, c) => <td key={c} className={c > 0 && cell !== "" && !Number.isNaN(Number(cell)) ? "num" : undefined}>{popCell(cell)}</td>)}</tr>
            ))}
          </tbody>
        </table>
      </div>
      {body.length > shown.length ? <p className="tool-muted" style={{ fontSize: 12, marginTop: 8 }}>First 50 of {body.length} rows. Download the CSV for all of them.</p> : null}
    </ToolPanel>
  );
}

function popCell(cell) {
  const value = Number(cell);
  if (cell === "" || Number.isNaN(value) || Number.isInteger(value)) return cell;
  return Math.abs(value) >= 1000 || Math.abs(value) < 0.001 ? value.toPrecision(4) : value.toFixed(4);
}

function PopulationWorkspace({ engine, poolLib }) {
  const { useState, useMemo, useRef } = React;
  const [step, setStep] = useState("data");
  const [source, setSource] = useState(null);       // { name, size, sha256, kind, text, rows, mapText }
  const [formatOverride, setFormatOverride] = useState(null);
  const [parseOptions, setParseOptions] = useState(null);
  const [popMap, setPopMap] = useState(null);        // { name, sha256, map, errors }
  const [popOrder, setPopOrder] = useState(null);    // array of names in display order
  const [filters, setFilters] = useState(POP_DEFAULT_FILTERS);
  const [settings, setSettings] = useState(null);
  const [run, setRun] = useState({ status: "idle" });
  const [results, setResults] = useState(null);
  const [rerun, setRerun] = useState(null);
  const [busy, setBusy] = useState(null);
  const [error, setError] = useState(null);
  const poolRef = useRef(null);

  const parsed = useMemo(() => {
    if (!source) return null;
    try {
      const input = source.rows || source.text;
      const options = { ...(parseOptions || {}), ...(source.mapText ? { mapText: source.mapText } : {}) };
      return engine.parseGenotypes(input, { fileName: source.name, format: formatOverride || undefined, options: Object.keys(options).length ? options : undefined });
    } catch (err) {
      return { dataset: null, errors: [err.message || String(err)], warnings: [] };
    }
  }, [engine, source, formatOverride, parseOptions]);

  const mapped = useMemo(() => {
    if (!parsed || !parsed.dataset) return null;
    if (!popMap || !popMap.map) return { dataset: parsed.dataset, warnings: [] };
    return engine.applyPopMap(parsed.dataset, popMap.map);
  }, [engine, parsed, popMap]);

  const ordered = useMemo(() => {
    if (!mapped) return null;
    const ds = mapped.dataset;
    if (!popOrder) return ds;
    const order = popOrder.map((name) => ds.pops.indexOf(name)).filter((i) => i >= 0);
    if (order.length !== ds.pops.length) return ds;
    return engine.reorderPops(ds, order);
  }, [engine, mapped, popOrder]);

  const filtered = useMemo(() => (ordered ? engine.filterDataset(ordered, filters) : null), [engine, ordered, filters]);
  const summary = useMemo(() => (filtered ? engine.summarize(filtered.dataset) : null), [engine, filtered]);
  const rawSummary = useMemo(() => (ordered ? engine.summarize(ordered) : null), [engine, ordered]);

  const effectiveSettings = useMemo(() => {
    if (!summary) return null;
    const auto = summary.nLoci >= 1000 ? "maxlik" : "bayesian";
    const base = settings || {
      analyses: { diversity: true, hwe: true, fstats: true, pairwise: true, nei: true, pca: true, ancestry: true },
      hwePermutations: 10000,
      fstPermutations: 1000,
      ancestry: { model: "auto", kMin: 1, kMax: Math.min(10, Math.max(2, summary.nPop + 2)), reps: null, preset: "quick", burnin: 5000, iterations: 10000, cvFolds: 5, tol: 1e-4, maxIter: 10000, seed: 20261004 },
    };
    const model = base.ancestry.model === "auto" ? auto : base.ancestry.model;
    const reps = base.ancestry.reps ?? (model === "bayesian" ? 10 : 5);
    return { ...base, ancestry: { ...base.ancestry, resolvedModel: model, autoModel: auto, resolvedReps: reps } };
  }, [settings, summary]);

  const steps = [
    { key: "data", label: "Data", done: Boolean(parsed && parsed.dataset) },
    { key: "check", label: "Check", done: Boolean(results), disabled: !(parsed && parsed.dataset) },
    { key: "analyze", label: "Analyze", done: run.status === "done", disabled: !(filtered && filtered.dataset.individuals.length) },
    { key: "results", label: "Results", disabled: !results },
  ];

  async function loadGenotypeFiles(fileList) {
    setError(null);
    setBusy("Reading the file…");
    try {
      const files = Array.from(fileList);
      const main = files.find((f) => !/\.map$/i.test(f.name)) || files[0];
      const mapFile = files.find((f) => /\.map$/i.test(f.name) && f !== main);
      const read = await toolReadFile(main);
      const mapText = mapFile ? (await toolReadFile(mapFile)).text : null;
      setSource({ ...read, mapText });
      setFormatOverride(null);
      setParseOptions(null);
      setPopOrder(null);
      setFilters(POP_DEFAULT_FILTERS);
      setResults(null);
      setRun({ status: "idle" });
      setRerun(null);
    } catch (err) {
      setError(err.message || String(err));
    } finally {
      setBusy(null);
    }
  }

  async function loadPopMap(fileList) {
    setError(null);
    try {
      const read = await toolReadFile(fileList[0]);
      const parsedMap = engine.parsePopMap(read.text);
      setPopMap({ name: read.name, sha256: read.sha256, map: parsedMap.map, errors: parsedMap.errors || [] });
      setPopOrder(null);
      setResults(null);
    } catch (err) {
      setError(err.message || String(err));
    }
  }

  async function loadExample(example) {
    setError(null);
    setBusy(`Loading the ${example.label}…`);
    try {
      const file = await toolFetchAsFile(POP_EXAMPLE_BASE + example.file, example.file);
      const mapFile = await toolFetchAsFile(POP_EXAMPLE_BASE + example.popmap, example.popmap);
      const read = await toolReadFile(file);
      const mapRead = await toolReadFile(mapFile);
      const parsedMap = engine.parsePopMap(mapRead.text);
      setSource({ ...read, mapText: null });
      setPopMap({ name: mapRead.name, sha256: mapRead.sha256, map: parsedMap.map, errors: parsedMap.errors || [] });
      setFormatOverride(null);
      setParseOptions(null);
      setPopOrder(null);
      setFilters(POP_DEFAULT_FILTERS);
      setSettings(null);
      setResults(null);
      setRun({ status: "idle" });
      setRerun(null);
      setStep("check");
    } catch (err) {
      setError(err.message || String(err));
    } finally {
      setBusy(null);
    }
  }

  function cancelRun() {
    if (poolRef.current) poolRef.current.cancel();
  }

  async function startRun(overrides = null) {
    const s = overrides || effectiveSettings;
    const dataset = filtered.dataset;
    setError(null);
    setResults(null);
    const startedAt = Date.now();
    setRun({ status: "running", startedAt, jobs: [], done: 0 });
    setStep("analyze");
    const pool = poolLib.createPool({ dataset });
    poolRef.current = pool;
    const a = s.ancestry;
    const jobs = [];
    const add = (key, kind, params, weight, label, meta = {}) => jobs.push({ key, kind, params, weight, label, meta });
    const N = dataset.individuals.length;
    const L = dataset.loci.length;
    if (s.analyses.diversity) add("diversity", "diversity", {}, 1, "Diversity");
    if (s.analyses.fstats) add("fstats", "fstats", {}, 1, "F-statistics");
    if (s.analyses.nei) add("nei", "nei", {}, 1, "Nei's distance");
    if (s.analyses.pca) add("pca", "pca", { components: 10, maxLoci: 20000, seed: a.seed }, 2, "PCA");
    if (s.analyses.hwe) add("hwe", "hwe", { permutations: s.hwePermutations, seed: a.seed }, Math.max(1, (s.hwePermutations * N * L) / 2e7), "Hardy-Weinberg tests");
    if (s.analyses.pairwise) add("pairwise", "pairwise-fst", { permutations: s.fstPermutations, seed: a.seed }, Math.max(1, (s.fstPermutations * N * L) / 2e7), "Pairwise FST");
    if (s.analyses.ancestry) {
      const model = a.resolvedModel;
      const reps = a.resolvedReps;
      const ks = [];
      for (let K = a.kMin; K <= a.kMax; K += 1) ks.push(K);
      let index = 0;
      const runs = [];
      for (const K of ks) {
        for (let rep = 0; rep < reps; rep += 1) {
          const seed = engine.runSeed(a.seed, index);
          index += 1;
          const params = model === "bayesian"
            ? { K, burnin: a.burnin, iterations: a.iterations, seed }
            : { K, seed, tol: a.tol, maxIter: a.maxIter, cvFolds: a.cvFolds };
          const weight = model === "bayesian" ? (K * (a.burnin + a.iterations) * N * L) / 2e7 : (K * N * L * (1 + a.cvFolds)) / 2e5;
          runs.push({ key: `run-${K}-${rep}`, kind: model, params, weight: Math.max(1, weight), label: `K = ${K}, run ${rep + 1}`, meta: { K, rep, seed } });
        }
      }
      runs.sort((x, y) => y.meta.K - x.meta.K);
      jobs.push(...runs);
    }
    const totalWeight = jobs.reduce((n, j) => n + j.weight, 0);
    const progress = new Float64Array(jobs.length);
    let lastPaint = 0;
    const paint = (force) => {
      const now = Date.now();
      if (!force && now - lastPaint < 150) return;
      lastPaint = now;
      let doneWeight = 0;
      jobs.forEach((j, i) => { doneWeight += progress[i] * j.weight; });
      setRun((r) => (r.status === "running" ? { ...r, fraction: doneWeight / totalWeight, done: progress.filter((p) => p >= 1).length, total: jobs.length } : r));
    };
    const out = {};
    try {
      await Promise.all(jobs.map((job, i) => pool.run(job.kind, job.params, (done, total) => { progress[i] = total ? Math.min(0.999, done / total) : progress[i]; paint(false); })
        .then((result) => { progress[i] = 1; out[job.key] = job.meta.K !== undefined ? { ...result, meta: job.meta } : result; paint(true); })));
      pool.cancel();
      poolRef.current = null;
      const assembled = popAssemble(engine, dataset, s, out);
      const record = await popRunRecord(engine, { source, parsed, popMap, ordered: popOrder || dataset.pops, filters, filtered, summary, settings: s, results: assembled });
      setResults({ ...assembled, record, dataset });
      setRun({ status: "done", elapsedMs: Date.now() - startedAt });
      setStep("results");
      return { ...assembled, record };
    } catch (err) {
      pool.cancel();
      poolRef.current = null;
      if (err && err.name === "AbortError") setRun({ status: "cancelled" });
      else { setRun({ status: "error" }); setError(err.message || String(err)); }
      return null;
    }
  }

  async function loadRunRecord(fileList) {
    setError(null);
    try {
      const read = await toolReadFile(fileList[0]);
      const record = JSON.parse(read.text);
      if (record.kind !== "conductbiology.population.run") throw new Error("That file is not a run record from this tool.");
      if (!source || record.input.sha256 !== source.sha256) {
        setRerun({ status: "mismatch", record, message: `Load ${record.input.fileName} (SHA-256 ${record.input.sha256.slice(0, 12)}…) first, then the run record.` });
        return;
      }
      setFormatOverride(record.input.format);
      setParseOptions(record.input.options || null);
      setPopOrder(record.populations.order);
      setFilters(record.filters);
      setSettings(record.settings);
      setRerun({ status: "ready", record, message: "Settings loaded from the run record. Run the analysis again to compare." });
      setStep("analyze");
    } catch (err) {
      setError(err.message || String(err));
    }
  }

  // /population/app?example=microsat (or snps) opens straight into an example.
  React.useEffect(() => {
    const key = new URLSearchParams(window.location.search).get("example");
    const example = POP_EXAMPLES.find((ex) => ex.key === key) || (key ? POP_EXAMPLES[0] : null);
    if (example) loadExample(example);
  }, []);

  const ds = filtered ? filtered.dataset : null;

  return (
    <main>
      <div className="page-wrap">
        <div className="tool-head">
          <span className="mono" style={{ fontSize: 11, color: "var(--brand-700)", letterSpacing: ".08em", textTransform: "uppercase", fontWeight: 600 }}>ConductBiology Population genetics</span>
          <h1>{source ? source.name : "From genotype file to population structure"}</h1>
          <p>Diversity, Hardy-Weinberg, F-statistics, PCA, ancestry runs over a range of K, ΔK, aligned bar plots, and a methods section, from one file.</p>
          <div className="tool-actions no-print">
            {POP_EXAMPLES.map((ex) => (
              <button key={ex.key} type="button" className="btn btn-ghost btn-tap" disabled={Boolean(busy) || run.status === "running"} onClick={() => loadExample(ex)}>Try the {ex.label}</button>
            ))}
            {busy ? <span className="tool-muted" style={{ fontSize: 13 }}>{busy}</span> : null}
          </div>
          <ToolPrivacyLine />
        </div>
        <div className="no-print"><ToolSteps steps={steps} current={step} onSelect={setStep} /></div>
      </div>

      <div className="page-wrap tool-body">
        {error ? <div style={{ marginBottom: 16 }}><ToolNotice tone="block" title="Something needs attention">{error}</ToolNotice></div> : null}

        {step === "data" ? (
          <PopDataStep engine={engine} source={source} parsed={parsed} mapped={mapped} popMap={popMap} formatOverride={formatOverride}
                       setFormatOverride={(f) => { setFormatOverride(f); setParseOptions(null); setResults(null); }}
                       parseOptions={parseOptions} setParseOptions={(o) => { setParseOptions(o); setResults(null); }}
                       onFiles={loadGenotypeFiles} onPopMap={loadPopMap} onClearPopMap={() => { setPopMap(null); setPopOrder(null); }}
                       onNext={() => setStep("check")} busy={busy} />
        ) : null}

        {step === "check" && ordered ? (
          <PopCheckStep ordered={ordered} rawSummary={rawSummary} filtered={filtered} summary={summary} filters={filters}
                        setFilters={(f) => { setFilters(f); setResults(null); }}
                        setPopOrder={(o) => { setPopOrder(o); setResults(null); }} onNext={() => setStep("analyze")} />
        ) : null}

        {step === "analyze" && ds ? (
          <PopAnalyzeStep summary={summary} settings={effectiveSettings} setSettings={setSettings} run={run} onRun={() => startRun()} onCancel={cancelRun}
                          rerun={rerun} />
        ) : null}

        {step === "results" && results ? (
          <PopResults engine={engine} results={results} onRerunRecord={loadRunRecord} rerun={rerun} setRerun={setRerun} />
        ) : null}
      </div>
    </main>
  );
}

/* Assembling results ------------------------------------------------------- */

function popAssemble(engine, dataset, s, out) {
  const N = dataset.individuals.length;
  const res = {
    settings: s,
    diversity: out.diversity || null,
    fstats: out.fstats || null,
    nei: out.nei || null,
    pca: out.pca || null,
    hwe: out.hwe || null,
    pairwise: out.pairwise || null,
    upgma: null,
    ancestry: null,
  };
  if (res.nei && dataset.pops.length >= 2) res.upgma = engine.upgma(res.nei.d, res.nei.pops || dataset.pops);
  if (s.analyses.ancestry) {
    const model = s.ancestry.resolvedModel;
    const runs = Object.values(out).filter((r) => r && r.meta && r.meta.K !== undefined);
    const ks = [...new Set(runs.map((r) => r.meta.K))].sort((a, b) => a - b);
    const byK = ks.map((K) => {
      const list = runs.filter((r) => r.meta.K === K).sort((a, b) => a.meta.rep - b.meta.rep);
      const aligned = engine.alignRuns(list.map((r) => ({ Q: r.Q, K })), N, { orders: 20, seed: s.ancestry.seed });
      return {
        K,
        runs: list.map((r) => ({ rep: r.meta.rep, seed: r.meta.seed, lnPD: r.lnPD ?? null, meanLnL: r.meanLnL ?? null, varLnL: r.varLnL ?? null, alpha: r.alpha ?? null, logLik: r.logLik ?? null, cvError: r.cvError ?? null, iterations: r.iterations ?? null, converged: r.converged ?? null, elapsedMs: r.elapsedMs ?? null, Q: r.Q })),
        meanQ: aligned.meanQ,
        hPrime: aligned.hPrime ?? null,
        perm: aligned.perm,
      };
    });
    const colorPerms = engine.alignAcrossK(byK.map((k) => ({ K: k.K, Q: k.meanQ })));
    for (const k of byK) k.displayQ = popPermuteColumns(k.meanQ, k.K, colorPerms ? colorPerms[k.K] : null);
    let choice = null;
    if (model === "bayesian") {
      const ev = engine.evanno(byK.map((k) => ({ K: k.K, lnPD: k.runs.map((r) => r.lnPD) })));
      choice = { kind: "evanno", rows: ev.rows || ev, bestK: ev.bestK ?? null };
    } else {
      const cv = engine.cvSummary(byK.map((k) => ({ K: k.K, cv: k.runs.map((r) => r.cvError).filter((v) => v !== null && v !== undefined) })));
      choice = { kind: "cv", rows: cv.rows, bestK: cv.bestK ?? null };
    }
    res.ancestry = { model, byK, choice };
  }
  return res;
}

function popSummaryForRecord(results) {
  const summary = {};
  if (results.hwe) {
    const tested = results.hwe.filter((r) => r.p !== null && r.p !== undefined);
    summary.hwe = { tests: tested.length, significant: tested.filter((r) => r.q !== null && r.q < 0.05).length };
  }
  if (results.fstats) summary.fstats = results.fstats.overall;
  if (results.pairwise) {
    const values = [];
    results.pairwise.fst.forEach((row, i) => row.forEach((v, j) => { if (j < i && Number.isFinite(v)) values.push(v); }));
    if (values.length) summary.pairwise = { min: Math.min(...values), max: Math.max(...values) };
  }
  if (results.pca) summary.pca = { explained: results.pca.explained, usedLoci: results.pca.usedLoci };
  if (results.ancestry) {
    const a = results.ancestry;
    summary.ancestry = {
      model: a.model,
      bestKDeltaK: a.choice.kind === "evanno" ? a.choice.bestK : null,
      bestKCv: a.choice.kind === "cv" ? a.choice.bestK : null,
      hPrime: a.byK.map((k) => ({ K: k.K, hPrime: k.hPrime })),
    };
  }
  return summary;
}

// Numbers that must match when a run is repeated: rounded so harmless last-bit
// differences between browser engines do not count as a change.
function popResultsDigest(results) {
  const r9 = (v) => (v === null || v === undefined || !Number.isFinite(v) ? null : Number(v.toFixed(9)));
  const arr = (a) => Array.from(a || [], r9);
  return {
    fstats: results.fstats ? { overall: results.fstats.overall, perLocus: results.fstats.perLocus.map((l) => [r9(l.fst), r9(l.fis), r9(l.fit)]) } : null,
    pairwise: results.pairwise ? { fst: results.pairwise.fst.map((row) => row.map(r9)), p: results.pairwise.p ? results.pairwise.p.map((row) => row.map(r9)) : null } : null,
    hwe: results.hwe ? results.hwe.map((h) => [h.pop, h.locus, r9(h.p)]) : null,
    pca: results.pca ? arr(results.pca.eigenvalues) : null,
    ancestry: results.ancestry ? results.ancestry.byK.map((k) => ({ K: k.K, runs: k.runs.map((run) => ({ lnPD: r9(run.lnPD), logLik: r9(run.logLik), cv: r9(run.cvError), Q: arr(run.Q) })) })) : null,
  };
}

async function popRunRecord(engine, { source, parsed, popMap, ordered, filters, filtered, summary, settings, results }) {
  const data = {
    nInd: summary.nInd,
    nPop: summary.nPop,
    nLoci: summary.nLoci,
    nAlleles: summary.nAlleles,
    missingRate: summary.missingRate,
    popSizes: summary.popSizes.map((p) => ({ name: popName(filtered.dataset, p.pop), n: p.n })),
  };
  const digest = await window.CBHash.hashJson(popResultsDigest(results));
  // Record what actually ran: the resolved model and run count, not "auto" or a blank.
  const { resolvedModel, autoModel, resolvedReps, ...ancestry } = settings.ancestry;
  const recorded = { ...settings, ancestry: { ...ancestry, model: resolvedModel, reps: resolvedReps } };
  return {
    kind: "conductbiology.population.run",
    tool: { name: engine.TOOL_NAME, version: engine.TOOL_VERSION },
    createdAt: new Date().toISOString(),
    input: { fileName: source.name, size: source.size, sha256: source.sha256, format: parsed.format, options: parsed.options || null, popMap: popMap ? { fileName: popMap.name, sha256: popMap.sha256 } : null },
    populations: { order: [...filtered.dataset.pops] },
    filters,
    removed: filtered.removed,
    settings: recorded,
    data,
    summary: popSummaryForRecord(results),
    resultsDigest: digest,
  };
}

/* Step 1: data ---------------------------------------------------------------- */

function PopDataStep({ source, parsed, mapped, popMap, formatOverride, setFormatOverride, parseOptions, setParseOptions, onFiles, onPopMap, onClearPopMap, onNext, busy }) {
  const ds = parsed && parsed.dataset;
  const opts = parsed && parsed.options;
  return (
    <div className="tool-grid">
      <div className="tool-stack">
        <ToolDrop title="Genotype file" accept={POP_ACCEPT} multiple disabled={Boolean(busy)} onFiles={onFiles}
                  hint="STRUCTURE, Genepop, GenAlEx, a CSV or spreadsheet with two columns per locus, VCF (also .vcf.gz), or PLINK .ped (add the .map file too). Diploid data." />
        {source ? (
          <div className="tool-file" style={{ marginTop: 0 }}>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div className="tool-file-name">{source.name}</div>
              <div className="tool-file-meta">{toolFmtBytes(source.size)} · SHA-256 {source.sha256.slice(0, 12)}…{source.sheetName ? ` · sheet "${source.sheetName}"` : ""}</div>
            </div>
          </div>
        ) : null}

        {parsed ? (
          parsed.dataset ? (
            <ToolPanel title="What we found">
              <div className="tool-stat-row">
                <ToolStat label="Format" value={<span style={{ fontSize: 17 }}>{POP_FORMATS.find((f) => f.value === parsed.format)?.label || parsed.format}</span>} />
                <ToolStat label="Individuals" value={ds.individuals.length} />
                <ToolStat label="Populations" value={(mapped ? mapped.dataset : ds).pops.length} />
                <ToolStat label="Loci" value={ds.loci.length.toLocaleString()} />
              </div>
              {[...(parsed.warnings || []), ...((mapped && mapped.warnings) || [])].length ? (
                <div style={{ marginTop: 12 }}><ToolNotice tone="warn" title="Notes from reading the file" items={[...(parsed.warnings || []), ...((mapped && mapped.warnings) || [])].map((w) => w.message || w)} /></div>
              ) : null}
              <div style={{ display: "flex", gap: 10, marginTop: 14 }}>
                <button type="button" className="btn btn-primary btn-tap" onClick={onNext}>Check the data</button>
              </div>
            </ToolPanel>
          ) : (
            <ToolNotice tone="block" title="The file could not be read" items={(parsed.errors || []).map((e) => e.message || e)}>
              Check the format below, or the file itself.
            </ToolNotice>
          )
        ) : null}
      </div>

      <div className="tool-stack">
        <ToolPanel title="Populations">
          <p style={{ margin: "0 0 10px", fontSize: 13.5, color: "var(--ink-700)", lineHeight: 1.5 }}>
            Optional for STRUCTURE, Genepop, GenAlEx, and tables, which carry populations. Needed for VCF. Two columns: sample and population.
          </p>
          <ToolDrop title="Population map" accept=".txt,.csv,.tsv,.popmap" onFiles={onPopMap} />
          {popMap ? (
            <div className="tool-file">
              <div style={{ minWidth: 0, flex: 1 }}>
                <div className="tool-file-name">{popMap.name}</div>
                <div className="tool-file-meta">{popMap.map.size} samples{popMap.errors.length ? ` · ${popMap.errors.length} problems` : ""}</div>
              </div>
              <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={onClearPopMap}>Remove</button>
            </div>
          ) : null}
        </ToolPanel>
        {source ? (
          <ToolPanel title="Reading options">
            <div style={{ display: "grid", gap: 12 }}>
              <label className="tool-field"><span>Format</span>
                <select className="tool-input" value={formatOverride || (parsed && parsed.format) || ""} onChange={(e) => setFormatOverride(e.target.value)}>
                  {POP_FORMATS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
                </select>
              </label>
              {parsed && parsed.format === "structure" && opts ? (
                <>
                  <label className="tool-field"><span>Rows per individual</span>
                    <select className="tool-input" value={opts.rowsPerIndividual} onChange={(e) => setParseOptions({ ...opts, rowsPerIndividual: Number(e.target.value) })}>
                      <option value={1}>One row (two columns per locus)</option>
                      <option value={2}>Two rows (one column per locus)</option>
                    </select>
                  </label>
                  <label className="tool-check"><input type="checkbox" checked={Boolean(opts.hasHeader)} onChange={(e) => setParseOptions({ ...opts, hasHeader: e.target.checked })} />First line holds locus names</label>
                  <label className="tool-check"><input type="checkbox" checked={Boolean(opts.hasPopColumn)} onChange={(e) => setParseOptions({ ...opts, hasPopColumn: e.target.checked })} />Second column is the population (PopData)</label>
                  <label className="tool-field"><span>Other columns before the genotypes</span>
                    <input className="tool-input" type="number" min={0} max={6} value={opts.extraColumns ?? 0} onChange={(e) => setParseOptions({ ...opts, extraColumns: Number(e.target.value) })} />
                    <small>For example PopFlag or LocData columns.</small>
                  </label>
                </>
              ) : null}
            </div>
          </ToolPanel>
        ) : null}
      </div>
    </div>
  );
}

/* Step 2: check --------------------------------------------------------------- */

function PopCheckStep({ ordered, rawSummary, filtered, summary, filters, setFilters, setPopOrder, onNext }) {
  const [showLoci, setShowLoci] = React.useState(200);
  const pops = ordered.pops;
  const move = (index, delta) => {
    const next = [...pops];
    const target = index + delta;
    if (target < 0 || target >= next.length) return;
    [next[index], next[target]] = [next[target], next[index]];
    setPopOrder(next);
  };
  const pctInput = (value) => (value === null || value === undefined ? "" : Math.round(value * 1000) / 10);
  const fromPct = (text) => (text === "" ? null : Math.max(0, Math.min(100, Number(text))) / 100);
  const removed = filtered.removed;
  const sizes = rawSummary.popSizes;
  return (
    <div className="tool-stack">
      <div className="tool-stat-row">
        <ToolStat label="Individuals" value={summary.nInd} sub={rawSummary.nInd !== summary.nInd ? `of ${rawSummary.nInd}` : null} />
        <ToolStat label="Populations" value={summary.nPop} />
        <ToolStat label="Loci" value={summary.nLoci.toLocaleString()} sub={rawSummary.nLoci !== summary.nLoci ? `of ${rawSummary.nLoci.toLocaleString()}` : null} />
        <ToolStat label="Alleles" value={summary.nAlleles.toLocaleString()} />
        <ToolStat label="Missing" value={popPct(summary.missingRate)} sub="of genotypes" />
      </div>
      <div className="grid-2" style={{ alignItems: "start" }}>
        <ToolPanel title="Population order">
          <p style={{ margin: "0 0 10px", fontSize: 13.5, color: "var(--ink-700)" }}>Plots and tables follow this order.</p>
          <ol style={{ listStyle: "none", margin: 0, padding: 0, display: "grid", gap: 6 }}>
            {pops.map((name, i) => (
              <li key={name} style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", border: "1px solid var(--ink-200)", borderRadius: 8, background: "white" }}>
                <span style={{ flex: 1, fontWeight: 500 }}>{name}</span>
                <span className="tool-muted" style={{ fontSize: 12.5 }}>n = {sizes.find((s) => (typeof s.pop === "number" ? pops[s.pop] : s.pop) === name)?.n ?? "?"}</span>
                <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36, minWidth: 40 }} aria-label={`Move ${name} up`} disabled={i === 0} onClick={() => move(i, -1)}>↑</button>
                <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36, minWidth: 40 }} aria-label={`Move ${name} down`} disabled={i === pops.length - 1} onClick={() => move(i, 1)}>↓</button>
              </li>
            ))}
          </ol>
        </ToolPanel>
        <ToolPanel title="Filters">
          <div style={{ display: "grid", gap: 12 }}>
            <label className="tool-check"><input type="checkbox" checked={filters.dropMonomorphic} onChange={(e) => setFilters({ ...filters, dropMonomorphic: e.target.checked })} />Drop loci with one allele</label>
            <label className="tool-field"><span>Drop loci missing in more than</span>
              <div style={{ display: "flex", gap: 8, alignItems: "center" }}><input className="tool-input" style={{ maxWidth: 120 }} type="number" min={0} max={100} placeholder="off" value={pctInput(filters.maxLocusMissing)} onChange={(e) => setFilters({ ...filters, maxLocusMissing: fromPct(e.target.value) })} /><span className="tool-muted">% of individuals</span></div>
            </label>
            <label className="tool-field"><span>Drop individuals missing more than</span>
              <div style={{ display: "flex", gap: 8, alignItems: "center" }}><input className="tool-input" style={{ maxWidth: 120 }} type="number" min={0} max={100} placeholder="off" value={pctInput(filters.maxIndMissing)} onChange={(e) => setFilters({ ...filters, maxIndMissing: fromPct(e.target.value) })} /><span className="tool-muted">% of loci</span></div>
            </label>
            <label className="tool-field"><span>Minor allele frequency at least (biallelic loci)</span>
              <input className="tool-input" style={{ maxWidth: 120 }} type="number" min={0} max={0.5} step={0.01} placeholder="off" value={filters.minMaf ?? ""} onChange={(e) => setFilters({ ...filters, minMaf: e.target.value === "" ? null : Number(e.target.value) })} />
            </label>
            {removed.loci.length || removed.individuals.length ? (
              <ToolNotice tone="info" title="Removed"
                          items={[removed.individuals.length ? `${removed.individuals.length} individual${removed.individuals.length === 1 ? "" : "s"}: ${removed.individuals.slice(0, 6).map((x) => x.id).join(", ")}${removed.individuals.length > 6 ? "…" : ""}` : null,
                                  removed.loci.length ? `${removed.loci.length} loc${removed.loci.length === 1 ? "us" : "i"}: ${removed.loci.slice(0, 6).map((x) => `${x.name} (${x.text || x.reason})`).join(", ")}${removed.loci.length > 6 ? "…" : ""}` : null].filter(Boolean)} />
            ) : null}
          </div>
        </ToolPanel>
      </div>
      <ToolPanel title="Loci" actions={<button type="button" className="btn btn-primary btn-sm" onClick={onNext}>Set up the analysis</button>}>
        <div className="table-scroll" style={{ maxHeight: 420, overflowY: "auto", border: "1px solid var(--ink-200)", borderRadius: 8 }}>
          <table className="tool-table">
            <thead><tr><th>Locus</th><th className="num">Alleles</th><th className="num">Missing</th><th className="num">MAF</th><th>Note</th></tr></thead>
            <tbody>
              {summary.perLocus.slice(0, showLoci).map((l) => (
                <tr key={l.name}><td className="tool-mono">{l.name}</td><td className="num">{l.alleles}</td><td className="num">{popPct(l.missingRate)}</td><td className="num">{l.maf === null || l.maf === undefined ? "" : popFmt(l.maf, 3)}</td><td className="tool-muted">{l.monomorphic ? "one allele" : ""}</td></tr>
              ))}
            </tbody>
          </table>
        </div>
        {summary.perLocus.length > showLoci ? <button type="button" className="btn btn-link btn-tap" onClick={() => setShowLoci(showLoci + 1000)}>Show more ({summary.perLocus.length - showLoci} left)</button> : null}
      </ToolPanel>
    </div>
  );
}

/* Step 3: analyze ------------------------------------------------------------- */

function PopAnalyzeStep({ summary, settings, setSettings, run, onRun, onCancel, rerun }) {
  const s = settings;
  const a = s.ancestry;
  const set = (patch) => setSettings({ ...s, ...patch });
  const setA = (patch) => setSettings({ ...s, ancestry: { ...a, ...patch } });
  const setAn = (key, value) => setSettings({ ...s, analyses: { ...s.analyses, [key]: value } });
  const running = run.status === "running";
  const ks = Math.max(0, a.kMax - a.kMin + 1);
  const totalRuns = s.analyses.ancestry ? ks * a.resolvedReps : 0;
  const preset = Object.entries(POP_PRESETS).find(([, p]) => p.burnin === a.burnin && p.iterations === a.iterations)?.[0] || "custom";
  return (
    <div className="tool-grid">
      <div className="tool-stack">
        {rerun && rerun.message ? <ToolNotice tone={rerun.status === "mismatch" ? "warn" : "info"}>{rerun.message}</ToolNotice> : null}
        <ToolPanel title="Analyses">
          <div className="tool-form-grid">
            {[["diversity", "Genetic diversity per population"], ["hwe", "Hardy-Weinberg tests"], ["fstats", "F-statistics (Weir and Cockerham)"], ["pairwise", "Pairwise FST with permutations"], ["nei", "Nei's distance and UPGMA tree"], ["pca", "PCA of individuals"], ["ancestry", "Ancestry over a range of K"]].map(([key, label]) => (
              <label key={key} className="tool-check"><input type="checkbox" checked={s.analyses[key]} disabled={running} onChange={(e) => setAn(key, e.target.checked)} />{label}</label>
            ))}
          </div>
          <div className="tool-form-grid" style={{ marginTop: 10 }}>
            <label className="tool-field"><span>Hardy-Weinberg permutations</span>
              <input className="tool-input" type="number" min={0} step={1000} value={s.hwePermutations} disabled={running || !s.analyses.hwe} onChange={(e) => set({ hwePermutations: Math.max(0, Number(e.target.value)) })} />
              <small>Used for loci with three or more alleles.</small>
            </label>
            <label className="tool-field"><span>Pairwise FST permutations</span>
              <input className="tool-input" type="number" min={0} step={100} value={s.fstPermutations} disabled={running || !s.analyses.pairwise} onChange={(e) => set({ fstPermutations: Math.max(0, Number(e.target.value)) })} />
            </label>
          </div>
        </ToolPanel>

        {s.analyses.ancestry ? (
          <ToolPanel title="Ancestry">
            <div style={{ display: "grid", gap: 14 }}>
              <label className="tool-field"><span>Model</span>
                <select className="tool-input" value={a.model} disabled={running} onChange={(e) => setA({ model: e.target.value, reps: null })}>
                  <option value="auto">Automatic ({a.autoModel === "bayesian" ? "Bayesian" : "maximum likelihood"} for {summary.nLoci.toLocaleString()} loci)</option>
                  <option value="bayesian">Bayesian admixture model (as in STRUCTURE)</option>
                  <option value="maxlik">Maximum-likelihood admixture model (as in ADMIXTURE)</option>
                </select>
                <small>{a.resolvedModel === "bayesian" ? "MCMC with independent allele frequencies and α inferred. Gives ln P(D) and the Evanno ΔK." : "EM with cross-validation. Fast for thousands of loci; choose K by the lowest cross-validation error."}</small>
                {a.resolvedModel === "maxlik" && summary.nLoci < 1000 ? <small style={{ color: "var(--warn)" }}>With few loci and many rare alleles, as in microsatellites, cross-validation error tends to favor too few clusters. The Bayesian model with ΔK suits these data better.</small> : null}
              </label>
              <div className="tool-form-grid">
                <label className="tool-field"><span>K from</span><input className="tool-input" type="number" min={1} max={a.kMax} value={a.kMin} disabled={running} onChange={(e) => setA({ kMin: Math.max(1, Math.min(Number(e.target.value), a.kMax)) })} /></label>
                <label className="tool-field"><span>K to</span><input className="tool-input" type="number" min={a.kMin} max={10} value={a.kMax} disabled={running} onChange={(e) => setA({ kMax: Math.max(a.kMin, Math.min(10, Number(e.target.value))) })} /></label>
                <label className="tool-field"><span>Runs per K</span><input className="tool-input" type="number" min={1} max={50} value={a.resolvedReps} disabled={running} onChange={(e) => setA({ reps: Math.max(1, Math.min(50, Number(e.target.value))) })} />
                  <small>{a.resolvedModel === "bayesian" ? "ΔK needs at least 2." : "Independent random starts."}</small></label>
                <label className="tool-field"><span>Seed</span><input className="tool-input" type="number" value={a.seed} disabled={running} onChange={(e) => setA({ seed: Math.floor(Number(e.target.value) || 0) })} /></label>
              </div>
              {a.resolvedModel === "bayesian" ? (
                <>
                  <div className="tool-field"><span>Run length</span>
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }} role="group" aria-label="Run length">
                      {Object.entries(POP_PRESETS).map(([key, p]) => (
                        <button key={key} type="button" className={`tool-tab`} aria-selected={preset === key} disabled={running}
                                onClick={() => setA({ burnin: p.burnin, iterations: p.iterations })}>{p.label} · {(p.burnin / 1000)}k + {(p.iterations / 1000)}k</button>
                      ))}
                    </div>
                  </div>
                  <div className="tool-form-grid">
                    <label className="tool-field"><span>Burn-in iterations</span><input className="tool-input" type="number" min={100} step={1000} value={a.burnin} disabled={running} onChange={(e) => setA({ burnin: Math.max(100, Number(e.target.value)) })} /></label>
                    <label className="tool-field"><span>Iterations after burn-in</span><input className="tool-input" type="number" min={100} step={1000} value={a.iterations} disabled={running} onChange={(e) => setA({ iterations: Math.max(100, Number(e.target.value)) })} /></label>
                  </div>
                </>
              ) : (
                <label className="tool-field"><span>Cross-validation folds</span><input className="tool-input" style={{ maxWidth: 140 }} type="number" min={0} max={10} value={a.cvFolds} disabled={running} onChange={(e) => setA({ cvFolds: Math.max(0, Math.min(10, Number(e.target.value))) })} />
                  <small>0 skips cross-validation.</small></label>
              )}
            </div>
          </ToolPanel>
        ) : null}
      </div>

      <div className="tool-stack" style={{ position: "sticky", top: 76 }}>
        <ToolPanel title="Run">
          <p style={{ margin: "0 0 12px", fontSize: 13.5, color: "var(--ink-700)", lineHeight: 1.5 }}>
            {summary.nInd} individuals, {summary.nLoci.toLocaleString()} loci{totalRuns ? `, ${totalRuns} ancestry runs (K ${a.kMin} to ${a.kMax}, ${a.resolvedReps} each)` : ""}. Runs use {Math.max(1, Math.min(8, (navigator.hardwareConcurrency || 4) - 1))} parallel workers on this computer.
          </p>
          {running ? (
            <div style={{ display: "grid", gap: 10 }}>
              <div className="tool-progress" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round((run.fraction || 0) * 100)}><span style={{ width: `${Math.round((run.fraction || 0) * 100)}%` }} /></div>
              <div style={{ fontSize: 13, color: "var(--ink-600)" }}>{Math.round((run.fraction || 0) * 100)}% · {run.done || 0} of {run.total || "?"} jobs done</div>
              <button type="button" className="btn btn-ghost btn-tap" onClick={onCancel}>Cancel</button>
            </div>
          ) : (
            <div style={{ display: "grid", gap: 10 }}>
              <button type="button" className="btn btn-primary btn-tap" onClick={onRun}>{run.status === "done" ? "Run again" : "Run analysis"}</button>
              {run.status === "cancelled" ? <ToolNotice tone="warn">Cancelled. Nothing was kept from the partial run.</ToolNotice> : null}
            </div>
          )}
        </ToolPanel>
      </div>
    </div>
  );
}

/* Step 4: results --------------------------------------------------------------- */

const POP_TABS = [
  { key: "ancestry", label: "Ancestry" },
  { key: "pca", label: "PCA" },
  { key: "diversity", label: "Diversity" },
  { key: "differentiation", label: "Differentiation" },
  { key: "hwe", label: "Hardy-Weinberg" },
  { key: "methods", label: "Methods" },
  { key: "downloads", label: "Downloads" },
];

function PopResults({ engine, results, onRerunRecord, rerun, setRerun }) {
  const available = POP_TABS.filter((t) => {
    if (t.key === "ancestry") return Boolean(results.ancestry);
    if (t.key === "pca") return Boolean(results.pca);
    if (t.key === "diversity") return Boolean(results.diversity);
    if (t.key === "differentiation") return Boolean(results.fstats || results.pairwise || results.nei);
    if (t.key === "hwe") return Boolean(results.hwe);
    return true;
  });
  const [tab, setTab] = React.useState(available[0].key);
  React.useEffect(() => {
    if (rerun && rerun.status === "ready" && results.record) {
      const same = rerun.record.resultsDigest === results.record.resultsDigest;
      setRerun({ status: same ? "same" : "different", record: rerun.record, message: same ? "Same results as the run record." : "Results differ from the run record." });
    }
  }, [results]);
  return (
    <div className="tool-stack">
      {rerun && (rerun.status === "same" || rerun.status === "different") ? (
        <ToolNotice tone={rerun.status === "same" ? "ok" : "block"} title={rerun.status === "same" ? "Rerun matches" : "Rerun differs"}>{rerun.message}</ToolNotice>
      ) : null}
      <div className="tool-tabs" role="tablist" aria-label="Results">
        {available.map((t) => <button key={t.key} type="button" role="tab" className="tool-tab" aria-selected={tab === t.key} onClick={() => setTab(t.key)}>{t.label}</button>)}
      </div>
      {tab === "ancestry" ? <PopAncestryView engine={engine} results={results} /> : null}
      {tab === "pca" ? <PopPcaView engine={engine} results={results} /> : null}
      {tab === "diversity" ? <PopDiversityView results={results} /> : null}
      {tab === "differentiation" ? <PopDifferentiationView engine={engine} results={results} /> : null}
      {tab === "hwe" ? <PopHweView results={results} /> : null}
      {tab === "methods" ? <PopMethodsView engine={engine} results={results} /> : null}
      {tab === "downloads" ? <PopDownloadsView engine={engine} results={results} onRerunRecord={onRerunRecord} /> : null}
    </div>
  );
}

function PopFigure({ svg, width, height, name, fill }) {
  return (
    <div>
      <div style={{ overflowX: "auto", border: "1px solid var(--ink-200)", borderRadius: 8, background: "white" }}>
        <div style={{ minWidth: Math.min(width, 560), maxWidth: fill ? "none" : width }} dangerouslySetInnerHTML={{ __html: svg.replace(/ width="[\d.]+" height="[\d.]+"/, ' width="100%" style="display:block;height:auto"') }} />
      </div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 8 }}>
        <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={() => toolDownload(`${name}.svg`, svg, "image/svg+xml")}>SVG</button>
        <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={async () => toolDownload(`${name}.png`, await popSvgToPng(svg, width, height, 2))}>PNG 2×</button>
        <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={async () => toolDownload(`${name}-4x.png`, await popSvgToPng(svg, width, height, 4))}>PNG 4×</button>
      </div>
    </div>
  );
}

// Width of an element, kept current with ResizeObserver, so figures are drawn at the
// size they are shown instead of being stretched.
function usePopWidth(ref, fallback) {
  const [width, setWidth] = React.useState(fallback);
  React.useEffect(() => {
    if (!ref.current || typeof ResizeObserver === "undefined") return undefined;
    const observer = new ResizeObserver((entries) => {
      const w = Math.floor(entries[0].contentRect.width);
      if (w > 0) setWidth(w);
    });
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);
  return width;
}

function PopAncestryView({ engine, results }) {
  const boxRef = React.useRef(null);
  const boxWidth = usePopWidth(boxRef, 900);
  const a = results.ancestry;
  const ds = results.dataset;
  const ks = a.byK.map((k) => k.K);
  const defaultK = a.choice.bestK && ks.includes(a.choice.bestK) ? a.choice.bestK : ks.find((k) => k > 1) || ks[0];
  const [K, setK] = React.useState(defaultK);
  const [multi, setMulti] = React.useState(false);
  const [sortWithin, setSortWithin] = React.useState(true);
  const popOf = ds.individuals.map((ind) => ind.pop);
  const current = a.byK.find((k) => k.K === K);
  const order = engine.barOrder(popOf, ds.pops.length, current.displayQ, K, sortWithin);
  const panels = multi ? a.byK.filter((k) => k.K > 1).map((k) => ({ K: k.K, Q: k.displayQ, label: `K = ${k.K}` })) : [{ K, Q: current.displayQ, label: `K = ${K}` }];
  const fig = engine.barplotSvg({ order, popOf, popNames: ds.pops, panels, width: Math.max(640, boxWidth - 2), panelHeight: multi ? 80 : 130 });
  const rows = a.choice.rows;
  const lnPlot = a.choice.kind === "evanno"
    ? engine.linePlotSvg({ points: rows.map((r) => ({ x: r.K, y: r.mean, sd: r.sd })), xLabel: "K", yLabel: "Mean ln P(D)", title: "ln P(D) by K" })
    : engine.linePlotSvg({ points: rows.map((r) => ({ x: r.K, y: r.mean, sd: r.sd })), xLabel: "K", yLabel: "Cross-validation error", title: "Cross-validation error by K", highlightX: a.choice.bestK });
  const dkPlot = a.choice.kind === "evanno" && rows.some((r) => Number.isFinite(r.deltaK))
    ? engine.linePlotSvg({ points: rows.filter((r) => Number.isFinite(r.deltaK)).map((r) => ({ x: r.K, y: r.deltaK })), xLabel: "K", yLabel: "ΔK", title: "Evanno ΔK", highlightX: a.choice.bestK })
    : null;
  return (
    <div className="tool-stack">
      <ToolPanel title="Ancestry proportions" actions={<span className="chip">{a.model === "bayesian" ? "Bayesian model" : "Maximum likelihood"}</span>}>
        <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center", marginBottom: 12 }}>
          <span style={{ fontSize: 13, color: "var(--ink-600)", marginRight: 4 }}>K</span>
          {ks.map((k) => <button key={k} type="button" className="tool-tab" aria-selected={!multi && k === K} onClick={() => { setK(k); setMulti(false); }}>{k}{k === a.choice.bestK ? " ★" : ""}</button>)}
          <button type="button" className="tool-tab" aria-selected={multi} onClick={() => setMulti(!multi)}>All K</button>
          <label className="tool-check" style={{ marginLeft: 8, minHeight: 40 }}><input type="checkbox" checked={sortWithin} onChange={(e) => setSortWithin(e.target.checked)} />Sort within populations</label>
        </div>
        <div ref={boxRef}><PopFigure svg={fig.svg} width={fig.width} height={fig.height} name={multi ? "ancestry-all-k" : `ancestry-k${K}`} /></div>
        <p className="tool-muted" style={{ fontSize: 12.5, marginTop: 8 }}>
          {a.choice.kind === "evanno" ? (a.choice.bestK ? `★ ΔK is highest at K = ${a.choice.bestK}. ` : "") : (a.choice.bestK ? `★ Cross-validation error is lowest at K = ${a.choice.bestK}. ` : "")}
          Bars are the mean of {current.runs.length} aligned run{current.runs.length === 1 ? "" : "s"}{current.hPrime !== null ? `; replicate similarity H′ = ${popFmt(current.hPrime, 3)}` : ""}.
        </p>
        {current.hPrime !== null && current.hPrime < 0.9 ? <ToolNotice tone="warn">Replicate runs at K = {K} found different solutions (H′ below 0.9). Longer runs or more replicates may help.</ToolNotice> : null}
      </ToolPanel>
      <div className="grid-2" style={{ alignItems: "start" }}>
        <ToolPanel title={a.choice.kind === "evanno" ? "ln P(D)" : "Cross-validation"}><PopFigure svg={lnPlot.svg} width={lnPlot.width} height={lnPlot.height} name={a.choice.kind === "evanno" ? "lnpd-by-k" : "cv-error-by-k"} /></ToolPanel>
        {dkPlot ? <ToolPanel title="ΔK (Evanno)"><PopFigure svg={dkPlot.svg} width={dkPlot.width} height={dkPlot.height} name="delta-k" /></ToolPanel> : (
          <ToolPanel title="Choosing K"><p style={{ margin: 0, fontSize: 13.5, color: "var(--ink-700)", lineHeight: 1.55 }}>{a.choice.kind === "evanno" ? "ΔK needs at least two runs per K and three consecutive K values." : "Lower cross-validation error means better prediction of held-out genotypes. Look for where the curve stops improving."}</p></ToolPanel>
        )}
      </div>
      <ToolPanel title="By K">
        <div className="table-scroll">
          <table className="tool-table">
            <thead>{a.choice.kind === "evanno"
              ? <tr><th>K</th><th className="num">Runs</th><th className="num">Mean ln P(D)</th><th className="num">SD</th><th className="num">L′(K)</th><th className="num">|L″(K)|</th><th className="num">ΔK</th><th className="num">H′</th></tr>
              : <tr><th>K</th><th className="num">Runs</th><th className="num">Mean CV error</th><th className="num">SD</th><th className="num">H′</th></tr>}</thead>
            <tbody>
              {rows.map((r) => {
                const k = a.byK.find((x) => x.K === r.K);
                return a.choice.kind === "evanno" ? (
                  <tr key={r.K}><td>{r.K}{r.K === a.choice.bestK ? " ★" : ""}</td><td className="num">{r.reps}</td><td className="num">{popFmt(r.mean, 1)}</td><td className="num">{popFmt(r.sd, 2)}</td><td className="num">{popFmt(r.lPrime, 1)}</td><td className="num">{popFmt(r.lDoublePrime, 1)}</td><td className="num">{popFmt(r.deltaK, 2)}</td><td className="num">{popFmt(k && k.hPrime, 3)}</td></tr>
                ) : (
                  <tr key={r.K}><td>{r.K}{r.K === a.choice.bestK ? " ★" : ""}</td><td className="num">{r.reps}</td><td className="num">{popFmt(r.mean, 4)}</td><td className="num">{popFmt(r.sd, 4)}</td><td className="num">{popFmt(k && k.hPrime, 3)}</td></tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </ToolPanel>
    </div>
  );
}

function PopPcaView({ engine, results }) {
  const pca = results.pca;
  const ds = results.dataset;
  const [axes, setAxes] = React.useState([0, 1]);
  const comps = pca.eigenvalues.length;
  const fig = comps >= 2 ? engine.scatterSvg({
    points: pca.scores.map((row, i) => ({ x: row[axes[0]], y: row[axes[1]], group: ds.individuals[i].pop })),
    groupNames: ds.pops,
    xLabel: `PC${axes[0] + 1} (${popPct(pca.explained[axes[0]])})`,
    yLabel: `PC${axes[1] + 1} (${popPct(pca.explained[axes[1]])})`,
  }) : null;
  return (
    <div className="tool-grid">
      <ToolPanel title="PCA of individuals">
        {fig ? <>
          <div style={{ display: "flex", gap: 10, marginBottom: 12, flexWrap: "wrap" }}>
            {[0, 1].map((slot) => (
              <label key={slot} className="tool-field" style={{ minWidth: 120 }}><span>{slot === 0 ? "Horizontal" : "Vertical"}</span>
                <select className="tool-input" value={axes[slot]} onChange={(e) => { const next = [...axes]; next[slot] = Number(e.target.value); setAxes(next); }}>
                  {pca.eigenvalues.map((_, c) => <option key={c} value={c}>PC{c + 1}</option>)}
                </select>
              </label>
            ))}
          </div>
          <PopFigure svg={fig.svg} width={fig.width} height={fig.height} name={`pca-pc${axes[0] + 1}-pc${axes[1] + 1}`} />
        </> : <p className="tool-muted">PCA needs at least two components.</p>}
        {pca.usedLoci < ds.loci.length ? <p className="tool-muted" style={{ fontSize: 12.5 }}>Computed on a random subset of {pca.usedLoci.toLocaleString()} loci.</p> : null}
      </ToolPanel>
      <ToolPanel title="Variance explained">
        <table className="tool-table">
          <thead><tr><th>PC</th><th className="num">Eigenvalue</th><th className="num">Explained</th></tr></thead>
          <tbody>{pca.eigenvalues.map((v, c) => <tr key={c}><td>PC{c + 1}</td><td className="num">{popFmt(v, 3)}</td><td className="num">{popPct(pca.explained[c])}</td></tr>)}</tbody>
        </table>
      </ToolPanel>
    </div>
  );
}

function PopDiversityView({ results }) {
  const div = results.diversity;
  const ds = results.dataset;
  const [perLocus, setPerLocus] = React.useState(false);
  const cols = [["n", "N", 0], ["na", "Na", 2], ["ne", "Ne", 2], ["ho", "Ho", 3], ["he", "He", 3], ["uhe", "uHe", 3], ["fis", "FIS", 3], ["allelicRichness", "AR", 2], ["privateAlleles", "Private", 0]];
  return (
    <div className="tool-stack">
      <ToolPanel title="Diversity by population">
        <div className="table-scroll">
          <table className="tool-table">
            <thead><tr><th>Population</th>{cols.map(([, label]) => <th key={label} className="num keep-case">{label}</th>)}</tr></thead>
            <tbody>
              {div.perPop.map((row, p) => (
                <tr key={p}><td style={{ fontWeight: 500 }}>{popName(ds, row.pop ?? p)}</td>{cols.map(([key, , d]) => <td key={key} className="num">{popFmt(row[key], d)}{row.se && row.se[key] !== undefined && row.se[key] !== null && d > 0 ? <span className="tool-muted" style={{ fontSize: 11 }}> ± {popFmt(row.se[key], d)}</span> : null}</td>)}</tr>
              ))}
              {div.overall ? <tr><td style={{ fontWeight: 600 }}>All individuals</td>{cols.map(([key, , d]) => <td key={key} className="num" style={{ fontWeight: 600 }}>{popFmt(div.overall[key], d)}</td>)}</tr> : null}
            </tbody>
          </table>
        </div>
        <p className="tool-muted" style={{ fontSize: 12.5, marginTop: 10, lineHeight: 1.5 }}>
          Means over loci ± standard error. N individuals; Na alleles; Ne effective alleles; Ho observed and He expected heterozygosity; uHe unbiased He; FIS = 1 − Ho/uHe; AR allelic richness rarefied to the smallest sample; Private alleles found in one population only.
        </p>
        <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={() => setPerLocus(!perLocus)}>{perLocus ? "Hide per-locus values" : "Show per-locus values"}</button>
      </ToolPanel>
      {perLocus ? (
        <ToolPanel title="Per locus">
          <div className="table-scroll" style={{ maxHeight: 480, overflowY: "auto" }}>
            <table className="tool-table">
              <thead><tr><th>Population</th><th>Locus</th><th className="num keep-case">N</th><th className="num keep-case">Na</th><th className="num keep-case">Ho</th><th className="num keep-case">He</th><th className="num keep-case">uHe</th><th className="num keep-case">FIS</th></tr></thead>
              <tbody>
                {div.perPopLocus.flatMap((rows, p) => rows.slice(0, 2000).map((r, l) => (
                  <tr key={`${p}-${l}`}><td>{popName(ds, div.perPop[p].pop ?? p)}</td><td className="tool-mono">{r.locus === undefined ? ds.loci[l].name : (typeof r.locus === "number" ? ds.loci[r.locus].name : r.locus)}</td><td className="num">{r.n}</td><td className="num">{r.na}</td><td className="num">{popFmt(r.ho)}</td><td className="num">{popFmt(r.he)}</td><td className="num">{popFmt(r.uhe)}</td><td className="num">{popFmt(r.fis)}</td></tr>
                )))}
              </tbody>
            </table>
          </div>
        </ToolPanel>
      ) : null}
    </div>
  );
}

// Permutation p-values exist only when permutations were run.
function popHasP(pw) {
  return Boolean(pw && pw.p && pw.p.some((row) => row.some((v) => v !== null && v !== undefined)));
}

function PopDifferentiationView({ engine, results }) {
  const ds = results.dataset;
  const f = results.fstats;
  const pw = results.pairwise;
  const nei = results.nei;
  const heat = pw && ds.pops.length >= 2 ? engine.heatmapSvg({ labels: pw.pops || ds.pops, matrix: pw.fst, pValues: popHasP(pw) ? pw.p : null, title: "Pairwise FST" }) : null;
  const neiHeat = nei && ds.pops.length >= 2 ? engine.heatmapSvg({ labels: nei.pops || ds.pops, matrix: nei.d, title: "Nei's distance" }) : null;
  const tree = results.upgma && ds.pops.length >= 3 ? engine.treeSvg({ root: results.upgma.root, title: "UPGMA tree from Nei's distance" }) : null;
  return (
    <div className="tool-stack">
      {f ? (
        <div className="tool-stat-row">
          <ToolStat label="FST" value={popFmt(f.overall.fst)} sub="Weir and Cockerham θ" />
          <ToolStat label="FIS" value={popFmt(f.overall.fis)} />
          <ToolStat label="FIT" value={popFmt(f.overall.fit)} />
        </div>
      ) : null}
      {ds.pops.length < 2 ? <ToolNotice tone="info">Differentiation needs at least two populations.</ToolNotice> : null}
      <div className="grid-2" style={{ alignItems: "start" }}>
        {heat ? <ToolPanel title="Pairwise FST"><PopFigure svg={heat.svg} width={heat.width} height={heat.height} name="pairwise-fst" />{popHasP(pw) ? <p className="tool-muted" style={{ fontSize: 12.5 }}>* p &lt; 0.05 from permutations of individuals between the two populations.</p> : null}</ToolPanel> : null}
        {neiHeat ? <ToolPanel title="Nei's distance"><PopFigure svg={neiHeat.svg} width={neiHeat.width} height={neiHeat.height} name="nei-distance" /></ToolPanel> : null}
      </div>
      {tree ? <ToolPanel title="UPGMA tree"><PopFigure svg={tree.svg} width={tree.width} height={tree.height} name="upgma-tree" /></ToolPanel> : null}
      {f ? (
        <ToolPanel title="F-statistics by locus">
          <div className="table-scroll" style={{ maxHeight: 420, overflowY: "auto" }}>
            <table className="tool-table">
              <thead><tr><th>Locus</th><th className="num keep-case">FIS</th><th className="num keep-case">FST</th><th className="num keep-case">FIT</th></tr></thead>
              <tbody>{f.perLocus.slice(0, 3000).map((l, i) => <tr key={i}><td className="tool-mono">{typeof l.locus === "number" ? ds.loci[l.locus].name : l.locus}</td><td className="num">{popFmt(l.fis)}</td><td className="num">{popFmt(l.fst)}</td><td className="num">{popFmt(l.fit)}</td></tr>)}</tbody>
            </table>
          </div>
        </ToolPanel>
      ) : null}
    </div>
  );
}

function PopHweView({ results }) {
  const ds = results.dataset;
  const [onlySig, setOnlySig] = React.useState(false);
  const rows = results.hwe.filter((r) => !onlySig || (r.q !== null && r.q < 0.05));
  const tested = results.hwe.filter((r) => r.p !== null && r.p !== undefined);
  const sig = tested.filter((r) => r.q !== null && r.q < 0.05).length;
  return (
    <ToolPanel title="Hardy-Weinberg tests" actions={<label className="tool-check" style={{ minHeight: 36 }}><input type="checkbox" checked={onlySig} onChange={(e) => setOnlySig(e.target.checked)} />Only q &lt; 0.05</label>}>
      <p style={{ margin: "0 0 10px", fontSize: 13.5, color: "var(--ink-700)" }}>{sig} of {tested.length} tests have q &lt; 0.05 after Benjamini-Hochberg adjustment.</p>
      <div className="table-scroll" style={{ maxHeight: 520, overflowY: "auto" }}>
        <table className="tool-table">
          <thead><tr><th>Population</th><th>Locus</th><th className="num">N</th><th className="num">Alleles</th><th className="num keep-case">Ho</th><th className="num keep-case">He</th><th className="num keep-case">p</th><th className="num keep-case">q</th><th>Test</th></tr></thead>
          <tbody>
            {rows.slice(0, 3000).map((r, i) => (
              <tr key={i}>
                <td>{popName(ds, r.pop)}</td><td className="tool-mono">{typeof r.locus === "number" ? ds.loci[r.locus].name : r.locus}</td>
                <td className="num">{r.n}</td><td className="num">{r.alleles}</td><td className="num">{popFmt(r.ho)}</td><td className="num">{popFmt(r.he)}</td>
                <td className={`num ${r.p !== null && r.p < 0.05 ? "tool-warn" : ""}`}>{r.p === null || r.p === undefined ? "n/a" : r.p < 0.0001 ? "< 0.0001" : popFmt(r.p, 4)}</td>
                <td className={`num ${r.q !== null && r.q < 0.05 ? "tool-bad" : ""}`}>{r.q === null || r.q === undefined ? "n/a" : r.q < 0.0001 ? "< 0.0001" : popFmt(r.q, 4)}</td>
                <td className="tool-muted">{{ exact: "exact", "monte-carlo": "Monte Carlo", monomorphic: "one allele", "no-data": "no data" }[r.method] || r.method}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </ToolPanel>
  );
}

function PopMethodsView({ engine, results }) {
  const text = engine.methodsText(results.record);
  const [copied, setCopied] = React.useState(false);
  const blocks = text.split(/\n\n+/);
  return (
    <ToolPanel title="Methods" actions={<>
      <button type="button" className="btn btn-ghost btn-sm" onClick={() => { navigator.clipboard && navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1800); }}>{copied ? "Copied" : "Copy"}</button>
      <button type="button" className="btn btn-ghost btn-sm" onClick={() => toolDownload("methods.md", text, "text/markdown")}>Download</button>
    </>}>
      <div style={{ maxWidth: 820, fontSize: 14.5, lineHeight: 1.65, color: "var(--ink-800)" }}>
        {blocks.map((block, i) => {
          if (block.startsWith("### ")) return <h4 key={i} style={{ margin: "18px 0 6px", fontSize: 14 }}>{block.slice(4)}</h4>;
          if (block.startsWith("## ")) return <h3 key={i} style={{ margin: "0 0 10px", fontSize: 17 }}>{block.slice(3)}</h3>;
          if (block.startsWith("- ")) return <ul key={i} style={{ margin: 0, paddingLeft: 18, fontSize: 13 }}>{block.split("\n").map((line, j) => <li key={j}>{line.replace(/^- /, "")}</li>)}</ul>;
          return <p key={i} style={{ margin: "0 0 12px" }}>{block}</p>;
        })}
      </div>
    </ToolPanel>
  );
}

function popTableFiles(engine, results) {
  const ds = results.dataset;
  const files = [];
  const name = (pop) => popName(ds, pop);
  if (results.diversity) {
    files.push({ name: "tables/diversity-by-population.csv", data: engine.toCsv(["population", "n", "na", "ne", "ho", "he", "uhe", "fis", "allelic_richness", "private_alleles"],
      results.diversity.perPop.map((r, p) => [name(r.pop ?? p), r.n, r.na, r.ne, r.ho, r.he, r.uhe, r.fis, r.allelicRichness, r.privateAlleles])) });
    files.push({ name: "tables/diversity-by-locus.csv", data: engine.toCsv(["population", "locus", "n", "na", "ne", "ho", "he", "uhe", "fis", "allelic_richness"],
      results.diversity.perPopLocus.flatMap((rows, p) => rows.map((r, l) => [name(results.diversity.perPop[p].pop ?? p), typeof r.locus === "number" ? ds.loci[r.locus].name : (r.locus ?? ds.loci[l].name), r.n, r.na, r.ne, r.ho, r.he, r.uhe, r.fis, r.ar]))) });
  }
  if (results.hwe) files.push({ name: "tables/hardy-weinberg.csv", data: engine.toCsv(["population", "locus", "n", "alleles", "ho", "he", "p", "q", "test"], results.hwe.map((r) => [name(r.pop), typeof r.locus === "number" ? ds.loci[r.locus].name : r.locus, r.n, r.alleles, r.ho, r.he, r.p, r.q, r.method])) });
  if (results.fstats) files.push({ name: "tables/f-statistics-by-locus.csv", data: engine.toCsv(["locus", "fis", "fst", "fit"], [...results.fstats.perLocus.map((l) => [typeof l.locus === "number" ? ds.loci[l.locus].name : l.locus, l.fis, l.fst, l.fit]), ["all loci", results.fstats.overall.fis, results.fstats.overall.fst, results.fstats.overall.fit]]) });
  if (results.pairwise) {
    files.push({ name: "tables/pairwise-fst.csv", data: engine.matrixCsv(results.pairwise.pops || ds.pops, results.pairwise.fst) });
    if (popHasP(results.pairwise)) files.push({ name: "tables/pairwise-fst-p-values.csv", data: engine.matrixCsv(results.pairwise.pops || ds.pops, results.pairwise.p) });
  }
  if (results.nei) files.push({ name: "tables/nei-distance.csv", data: engine.matrixCsv(results.nei.pops || ds.pops, results.nei.d) });
  if (results.upgma) files.push({ name: "tables/upgma-tree.newick", data: `${results.upgma.newick}\n` });
  if (results.pca) {
    const comps = results.pca.eigenvalues.length;
    files.push({ name: "tables/pca-scores.csv", data: engine.toCsv(["individual", "population", ...Array.from({ length: comps }, (_, c) => `PC${c + 1}`)], results.pca.scores.map((row, i) => [ds.individuals[i].id, ds.pops[ds.individuals[i].pop], ...row])) });
    files.push({ name: "tables/pca-eigenvalues.csv", data: engine.toCsv(["component", "eigenvalue", "explained"], results.pca.eigenvalues.map((v, c) => [`PC${c + 1}`, v, results.pca.explained[c]])) });
  }
  if (results.ancestry) {
    const a = results.ancestry;
    if (a.choice.kind === "evanno") files.push({ name: "tables/evanno.csv", data: engine.toCsv(["K", "runs", "mean_lnPD", "sd_lnPD", "L_prime", "L_double_prime", "delta_K"], a.choice.rows.map((r) => [r.K, r.reps, r.mean, r.sd, r.lPrime, r.lDoublePrime, r.deltaK])) });
    else files.push({ name: "tables/cross-validation.csv", data: engine.toCsv(["K", "runs", "mean_cv_error", "sd_cv_error"], a.choice.rows.map((r) => [r.K, r.reps, r.mean, r.sd])) });
    files.push({ name: "tables/ancestry-runs.csv", data: engine.toCsv(["K", "run", "seed", "lnPD", "mean_lnL", "var_lnL", "alpha", "log_likelihood", "cv_error", "iterations"], a.byK.flatMap((k) => k.runs.map((r) => [k.K, r.rep + 1, r.seed, r.lnPD, r.meanLnL, r.varLnL, r.alpha, r.logLik, r.cvError, r.iterations]))) });
    for (const k of a.byK) {
      files.push({ name: `q-matrices/K${k.K}-mean-aligned.csv`, data: engine.qMatrixCsv(ds, k.displayQ, k.K) });
      k.runs.forEach((r) => files.push({ name: `q-matrices/K${k.K}-run${r.rep + 1}.csv`, data: engine.qMatrixCsv(ds, r.Q, k.K) }));
    }
  }
  return files;
}

function popFigureFiles(engine, results) {
  const ds = results.dataset;
  const files = [];
  const popOf = ds.individuals.map((ind) => ind.pop);
  if (results.ancestry) {
    const a = results.ancestry;
    for (const k of a.byK.filter((x) => x.K > 1)) {
      const order = engine.barOrder(popOf, ds.pops.length, k.displayQ, k.K, true);
      files.push({ name: `figures/ancestry-K${k.K}.svg`, data: engine.barplotSvg({ order, popOf, popNames: ds.pops, panels: [{ K: k.K, Q: k.displayQ, label: `K = ${k.K}` }], width: Math.max(720, Math.min(1400, ds.individuals.length * 4 + 80)) }).svg });
    }
    const rows = a.choice.rows;
    files.push({ name: a.choice.kind === "evanno" ? "figures/lnpd-by-k.svg" : "figures/cv-error-by-k.svg", data: engine.linePlotSvg({ points: rows.map((r) => ({ x: r.K, y: r.mean, sd: r.sd })), xLabel: "K", yLabel: a.choice.kind === "evanno" ? "Mean ln P(D)" : "Cross-validation error", highlightX: a.choice.kind === "cv" ? a.choice.bestK : null }).svg });
    if (a.choice.kind === "evanno" && rows.some((r) => Number.isFinite(r.deltaK))) files.push({ name: "figures/delta-k.svg", data: engine.linePlotSvg({ points: rows.filter((r) => Number.isFinite(r.deltaK)).map((r) => ({ x: r.K, y: r.deltaK })), xLabel: "K", yLabel: "ΔK", highlightX: a.choice.bestK }).svg });
  }
  if (results.pca && results.pca.eigenvalues.length >= 2) files.push({ name: "figures/pca-pc1-pc2.svg", data: engine.scatterSvg({ points: results.pca.scores.map((row, i) => ({ x: row[0], y: row[1], group: ds.individuals[i].pop })), groupNames: ds.pops, xLabel: `PC1 (${popPct(results.pca.explained[0])})`, yLabel: `PC2 (${popPct(results.pca.explained[1])})` }).svg });
  if (results.pairwise && ds.pops.length >= 2) files.push({ name: "figures/pairwise-fst.svg", data: engine.heatmapSvg({ labels: results.pairwise.pops || ds.pops, matrix: results.pairwise.fst, pValues: popHasP(results.pairwise) ? results.pairwise.p : null, title: "Pairwise FST" }).svg });
  if (results.upgma && ds.pops.length >= 3) files.push({ name: "figures/upgma-tree.svg", data: engine.treeSvg({ root: results.upgma.root, title: "UPGMA tree from Nei's distance" }).svg });
  return files;
}

function PopDownloadsView({ engine, results, onRerunRecord }) {
  const tables = popTableFiles(engine, results);
  const figures = popFigureFiles(engine, results);
  const methods = { name: "methods.md", data: engine.methodsText(results.record) };
  const record = { name: "run-record.json", data: JSON.stringify(results.record, null, 2) };
  const everything = [methods, record, ...figures, ...tables];
  const base = toolSlug(results.record.input.fileName.replace(/\.[^.]+$/, ""), "population-genetics");
  return (
    <div className="tool-grid">
      <ToolPanel title="Files">
        <button type="button" className="btn btn-primary btn-tap" onClick={() => toolDownload(`${base}-results.zip`, new Blob([engine.zipFiles(everything)], { type: "application/zip" }))}>
          Download everything (ZIP, {everything.length} files)
        </button>
        <ul style={{ margin: "14px 0 0", padding: 0, listStyle: "none", display: "grid", gap: 4 }}>
          {everything.map((f) => (
            <li key={f.name} style={{ display: "flex", gap: 10, alignItems: "center", fontSize: 13.5 }}>
              <span className="tool-mono" style={{ flex: 1 }}>{f.name}</span>
              <button type="button" className="btn btn-link" style={{ fontSize: 13, minHeight: 32 }} onClick={() => toolDownload(f.name.split("/").pop(), f.data, f.name.endsWith(".svg") ? "image/svg+xml" : f.name.endsWith(".json") ? "application/json" : "text/plain")}>Download</button>
            </li>
          ))}
        </ul>
      </ToolPanel>
      <div className="tool-stack">
      <ToolSaveToAccount module="population" defaultTitle={results.record.input.fileName.replace(/\.[^.]+$/, "")}
                         summary={{ fileName: results.record.input.fileName, individuals: results.record.data.nInd, populations: results.record.data.nPop, loci: results.record.data.nLoci, bestK: results.ancestry ? results.ancestry.choice.bestK : null }}
                         getPayload={async () => ({ record: results.record, files: everything.map((f) => ({ name: f.name, data: f.data })) })} />
      <ToolPanel title="Rerun">
        <p style={{ margin: "0 0 10px", fontSize: 13.5, color: "var(--ink-700)", lineHeight: 1.5 }}>
          The run record holds the file fingerprint, filters, settings, and seeds. Load the same genotype file, then the run record, and run again to check the results match.
        </p>
        <ToolDrop title="Load a run record (.json)" accept=".json,application/json" onFiles={onRerunRecord} />
        <p className="tool-muted" style={{ fontSize: 12, marginTop: 10 }}>Results fingerprint <span className="tool-mono">{results.record.resultsDigest.slice(0, 16)}…</span></p>
      </ToolPanel>
      </div>
    </div>
  );
}

Object.assign(window, { PopulationTool });
