/* Story-stimulus section for the June 11 update.
   - The comparison memo renders verbatim as the framing read.
   - Each frame's full menu is organized into the six categories of a sticky idea
     (Simple, Unexpected, Concrete, Credible, Emotional, Story). Each category is a
     card that leads with its strongest, star-marked items; expanding a card reveals
     the full menu. A filter row narrows the view to a single category so the page
     never reads as one long list. A frame selector switches Notch / Longevity.
   Data: NOTCH_STIM / NOTCH_POSTURE, LONGEVITY_STIM / LONGEVITY_POSTURE (window globals). */

const STIM_SECTIONS = [
  "What this idea is, in the fewest words",
  "Compact sayings",
  "Visual proverbs",
  "Existing schemas to work with or against",
  "High-concept pitches",
  "Generative analogies",
  "Pattern-breaks that earn the brow-raise",
  "Mysteries that hold attention",
  "Knowledge gaps worth highlighting",
  "News-style teasers",
  "Fables we could leverage or invent",
  "Abstractions made concrete",
  "Concrete contexts",
  "People worth including",
  "Hooks that increase memorability",
  "Common ground with the audience",
  "Authority and counter-authority voices",
  "Convincing details",
  "Statistics made human-scale",
  "Hardest-case proofs",
  "Testable credentials",
  "Emotional tank fillers",
  "The one rather than the many",
  "What the audience most cares about",
  "Words that fight semantic stretch",
  "Self-interest beyond the base",
  "Identity claims to offer",
  "Stories that show how to act",
  "Stories that give energy to act",
  "Attention-grabbing questions, primary customer",
  "Shocking facts, primary customer",
  "Attention-grabbing questions, investors and funders",
  "Shocking facts, investors and funders",
];

/* The six categories of a sticky idea (Heath, SUCCESs), in order. */
const HEATH = [
  { key: "simple", name: "Simple", desc: "Find the core, and the words that make it stick." },
  { key: "unexpected", name: "Unexpected", desc: "Break the pattern, open a gap, hold attention." },
  { key: "concrete", name: "Concrete", desc: "Make it touchable, specific, sensory." },
  { key: "credible", name: "Credible", desc: "Proof the claim can carry, inside and out." },
  { key: "emotional", name: "Emotional", desc: "Make them feel it, and feel it about themselves." },
  { key: "story", name: "Story", desc: "Stories that simulate, and stories that give energy." },
];

/* Each of the 33 sub-sections mapped to its Heath category (index-aligned to *_STIM). */
const SECTION_CAT = [
  "simple", "simple", "simple", "simple", "simple", "simple",
  "unexpected", "unexpected", "unexpected", "unexpected",
  "story",
  "concrete", "concrete", "concrete",
  "simple",
  "emotional",
  "credible", "credible", "credible", "credible", "credible",
  "emotional", "emotional", "emotional",
  "concrete",
  "emotional", "emotional",
  "story", "story",
  "unexpected", "unexpected", "unexpected", "unexpected",
];

function stimMd(s) {
  return String(s)
    .replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>")
    .replace(/\*([^*]+)\*/g, "<i>$1</i>");
}

function StimP({ md, className }) {
  return <p className={className} dangerouslySetInnerHTML={{ __html: stimMd(md) }}></p>;
}

/* Shared "kept ideas" store, persisted to localStorage so a shortlist survives reload. */
const KeptContext = React.createContext(null);

function useKeptStore() {
  const [kept, setKept] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem("protgen_kept") || "{}"); } catch (e) { return {}; }
  });
  React.useEffect(() => {
    try { localStorage.setItem("protgen_kept", JSON.stringify(kept)); } catch (e) {}
  }, [kept]);
  const toggle = (id, payload) => setKept((prev) => {
    const next = Object.assign({}, prev);
    if (next[id]) delete next[id]; else next[id] = payload;
    return next;
  });
  const remove = (id) => setKept((prev) => { const next = Object.assign({}, prev); delete next[id]; return next; });
  const clear = () => setKept({});
  return { kept, toggle, remove, clear };
}

