"use client";

import React from "react";
import GenerationsLoopDiagram from "./diagrams/GenerationsLoopDiagram";
import LambdaTradeoffDiagram from "./diagrams/LambdaTradeoffDiagram";
import MaxSetsDiagram from "./diagrams/MaxSetsDiagram";
import PermutationsHistogramDiagram from "./diagrams/PermutationsHistogramDiagram";
import PrefilterFunnelDiagram from "./diagrams/PrefilterFunnelDiagram";
import SeedDiceDiagram from "./diagrams/SeedDiceDiagram";

export type ParamKey =
  | "generations"
  | "population"
  | "genes_per_set"
  | "max_sets"
  | "lambda"
  | "seed"
  | "permutations"
  | "prefilter_n"
  // Objective help keys reuse the same modal infrastructure.
  | "obj_msi"
  | "obj_tmb"
  | "obj_hpv"
  | "obj_unsupervised"
  // Misc rich helpers that don't fit the hover tooltip envelope
  // (multi-paragraph, table-cell-anchored).
  | "module_survival";

export interface ParamHelpEntry {
  title: string;
  short: string;
  detailed: React.ReactNode;
}

// ---------- shared bits --------------------------------------------------

function Item({
  label,
  children,
}: {
  label: string;
  children: React.ReactNode;
}) {
  return (
    <p className="mb-3 last:mb-0 text-sm leading-relaxed text-ink">
      <strong className="text-ink">{label}:</strong>{" "}
      <span>{children}</span>
    </p>
  );
}

function Caption({ children }: { children: React.ReactNode }) {
  return (
    <p className="mb-3 text-xs italic text-muted">{children}</p>
  );
}

function Mono({ children }: { children: React.ReactNode }) {
  return (
    <code className="font-mono text-[12.5px] text-ink">{children}</code>
  );
}

function ExampleList({
  rows,
}: {
  rows: React.ReactNode[];
}) {
  return (
    <ul className="my-2 ml-5 list-disc text-sm leading-relaxed text-ink marker:text-muted">
      {rows.map((r, i) => (
        <li key={i}>{r}</li>
      ))}
    </ul>
  );
}

// ---------- the registry -------------------------------------------------