/* One selectable stimulus item: a keep toggle (star) plus the text. Text stays selectable. */
function StimItem({ id, text, starred, payload }) {
  const ctx = React.useContext(KeptContext);
  const kept = !!(ctx && ctx.kept[id]);
  return (
    <li data-starred={starred ? "true" : undefined} className={kept ? "is-kept" : ""}>
      <button
        type="button"
        className="keep-btn"
        aria-pressed={kept}
        title={kept ? "Remove from shortlist" : "Keep this"}
        aria-label={kept ? "Remove from shortlist" : "Keep this idea"}
        onClick={() => ctx && ctx.toggle(id, payload)}
      >
        <svg width="15" height="15" viewBox="0 0 24 24" fill={kept ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
      </button>
      <span className="stim-item-text" dangerouslySetInnerHTML={{ __html: stimMd(text) }}></span>
    </li>
  );
}

/* Render a cell's items as one or more lists, with "» " group labels between them. */
function StimItems({ items, frameKey, secIdx }) {
  if (!items || !items.length) {
    return <p className="stim-absent"><i>(not surfaced in this voice)</i></p>;
  }
  const out = [];
  let bucket = [];
  let uli = 0;
  const flush = () => {
    if (bucket.length) { out.push(<ul className="stim-list" key={"ul" + uli++}>{bucket}</ul>); bucket = []; }
  };
  items.forEach((raw, i) => {
    if (raw.indexOf("» ") === 0) {
      flush();
      out.push(<div className="stim-grouplbl" key={"g" + i} dangerouslySetInnerHTML={{ __html: stimMd(raw.slice(2)) }}></div>);
      return;
    }
    const star = raw.indexOf("★ ") === 0;
    const text = star ? raw.slice(2) : raw;
    const id = frameKey + ":" + secIdx + ":" + i;
    bucket.push(<StimItem key={i} id={id} text={text} starred={star} payload={{ t: text, fr: frameKey, sec: secIdx }} />);
  });
  flush();
  return <React.Fragment>{out}</React.Fragment>;
}

function StimCell({ entry, frameKey, secIdx }) {
  if (entry && entry.quote) {
    return (
      <React.Fragment>
        <div className="stim-core-lbl">The core to test</div>
        <blockquote className="stim-core">{entry.quote}</blockquote>
      </React.Fragment>
    );
  }
  return <StimItems items={entry && entry.items} frameKey={frameKey} secIdx={secIdx} />;
}

/* The comparison memo, rendered verbatim (sans its "Rendering notes" section). */
function StimMemo() {
  return (
    <div className="stim-memo">
      <p className="stim-memo-byline"><i>A framing note before the menus. Drafted 2026-06-10.</i></p>

      <h4>What's the same in both</h4>
      <StimP md={`Both frames are honest to ProTGen. Both center the same biology, targeted Notch activation, thymic restoration, immune competence, and both carry the same Act I credibility anchor (ProT-096 in IND-readying with Cellares as manufacturing partner). Both rest on the same team and the same indication ladder (post-treatment immune reconstitution; recovery from major surgery; vaccine adjuvancy; healthier lifespan).`} />
      <StimP md={`They differ at the layer of *what the protagonist of the story is*.`} />

      <h4>What's different</h4>
      <StimP md={`**Notch frame.** The mechanism is the protagonist. The story leads with what makes ProTGen scientifically distinct: the targeted-Notch-activator platform as the upstream control point for restoring immune competence. The indications are the proof that the protagonist works. The room walks out remembering *a control point nobody else owns*.`} />
      <StimP md={`**Longevity frame.** The patient is the protagonist. The story leads with what the world wants: more years lived well. The mechanism appears as the explanation for why this version of longevity is real where most others are aspirational. The room walks out remembering *the company that doesn't sound like the others, because there's a clinical asset behind the language*.`} />
      <StimP md={`The difference isn't science. The difference is whose face the audience sees first.`} />

      <h4>Who each frame pulls toward; who it loses</h4>
      <div className="stim-memo-pull">
        <div>
          <p className="stim-memo-fr">Notch frame</p>
          <p className="stim-memo-cap"><b>Pulls hardest with.</b></p>
          <ul>
            <li>Sophisticated biotech investors who underwrite mechanism</li>
            <li>Pharma BD scouts looking for upstream control points</li>
            <li>Cell-therapy specialists who already know what Notch is</li>
            <li>Scientific recruits who want hard, translational problems</li>
            <li>Clinical investigators who want a serious biological rationale</li>
          </ul>
          <p className="stim-memo-cap"><b>Risks losing.</b></p>
          <ul>
            <li>Longevity-investor audiences that want outcome-shaped language</li>
            <li>General-press or non-specialist readers who bounce off "Notch"</li>
            <li>The warm-intro forwarded-PDF reader with 60 seconds and no vocabulary</li>
            <li>Mainstream BD audiences when "platform" reads as too-broad</li>
          </ul>
        </div>
        <div>
          <p className="stim-memo-fr">Longevity frame</p>
          <p className="stim-memo-cap"><b>Pulls hardest with.</b></p>
          <ul>
            <li>Longevity capital (Altman-circle; Evolution; Emirates funds; healthspan-thesis allocators)</li>
            <li>Generalist investors who want an IC-defensible longevity position</li>
            <li>Pharma BD on the aging side of their portfolio</li>
            <li>Consumer-facing press</li>
            <li>Warm-intro forwarded-PDF readers who don't know what Notch is but know what "stay sharp at 80" means</li>
            <li>The longevity-conscious 55-year-old who's buying supplements and wants to know what's real</li>
          </ul>
          <p className="stim-memo-cap"><b>Risks losing.</b></p>
          <ul>
            <li>Sophisticated biotech investors who flinch at "longevity" as a category label</li>
            <li>Pharma BD scouts looking for clinical-asset-grade differentiation</li>
            <li>Cell-therapy specialists who'd rather hear "progenitor T-cell therapy in refractory hematologic malignancy"</li>
            <li>Clinical investigators who suspect "longevity" of being downstream of patient-care priorities</li>
          </ul>
        </div>
      </div>

      <h4>The trade-off named honestly</h4>
      <StimP md={`The Notch frame buys conviction at the cost of audience size. It is the right voice for the conversations where mechanism diligence is the gating question. It risks being too dense for the room that hasn't done that diligence yet.`} />
      <StimP md={`The Longevity frame buys audience reach at the cost of conviction depth. It is the right voice for the conversations where outcome and category fit are the gating questions. It risks sounding like every other longevity company until the Act I credibility anchor lands, which means the artifact has to do that landing fast.`} />
      <StimP md={`Neither is wrong. The question isn't *which is true*. The question is *which voice does ProTGen want to be heard in first, in which room, with which goal*.`} />

      <h4>A possible reading: it's a toolkit, not a single pick</h4>
      <StimP md={`The two frames are sequenceable for the same conversation. Open with the Longevity frame for warmth and pull. Move to the Notch frame when the room shows it can hold the mechanism. The pivot is one sentence: *"and the reason this is real and not the next vibes-and-supplements company is that we have a clinical-stage asset on a targeted Notch-activator platform, let me show you what that means."*`} />
      <StimP md={`It is also sequenceable across conversations. Use the Longevity frame to *get the meeting*; use the Notch frame to *win the meeting*. The forwarded-PDF reader engages with Longevity. The IC partner you eventually pitch wants Notch.`} />
      <StimP md={`The risk of using only one: addressed audiences narrows in either direction. The risk of trying to do both at once in one artifact: dilutes both. Sequencing, across conversations or within one conversation, is the move that keeps both available.`} />

      <h4>The question to settle</h4>
      <StimP md={`Two ways to ask it, depending on which is more useful right now:`} />
      <StimP md={`**The voice question.** *"Which of these sounds more like you when you say it out loud at a BIO booth?"* Whichever lands is the voice ProTGen leads with in the first thirty seconds of any conversation.`} />
      <StimP md={`**The audience question.** *"Which conversations do you want to be having more of at BIO, the mechanism-deep ones or the category-wide ones?"* Whichever you want more of is the frame to put in front first.`} />
      <StimP md={`There is a third question worth holding: *"Is this a 12-month decision or a 12-hour one?"* The 12-hour answer determines what gets carried at this BIO. The 12-month answer determines which conversations ProTGen positions itself to have for the next year of fundraising and BD.`} />

      <h4>A small recommendation, take or leave</h4>
      <StimP md={`If forced to pick one for BIO 2026, the Longevity frame is the safer first carry, because the pharma-BD-as-open-conversation posture is closer to the Longevity frame's invitation shape, because the warm-intro forwarded-PDF reader is a high-volume real audience at BIO, and because the Lilly TuneLabs entry happens to be the place where the Notch frame's mechanism specificity shows up anyway (since that's the named-door variant of the one-pager).`} />
      <StimP md={`That's a recommendation, not a conclusion. If the gut says Notch, the gut is also right, for a different reason and a different game.`} />

      <h4>How this stimulus is meant to be used</h4>
      <StimP md={`Read either frame's menu. Star what lands. Cross out what doesn't. Add what's missing.`} />
      <StimP md={`The two frames are stimulus material; they don't have to become the one-pagers. They might end up shaping the BIO booth opening line, the cold-email subject line, the social posts to queue, the way to answer "tell me about ProTGen" in line at a coffee station. The one-pagers already drafted Thursday carry one frame each (umbrella variant = Longevity; Lilly TuneLabs variant = Notch). The stimulus is for everything around them.`} />
    </div>
  );
}

/* helper: count items in a category for a frame's data */
function catCounts(secIdxs, data) {
  let curated = 0, total = 0;
  secIdxs.forEach((i) => {
    const e = data[i];
    if (e && e.quote) { curated += 1; total += 1; return; }
    if (e && e.items) {
      e.items.forEach((it) => {
        if (it.indexOf("» ") === 0) return;
        total += 1;
        if (it.indexOf("★ ") === 0) curated += 1;
      });
    }
  });
  return { curated, total };
}

/* One Heath-category card: curated items by default, full menu on expand. */
function CategoryCard({ cat, data, frameKey }) {
  const [expanded, setExpanded] = React.useState(false);
  const secIdxs = SECTION_CAT.map((c, i) => (c === cat.key ? i : -1)).filter((i) => i >= 0);
  const { curated, total } = catCounts(secIdxs, data);

  const renderSecs = secIdxs.filter((i) => {
    const e = data[i];
    if (!e) return false;
    if (e.quote) return true;
    if (!e.items) return false;
    if (expanded) return true;
    return e.items.some((it) => it.indexOf("★ ") === 0);
  });

  return (
    <section className="cat-card">
      <header className="cat-head">
        <div>
          <div className="cat-name">{cat.name}</div>
          <div className="cat-desc">{cat.desc}</div>
        </div>
        <div className="cat-count">{expanded ? total + " items" : curated + " curated"}</div>
      </header>
      <div className={"cat-body " + (expanded ? "view-full" : "view-curated")}>
        {renderSecs.map((i) => (
          <div className="cat-sub" key={i}>
            <div className="cat-sub-h">{STIM_SECTIONS[i]}</div>
            <StimCell entry={data[i]} frameKey={frameKey} secIdx={i} />
          </div>
        ))}
      </div>
      {total > curated && (
        <button type="button" className="cat-expand" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>
          {expanded ? "Show fewer" : "Show full menu (" + (total - curated) + " more)"}
        </button>
      )}
    </section>
  );
}

/* A single frame's panel: category filter on top, the six category cards, then the posture.
   Filters sit directly above the cards they reveal so nothing has to be scrolled past. */
function FramePanel({ frameKey }) {
  const [filter, setFilter] = React.useState("simple");
  const isNotch = frameKey === "notch";
  const posture = isNotch ? NOTCH_POSTURE : LONGEVITY_POSTURE;
  const data = isNotch ? NOTCH_STIM : LONGEVITY_STIM;
  const cats = HEATH.filter((c) => c.key === filter);
  return (
    <React.Fragment>
      <div className="cat-filter" role="group" aria-label="Filter the menu">
        <button type="button" className={"chip chip-posture" + (filter === "posture" ? " on" : "")} aria-pressed={filter === "posture"} onClick={() => setFilter("posture")}>{posture.label}</button>
        {HEATH.map((c) => (
          <button type="button" key={c.key} className={"chip" + (filter === c.key ? " on" : "")} aria-pressed={filter === c.key} onClick={() => setFilter(c.key)}>{c.name}</button>
        ))}
      </div>
      {filter === "posture" ? (
        <div className="dd-posture-block">
          {posture.paras.map((p, i) => <StimP key={i} md={p} className="stim-posture-p" />)}
        </div>
      ) : (
        <div className="cat-grid">
          {cats.map((c) => <CategoryCard key={frameKey + "-" + c.key} cat={c} data={data} frameKey={frameKey} />)}
        </div>
      )}
    </React.Fragment>
  );
}

/* Disclosure card: press the header to reveal its content (single-open accordion). */
function DiscCard({ id, title, sub, open, onToggle, badge, children }) {
  return (
    <section className={"disc" + (open ? " is-open" : "")}>
      <button type="button" className="disc-head" aria-expanded={open} aria-controls={id + "-body"} onClick={onToggle}>
        <span className="disc-headings">
          <span className="disc-title">{title}{badge}</span>
          <span className="disc-sub">{sub}</span>
        </span>
        <svg className="disc-chev" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>
      </button>
      {open && <div className="disc-body" id={id + "-body"}>{children}</div>}
    </section>
  );
}

/* The working canvas: a shortlist of kept ideas (grouped by frame) plus a notes field.
   Both persist to localStorage so the canvas can be reopened or used live in a meeting. */
function ShortlistCard() {
  const ctx = React.useContext(KeptContext);
  const [notes, setNotes] = React.useState(() => {
    try { return localStorage.getItem("protgen_notes") || ""; } catch (e) { return ""; }
  });
  React.useEffect(() => {
    try { localStorage.setItem("protgen_notes", notes); } catch (e) {}
  }, [notes]);

  const catName = (k) => { const c = HEATH.find((x) => x.key === k); return c ? c.name : k; };
  const frameName = (fr) => (fr === "notch" ? "Notch frame" : "Longevity frame");
  const ids = Object.keys(ctx.kept);
  const groups = { notch: [], longevity: [] };
  ids.forEach((id) => { const p = ctx.kept[id]; if (groups[p.fr]) groups[p.fr].push({ id, p }); });

  return (
    <div className="canvas">
      <div className="canvas-cols">
        <div className="canvas-shortlist">
          <div className="canvas-lbl">Shortlist <span className="canvas-count">{ids.length}</span></div>
          {ids.length === 0 ? (
            <p className="canvas-empty">Tap the star on any item, the ones that land, and they collect here for a quick read-back.</p>
          ) : (
            <React.Fragment>
              {["notch", "longevity"].map((fr) => (groups[fr] && groups[fr].length > 0) ? (
                <div className="canvas-group" key={fr}>
                  <div className="canvas-group-h">{frameName(fr)}</div>
                  <ul className="canvas-list">
                    {groups[fr].map(({ id, p }) => (
                      <li key={id}>
                        <button type="button" className="canvas-remove" aria-label="Remove from shortlist" title="Remove" onClick={() => ctx.remove(id)}>
                          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
                        </button>
                        <span className="canvas-item"><span className="canvas-tag">{catName(SECTION_CAT[p.sec])}</span><span dangerouslySetInnerHTML={{ __html: stimMd(p.t) }}></span></span>
                      </li>
                    ))}
                  </ul>
                </div>
              ) : null)}
              <button type="button" className="canvas-clear" onClick={() => ctx.clear()}>Clear shortlist</button>
            </React.Fragment>
          )}
        </div>
        <div className="canvas-notes">
          <label className="canvas-lbl" htmlFor="canvas-notes-ta">Notes &amp; questions</label>
          <textarea id="canvas-notes-ta" className="canvas-ta" placeholder="Type notes, questions, or what's missing. Saved on this device." value={notes} onChange={(e) => setNotes(e.target.value)}></textarea>
        </div>
      </div>
    </div>
  );
}

function StoryStimulus() {
  const store = useKeptStore();
  const [open, setOpen] = React.useState(null);
  const toggle = (k) => setOpen(open === k ? null : k);
  const keptCount = Object.keys(store.kept).length;

  return (
    <KeptContext.Provider value={store}>
      <h2 id="story-stimulus">Story stimulus</h2>
      <p>We turned each frame into a menu you can react to. Open the comparison for the framing read, or open a frame to read it by the six categories of a sticky idea. Tap the star on what lands; it collects in your shortlist, alongside a notes field we can work in together.</p>

      <div className="disc-stack" id="deep-dives">
        <DiscCard id="disc-comparison" title="Comparison" sub="How we read the two frames, side by side" open={open === "comparison"} onToggle={() => toggle("comparison")}>
          <StimMemo />
        </DiscCard>
        <DiscCard id="disc-notch" title="Notch frame" sub="Mechanism-anchored, organized by category" open={open === "notch"} onToggle={() => toggle("notch")}>
          <FramePanel frameKey="notch" />
        </DiscCard>
        <DiscCard id="disc-longevity" title="Longevity frame" sub="Outcome-anchored, organized by category" open={open === "longevity"} onToggle={() => toggle("longevity")}>
          <FramePanel frameKey="longevity" />
        </DiscCard>
        <DiscCard id="disc-canvas" title="Shortlist & notes" sub="What landed, plus a place to think out loud" open={open === "canvas"} onToggle={() => toggle("canvas")} badge={keptCount > 0 ? <span className="disc-badge">{keptCount}</span> : null}>
          <ShortlistCard />
        </DiscCard>
      </div>
    </KeptContext.Provider>
  );
}

Object.assign(window, { StoryStimulus });