export const PARAM_HELP: Record<ParamKey, ParamHelpEntry> = {
  generations: {
    title: "Generations",
    short:
      "How many rounds of 'keep the best, breed, mutate' the engine runs. More rounds refine further, but take longer.",
    detailed: (
      <>
        <Item label="What it is">
          one &ldquo;generation&rdquo; is a single round of the engine&rsquo;s
          loop — score every program, keep the best, then breed and mutate them
          into the next batch. Generations is how many rounds it runs.
        </Item>
        <Item label="How it works">
          the engine starts with random programs (mostly poor) and each round
          nudges the population toward better ones — like selective breeding.
        </Item>
        <Item label="More vs fewer">
          more rounds refine the winners further, but with diminishing returns
          once it has converged. The harder part is usually <em>discovering</em>{" "}
          the right genes in the first place (driven by Population and
          mutation), not polishing — so extra generations help less once the
          curve flattens.
        </Item>
        <Item label="Typical">30–60.</Item>
        <GenerationsLoopDiagram />
        <Caption>
          A 4-step loop: random programs → score each → keep the best → breed
          &amp; mutate → (back to score). The loop arrow is labelled
          &ldquo;× Generations&rdquo;.
        </Caption>
      </>
    ),
  },

  population: {
    title: "Population",
    short:
      "How many candidate programs compete in each round. A 'program' is a small pipeline built by composing the DSL operators (Select → Reduce → Fit) over the gene data. More programs explores more options, but is slower.",
    detailed: (
      <>
        <Item label="What it is">
          how many candidate programs compete each round. A &ldquo;program&rdquo;
          is a small pipeline built from the DSL operators —{" "}
          <strong>Select</strong> (pick genes) → <strong>Reduce</strong>{" "}
          (average them into one score) → <strong>Fit</strong> (use the score
          to separate the groups).
        </Item>
        <Item label="Why it matters">
          a bigger population samples more genes and structures each round, so
          it&rsquo;s the main lever for <em>discovery</em> — finding the right
          genes at all. Generations then refine what was found.
        </Item>
        <p className="mb-1 text-sm font-semibold text-ink">
          Example — three random programs in a population:
        </p>
        <ExampleList
          rows={[
            <Mono key="a">average(g04823, g11201, g00917) → score</Mono>,
            <Mono key="b">
              average(g15522, g02013) − average(g08840, g00231) → score
            </Mono>,
            <Mono key="c">average(g07788, g13002, g05340) → score</Mono>,
          ]}
        />
        <Item label="More vs fewer">
          more explores more options, but each round is slower.
        </Item>
        <Item label="Typical">150–300.</Item>
      </>
    ),
  },

  genes_per_set: {
    title: "Genes / set",
    short:
      "The most genes a single score may use. Smaller keeps programs simple and readable.",
    detailed: (
      <>
        <Item label="What it is">
          a &ldquo;set&rdquo; is the group of genes averaged together into a
          single score. Genes/set is the maximum size of that group.
        </Item>
        <Item label="Example">
          <Mono>
            Select(g05347, g00048, g06271) → average → one score
          </Mono>{" "}
          (uses 3; the cap might be 8).
        </Item>
        <Item label="Smaller vs larger">
          smaller keeps each score simple and readable; larger lets one score
          blend more genes.
        </Item>
        <Item label="Pairs with Max sets">
          Genes/set is the <em>width</em> of each group; Max sets is{" "}
          <em>how many</em> groups.
        </Item>
      </>
    ),
  },

  max_sets: {
    title: "Max sets",
    short:
      "How many separate gene-scores a program may combine (1 or 2). 2 lets it build a small 'score made of scores'.",
    detailed: (
      <>
        <Item label="What it is">
          how many <em>separate</em> gene groups a program may build and then
          combine (1 or 2). They use different genes — not the same ones
          reused.
        </Item>
        <p className="mb-1 text-sm font-semibold text-ink">Example (2 sets):</p>
        <ExampleList
          rows={[
            <>
              Set 1:{" "}
              <Mono>average(g05347, g00048, g06271) → scoreA</Mono>
            </>,
            <>
              Set 2: <Mono>average(g15522, g02013) → scoreB</Mono>
            </>,
            <>
              Combine: <Mono>scoreA − scoreB → final score</Mono>
            </>,
          ]}
        />
        <Item label="Why two">
          to capture a <em>contrast</em> — e.g. one group that&rsquo;s low in
          MSI-H and one that&rsquo;s high; the gap between them can separate
          better than either alone (like &ldquo;repair activity minus immune
          activity&rdquo;). One set can&rsquo;t express that; two can. If one
          group is enough, the engine can still use 1.
        </Item>
        <MaxSetsDiagram />
        <Caption>
          Two short chains feed a Combine(sub) node and produce a final score.
        </Caption>
      </>
    ),
  },

  lambda: {
    title: "λ (size penalty)",
    short:
      "How hard the engine is penalised for using more genes. Higher pushes it toward fewer genes (simpler answers).",
    detailed: (
      <>
        <Item label="What it is">
          a &ldquo;price per gene&rdquo; that discourages bloated programs.
          Every program is graded on a single number:
        </Item>
        <p className="mb-3 ml-5 text-sm">
          <Mono>fitness = separation − λ × (number of genes)</Mono>
        </p>
        <p className="mb-3 text-sm leading-relaxed text-ink">
          so the engine ranks by accuracy <em>minus</em> a size tax.
        </p>
        <Item label="The key idea">
          λ is the admission price each gene must beat: with λ = 0.005, a gene
          is only worth keeping if it adds more than 0.005 of separation.
        </Item>
        <Item label="Worked example">
          a 3-gene program scores 0.90 → net 0.90 − 0.015 = 0.885. Add a 4th
          gene that lifts it to 0.903 (only +0.003) → net 0.883, <em>lower</em>,
          so it&rsquo;s rejected. A 4th gene that lifts it to 0.91 (+0.01) →
          net 0.890, <em>higher</em>, so it&rsquo;s kept.
        </Item>
        <Item label="Turning the knob">
          λ = 0 → genes free → bloated, overfit programs. λ small (0.005) →
          trims useless genes, keeps useful ones. λ large → very lean programs,
          but may drop useful genes.
        </Item>
        <LambdaTradeoffDiagram />
        <Caption>
          Raw separation rises and plateaus; net fitness peaks at an
          intermediate size, and a larger λ shifts the peak to fewer genes.
        </Caption>
      </>
    ),
  },

  seed: {
    title: "Seed",
    short:
      "The starting point for the engine's randomness. The same seed reproduces the exact same run; change it to see a different run.",
    detailed: (
      <>
        <Item label="Why randomness">
          the space of possible programs is astronomically large (picking even
          8 genes out of 20,000 is ~10²⁹ combinations), so the engine
          can&rsquo;t try them all — it explores with randomness: random
          starting programs, random mutations, random breeding.
        </Item>
        <Item label="What the seed is">
          computers don&rsquo;t make true randomness; a formula generates a
          sequence of numbers, each from the previous one. The seed is the{" "}
          <em>starting number</em> fed into that formula — everything random
          flows from it. (One common formula:{" "}
          <Mono>next = (1664525 × current + 1013904223) mod 2³²</Mono>; with
          seed 42 the first value is 1,083,814,273, then mapped onto a gene
          position.)
        </Item>
        <Item label="What it does">
          think of the seed as one fixed list of dice rolls used in order
          across the whole run — early rolls pick the starting genes, later
          rolls drive mutations. Same seed → same list → identical run.
          Different seed → a different list → a different run.
        </Item>
        <Item label="Why vary it">
          because the search has luck in it, run a few seeds (42, 7, 123) and
          see whether the same genes keep appearing — that tells you a finding
          is real, not a fluke.
        </Item>
        <SeedDiceDiagram />
        <Caption>
          The seed is one fixed list of dice rolls; early rolls pick initial
          genes, later rolls drive mutations.
        </Caption>
      </>
    ),
  },

  permutations: {
    title: "Permutations",
    short:
      "How many times we re-run on deliberately scrambled labels to check the result isn't luck. The real result must beat these chance runs (that's the 'permutation p').",
    detailed: (
      <>
        <Item label="What it checks">
          whether the winning score is real signal or could have come up by
          luck. The engine searches so hard that some programs separate the
          groups well by pure coincidence, so a high score alone isn&rsquo;t
          proof.
        </Item>
        <Item label="How it works">
          shuffle the MSI-H / MSS labels across patients at random — this
          breaks any real gene↔label link, creating a &ldquo;no-signal
          world&rdquo;. Re-score there: any score above 0.5 is pure chance.
          Repeat many times (this knob = how many) to build a picture of what
          luck looks like.
        </Item>
        <Item label="Reading it">
          permutation p = the fraction of shuffled runs that scored ≥ the real
          winner. p = 0.005 means only 0.5% of chance runs matched it → very
          unlikely a fluke. Conventionally p &lt; 0.05 is the &ldquo;unlikely
          to be luck&rdquo; line.
        </Item>
        <Item label="The knob">
          with 200 shuffles the smallest p you can report is about 1 / 200 ≈
          0.005; more permutations give a finer, more trustworthy p.
        </Item>
        <PermutationsHistogramDiagram />
        <Caption>
          200 shuffled-label runs cluster near 0.58; the real result at 0.89
          sits past every one of them, giving p ≈ 0.005.
        </Caption>
      </>
    ),
  },

  prefilter_n: {
    title: "Prefilter top-N",
    short:
      "By default the engine searches all ~20,000 genes, so nothing is pre-excluded. Turning this on narrows to the N most promising genes first — faster, but it can drop a real gene that only shows signal in combination.",
    detailed: (
      <>
        <Item label="Default (off)">
          the engine searches all ~20,000 genes, so nothing is pre-excluded —
          the most honest setting for a discovery demo.
        </Item>
        <Item label="Turning it on">
          first narrows to the N genes most individually associated with the
          target (computed name-blind, on the training split only), then
          searches within that shortlist. Faster, because the space is ~10×
          smaller.
        </Item>
        <Item label="The trade-off">
          speed and focus vs completeness. A univariate shortlist can drop a
          gene that only matters <em>in combination</em> (no signal on its
          own), and it sets a ceiling — if a gene isn&rsquo;t in the
          shortlist, the engine can never find it.
        </Item>
        <PrefilterFunnelDiagram />
        <Caption>
          ~20,000 genes → keep the N most individually associated → the GP
          searches within N. Off by default = no funnel.
        </Caption>
      </>
    ),
  },

  // ---------- Objective explainers ----------

  obj_msi: {
    title: "MSI separation",
    short:
      "Reward programs whose score separates MSI-H from MSS (measured by AUROC). It rewards ANY separator, so it often finds shortcut genes, not the cause.",
    detailed: (
      <>
        <ObjectiveIntro />
        <Item label="What it optimises">
          a score that ranks MSI-H patients above MSS — i.e. tells the two
          subtypes apart.
        </Item>
        <Item label="How it&rsquo;s scored (AUROC)">
          pick one random MSI-H and one random MSS patient; AUROC is the
          chance the score puts the MSI-H one higher. 0.5 = coin-flip (no
          separation), 1.0 = perfect. We use AUROC, not plain accuracy,
          because MSI-H is only ~15% of patients — &ldquo;always guess
          MSS&rdquo; would look 85% accurate while separating nothing.
        </Item>
        <Item label="Represented as">
          <code className="font-mono text-ink">{`{ target: msi, metric: AUROC }`}</code>
          {" "}— the engine sees only the score + the MSI label, never
          gene names.
        </Item>
        <Item label="The catch">
          it rewards ANY separator. MSI-H and MSS differ in thousands of
          genes, so it usually grabs easy &ldquo;shortcut&rdquo; genes
          (consequences or coincidences), not the causal MMR genes.
        </Item>
        <ObjectiveFooter />
      </>
    ),
  },

  obj_tmb: {
    title: "Mutation burden",
    short:
      "Reward programs whose score is negatively associated with mutation burden (low score ↔ high TMB) — the broken spell-checker's fingerprint. A sharper proxy for the cause, but still association, not proof.",
    detailed: (
      <>
        <ObjectiveIntro />
        <Item label="What it optimises">
          a score that goes DOWN as mutation count goes UP — the
          fingerprint of a broken repair gene (switch it off → mutations
          pile up). The repair genes are the MMR set: MLH1, MSH2, MSH6,
          PMS2.
        </Item>
        <Item label="Represented as">
          <code className="font-mono text-ink">{`{ target: tmb, metric: correlation, direction: negative }`}</code>
          {" "}— the engine sees only the score + the TMB numbers.
        </Item>
        <Item label="Why &lsquo;negative&rsquo; (not just &lsquo;related&rsquo;)">
          we reward the score being NEGATIVELY correlated with TMB.
          Rewarding any correlation would also pick genes that RISE with
          mutations — the opposite of the repair signature.
        </Item>
        <Item label="Correlation isn&rsquo;t causation (honest)">
          this is a mechanism-shaped association — a much better proxy
          for the cause than predicting the label, but it doesn&rsquo;t
          prove causation. Genes silenced alongside the repair genes can
          mimic the same low-expression↔high-TMB pattern.
        </Item>
        <Item label="Toward causal">
          the program can choose to ADJUST for confounders (age, stage)
          via the Effect operator — the most causal move the
          observational data honestly allows. The engine decides whether
          it helps; we don&rsquo;t hardcode it.
        </Item>
        <ObjectiveFooter />
      </>
    ),
  },

  obj_hpv: {
    title: "HPV detection",
    short:
      "Find a gene-expression pattern that tells HPV+ tumours apart from HPV−. Scored by AUROC (0.5 = coin-flip, 1.0 = perfect).",
    detailed: (
      <>
        <p className="mb-4 rounded-md border border-border bg-bg p-3 text-[12.5px] leading-relaxed text-ink">
          <strong className="text-ink">HPV detection.</strong> Head &amp;
          neck cancers split into two kinds: those caused by the HPV
          virus (HPV+) and those that aren&rsquo;t (HPV−). The engine
          looks for a gene-expression pattern that tells the two apart.
        </p>
        <Item label="How it&rsquo;s scored (AUROC)">
          pick one random HPV+ and one random HPV− patient; AUROC is
          the chance the engine&rsquo;s score puts the HPV+ one higher.
          0.5 is a coin-flip, 1.0 is perfect. Because only ~15% of
          tumours are HPV+, we use AUROC rather than plain accuracy —
          &ldquo;always guess HPV−&rdquo; would look 85% accurate while
          separating nothing.
        </Item>
        <Item label="What the engine sees">
          only the score its program produces and the HPV+/HPV− label —
          never gene names. That&rsquo;s what makes recovering the
          known biology afterwards a real rediscovery, not a lookup.
        </Item>
        <Item label="Important">
          this is <em>detecting a known viral fingerprint</em> (the
          virus switches specific genes on), not discovering a new
          cause. The cause is the virus itself. A high AUROC means the
          engine recognised the fingerprint blind.
        </Item>
        <ObjectiveFooter />
      </>
    ),
  },

  obj_unsupervised: {
    title: "Unsupervised",
    short:
      "No target column at all. The engine searches for a program whose score splits patients into two clean groups; afterwards we check what that split lines up with.",
    detailed: (
      <>
        <ObjectiveIntro />
        <Item label="No target at all">
          Unlike the other objectives, this one is given NO target — only
          the gene numbers. The search never sees MSI, TMB, or any label.
        </Item>
        <Item label="What it optimises">
          a score that divides patients into two as-cleanly-separated-as-
          possible groups (measured by cluster separation). It is NOT told
          what the groups should be.
        </Item>
        <Item label="Shape vs aim">
          it always produces a two-group split, but it doesn&rsquo;t aim
          at MSS/dMMR — or anything. It finds whatever the strongest
          natural division in the data is.
        </Item>
        <Item label="How we read it">
          after the run we check what the discovered split lines up with
          — MSI? TMB? — the &ldquo;post-hoc alignment&rdquo; in the
          Result panel.
        </Item>
        <Item label="The win">
          if the strongest natural split turns out to BE the MSS-vs-dMMR
          divide, it aligns with MSI at high AUROC → the engine
          rediscovered the subtype without ever being told it exists.
        </Item>
        <Item label="Honest expectation">
          it may instead land on a different dominant axis (e.g. immune
          hot vs cold) that only partly overlaps MSI. MSI-H is only ~15%
          of patients, so a clean recovery isn&rsquo;t guaranteed — the
          result is HOW MUCH the blind split overlaps MSS/dMMR.
        </Item>
        <UnsupObjectiveFooter />
      </>
    ),
  },

  module_survival: {
    title: "What “Survives” checks",
    short:
      "Whether a group still separates the label when you take away a possible confounder — something that travels with the label but isn't its biology.",
    detailed: (
      <>
        <Item label="The question">
          whether a group still separates HPV when you take away a possible
          <em> confounder</em> — something that travels with HPV but
          isn&rsquo;t HPV biology. If the group&rsquo;s AUROC mostly came
          from the confounder, it collapses when the confounder is held
          constant; if the signal is real, it holds.
        </Item>
        <Item label="Site">
          HPV+ tumours are mostly in the oropharynx (back of the throat),
          so a gene could look like an &ldquo;HPV gene&rdquo; just by
          marking that location. The site check re-scores the group using
          ONLY oropharynx patients — everyone the same location. The two
          numbers in the chip read <em>full-cohort → oropharynx-only</em>.
        </Item>
        <Item label="Purity">
          a tumour sample is a mix of cancer cells and immune cells; HPV+
          tumours carry more immune cells, so a gene could look like an
          &ldquo;HPV gene&rdquo; just by marking immune content. The
          purity check re-scores using only the &ldquo;purest&rdquo;
          (least-immune) tumours. It&rsquo;s usually <Mono>—</Mono> here
          because those tumours include almost no HPV+ patients, so
          there&rsquo;s nothing to test.
        </Item>
        <Item label="Reading the chips">
          <span className="font-semibold" style={{ color: "#2F6E4C" }}>
            ✓
          </span>{" "}
          = held up when the confounder was held constant (likely real
          signal).{" "}
          <span className="font-semibold" style={{ color: "#A64242" }}>
            ✗
          </span>{" "}
          = dropped past the tolerance (part of it was the confounder).{" "}
          <Mono>—</Mono> = couldn&rsquo;t test (too few patients per
          class in the subgroup).
        </Item>
        <Caption>
          Tolerance is 0.05 AUROC: a subgroup AUROC within 0.05 of the
          full-cohort AUROC counts as &ldquo;survives.&rdquo; HNSC/HPV
          coherence-on runs only — other (dataset, target) pairs omit
          this column.
        </Caption>
      </>
    ),
  },
};

function ObjectiveIntro() {
  return (
    <p className="mb-4 rounded-md border border-border bg-bg p-3 text-[12.5px] leading-relaxed text-ink">
      An objective is the rule that scores every program — the fitness the
      engine maximises. It&rsquo;s computed only from the program&rsquo;s
      per-patient output (and, for the supervised objectives, a target
      column — MSI, TMB, or HPV — never gene names). It sets what
      &ldquo;good&rdquo; means; the engine then composes DSL programs to
      satisfy it.
    </p>
  );
}

function ObjectiveFooter() {
  return (
    <p className="mt-4 border-t border-border pt-3 text-[11.5px] italic text-muted">
      What the program chooses is how to build the score and how to
      compare it — a raw association (Associate) or a confounder-adjusted
      one (Effect), plus the correlation kind. What stays outside the DSL
      is the compass: the target it&rsquo;s scored against, in which
      direction, judged honestly on held-out data. The program can&rsquo;t
      pick the target — that would let the answer into the language.
    </p>
  );
}

// Unsupervised has no target / no Associate / no Effect — its footer
// is the deeper invariant: the program can't see a target to optimise
// against, which is exactly what makes a downstream label-alignment
// meaningful.
function UnsupObjectiveFooter() {
  return (
    <p className="mt-4 border-t border-border pt-3 text-[11.5px] italic text-muted">
      The only thing outside the DSL here is &ldquo;find the cleanest
      split, judged honestly on held-out data&rdquo; — there is no target,
      and the program can&rsquo;t see one. That&rsquo;s exactly what makes
      it meaningful when the split turns out to line up with a known
      label (MSI on the colorectal cohort, HPV on head &amp; neck): the
      engine wasn&rsquo;t told to look for it.
    </p>
  );
}
