/* global React, window */
/* Shared chat thread: the intake-loading interaction pattern (three named
   specialists, tool calls and thoughts revealing with dots) reused wherever
   MakerChat answers — study intake and the study result page.
   Persona list and step CSS come from explore-loading (window.IL2 / il2.css). */
const { useState: atS, useEffect: atE } = React;

const AT_P = () => (window.IL2 && window.IL2.personas) || [];
const atPOf = id => AT_P().find(x => x.id === id) || { name: "MakerChat", icon: "sparkles" };

function ATPills({ active }) {
  const p = atPOf(active);
  return <span className="at-who"><i className={"ti ti-" + p.icon}></i></span>;
}

function ATStep({ s, live, lineCount, open, onToggle }) {
  const p = atPOf(s.p);
  return (
    <div className="l2-step">
      <button className={"l2-stephd" + (live ? " live" : "")} onClick={onToggle}>
        <ATPills active={s.p} />
        <span className="l2-lbl">
          <b>{s.kind === "tools" ? (live ? "Synthesizing" : s.label) : s.label}</b>
          {live && <span className="l2-dots"><i></i><i></i><i></i></span>}
        </span>
        <i className={"ti ti-chevron-" + (open ? "up" : "down") + " chv"}></i>
      </button>
      {open && (
        <div className="l2-stepbd">
          {s.kind === "tools"
            ? s.lines.map((l, i) => <div key={l} className={"l2-line" + (!live || i < lineCount ? " on" : "")}>{l}</div>)
            : <div className="l2-think">{s.text}</div>}
        </div>
      )}
    </div>
  );
}

const atBold = t => t.split("**").map((x, j) => j % 2 ? <b key={j}>{x}</b> : x);

/* what the user actually answered, free text included */
function ATLabel(q, val) {
  if (val === undefined) return "";
  const o = q.opts.find(x => x.id === val);
  return o ? (o.short || o.label) : String(val);
}

/* A question can depend on an earlier answer — "which teams?" only exists once
   someone says other teams are reading it. Asking it up front would be asking
   about a situation that may not apply. */
const atVisible = (card, ans) => (card || []).filter(q => !q.when || q.when(ans || {}));

/* One question on screen at a time. Options are a list, not a wrapped field of
   pills: pills reflow by length so the eye has to hunt, one option per line can
   be scanned straight down. */
function ATQuestion({ q, picked, onAnswer }) {
  const [txt, setTxt] = atS("");
  atE(() => { setTxt(""); }, [q.qid]);
  const custom = picked !== undefined && !q.opts.some(o => o.id === picked);
  const send = () => { const v = txt.trim(); if (v) onAnswer(q.qid, v); };
  return (
    <div className="at-q">
      <div className="at-q__l">{q.label}</div>
      <div className="at-list">
        {q.opts.map((o, k) => (
          <button key={o.id}
            className={"at-li" + (picked === o.id ? " on" : "") + (o.primary ? " at-li--primary" : "")}
            onClick={() => onAnswer(q.qid, o.id)}>
            <span className="at-li__n">{k + 1}</span>
            <span className="at-li__t">{o.label}</span>
            {o.inferred && <span className="at-li__tag">suggested</span>}
          </button>
        ))}
        {/* The custom answer is a row like any other, with its field already
            open. Origami does this too: making people click "Other" first
            hides the escape hatch behind a step. */}
        <div className={"at-li at-li--free" + (custom ? " on" : "")}>
          <span className="at-li__n">{q.opts.length + 1}</span>
          <input value={custom && !txt ? String(picked) : txt}
            placeholder="Type a custom answer…"
            onChange={e => setTxt(e.target.value)}
            onKeyDown={e => { if (e.key === "Enter") send(); }} />
          {txt.trim() && <button className="at-li__go" onClick={send}><i className="ti ti-arrow-up"></i></button>}
        </div>
      </div>
    </div>
  );
}

/* The dock: pinned above the composer, one question showing, every question in
   the set reachable by its own tab.

   Back and Next walked the set in one direction and made the final answer
   double as the submit — so the moment the user committed to anything was the
   moment they answered the last question, with no chance to look back over the
   set first. Tabs say how many questions there are, let the user start
   anywhere and revise anything, and hold every answer until one Submit. A lone
   question has nothing to hold back for, so it commits on the click. */
function ATDock({ pending, answers, onAnswer }) {
  const key = pending ? pending.key : null;
  const [draft, setDraft] = atS({});
  const [step, setStep] = atS(0);
  atE(() => { setDraft({}); setStep(0); }, [key]);
  if (!pending) return null;

  const card = pending.c.card;
  const single = card.length === 1 && !card.some(q => q.when);
  const merged = Object.assign({}, answers, draft);
  const qs = atVisible(card, merged);
  const i = Math.min(step, qs.length - 1);
  const q = qs[i];
  const left = qs.filter(x => merged[x.qid] === undefined).length;

  const pick = (qid, val) => {
    if (single) { onAnswer({ [qid]: val }); return; }
    const next = Object.assign({}, draft, { [qid]: val });
    setDraft(next);
    /* land on the next thing still unanswered, wrapping, so filling the set in
       order costs one click a question and answering out of order does not
       strand the user at the end of the list */
    const now = Object.assign({}, answers, next);
    const vis = atVisible(card, now);
    const open = vis.map((x, k) => k).filter(k => now[vis[k].qid] === undefined);
    const at = vis.findIndex(x => x.qid === qid);
    if (open.length) {
      const ahead = open.find(k => k > at);
      setStep(ahead === undefined ? open[0] : ahead);
    }
  };
  const submit = () => { if (!left) onAnswer(draft); };

  return (
    <div className="at-dock">
      <div className="at-card is-live">
        <div className="at-card__h">
          <i className="ti ti-message-2-question"></i>
          <b>{pending.c.cardTitle || "Tell me more about your needs"}</b>
        </div>
        {!single && (
          <div className="at-tabs">
            {qs.map((x, k) => {
              const done = merged[x.qid] !== undefined;
              return (
                <button key={x.qid} className={"at-tab" + (k === i ? " on" : "") + (done ? " is-done" : "")}
                  title={x.tab || x.label}
                  onClick={() => setStep(k)}>
                  {done ? <i className="ti ti-check"></i> : <span className="at-tab__n">{k + 1}</span>}
                </button>
              );
            })}
          </div>
        )}
        <ATQuestion q={q} picked={merged[q.qid]} onAnswer={pick} />
        {!single && (
          <div className="at-nav">
            <button className="at-sub" disabled={!!left} onClick={submit}>
              {left ? left + " left to answer" : "Submit"}
              {!left && <i className="ti ti-arrow-right"></i>}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

/* One flat timeline of events so the reveal reads as one continuous session.

   A turn's steps used to render as one row per tool call or thought — up to
   three or four stacked headers for a single turn. Dan's feedback (2026-08-26):
   nobody can follow that live in a demo. Folded into one collapsed row per
   turn instead — a single beat, expandable if someone wants the detail. */
function atEvents(turns) {
  const ev = [];
  (turns || []).forEach((t, ti) => {
    if (t.me) ev.push({ kind: "me", text: t.me, key: "m" + ti });
    if (t.steps && t.steps.length) {
      const lines = [];
      t.steps.forEach(s => { if (s.kind === "tools") lines.push(...s.lines); else lines.push(s.text); });
      const toolCount = t.steps.filter(s => s.kind === "tools").reduce((n, s) => n + s.lines.length, 0);
      const merged = { p: t.steps[0].p, kind: "tools", label: (toolCount || lines.length) + " tools used", lines };
      ev.push({ kind: "step", s: merged, key: "s" + ti });
    }
    // a reply may be a function of the answers so far, so a question can
    // actually change what comes next rather than just being recorded
    if (t.reply) ev.push({ kind: "reply", text: t.reply, done: t.done, key: "r" + ti });
    /* A gate or a card halts the reveal. Everything after it in the script is
       a consequence of the answer, so it cannot run ahead of one. */
    if (t.card) ev.push({ kind: "card", c: t, key: "c" + ti });
  });
  return ev;
}
const atCost = e => e.kind === "step" ? (e.s.kind === "tools" ? e.s.lines.length + 1 : 3) : e.kind === "reply" ? 2 : 1;
// a card holds one or more questions and is not done until every one that
// applies has been answered
const atAnswered = (e, ans) =>
  e.kind !== "card" || atVisible(e.c.card, ans).every(q => !!ans && ans[q.qid] !== undefined);

function AgentThread({ turns, animate = true, tick = 460, answers, onAnswer, onPending }) {
  const ev = atEvents(turns);
  /* Mounting without animation used to reveal the whole script, replies
     included — the consequences of a question nobody had answered yet. Start
     at the first outstanding question instead: everything before it has
     happened, nothing after it has. */
  const [i, setI] = atS(() => {
    if (animate) return 0;
    const b = ev.findIndex(e => e.kind === "card" && !atAnswered(e, answers));
    return b >= 0 ? b : ev.length - 1;
  });
  const [sub, setSub] = atS(0);
  const [openId, setOpenId] = atS(null);
  const anchor = React.useRef(null);
  // The thread renders inside the caller's .i3-log, so find that scroller and
  // keep it pinned to the newest event — but only while the user is near the
  // bottom, so scrolling back to re-read isn't yanked away.
  atE(() => {
    const el = anchor.current && anchor.current.closest(".i3-log");
    if (!el) return;
    const gap = el.scrollHeight - el.scrollTop - el.clientHeight;
    if (gap < 160) el.scrollTop = el.scrollHeight;
  }, [i, sub]);
  // tell the caller what to dock, once the reveal has actually reached it
  atE(() => {
    if (!onPending) return;
    const b = ev.findIndex((e, k) => k <= i && e.kind === "card" && !atAnswered(e, answers));
    onPending(b >= 0 ? ev[b] : null);
  }, [i, answers, ev.length]);
  const atBlock = (from) => ev.findIndex((e, k) => k >= from && e.kind === "card" && !atAnswered(e, answers));
  atE(() => {
    if (!ev.length || i >= ev.length - 1) return;
    // hold at an unanswered question rather than revealing its consequences
    if (!atAnswered(ev[i], answers)) return;
    if (!animate) {
      // already waited once, in the loading state: jump to the next question
      const next = atBlock(i + 1);
      setI(next >= 0 ? next : ev.length - 1);
      setSub(0);
      return;
    }
    const id = setInterval(() => {
      setSub(s => {
        if (s + 1 >= atCost(ev[i])) { setI(k => Math.min(ev.length - 1, k + 1)); return 0; }
        return s + 1;
      });
    }, tick);
    return () => clearInterval(id);
  }, [i, animate, ev.length, tick, answers]);
  if (!ev.length) return null;
  const done = i >= ev.length - 1 && sub >= atCost(ev[ev.length - 1]) - 1 && atAnswered(ev[i], answers);
  return (
    <React.Fragment>
      {ev.slice(0, i + 1).map((e, k) => {
        if (e.kind === "step") {
          const live = animate && k === i && !done;
          return <ATStep key={e.key} s={e.s} live={live} lineCount={sub}
            open={openId === k} onToggle={() => setOpenId(openId === k ? -1 : k)} />;
        }
        if (e.kind === "card") {
          if (!atAnswered(e, answers)) return null;
          return (
            <div className="at-rec at-rec--card" key={e.key}>
              <i className="ti ti-circle-check-filled"></i>
              <span>{atVisible(e.c.card, answers).map(q => ATLabel(q, answers[q.qid])).join(" · ")}</span>
            </div>
          );
        }
        return (
          <div key={e.key} className={"i3-msg " + (e.kind === "me" ? "me" : "ai")} style={e.kind === "me" ? { marginTop: k ? 6 : 0, marginBottom: 4 } : { marginBottom: 6 }}>
            {atBold(typeof e.text === "function" ? e.text(answers || {}) : e.text)}
            {e.done && <div className="i3-msg__done"><i className="ti ti-circle-check-filled"></i>{typeof e.done === "function" ? e.done(answers || {}) : e.done}</div>}
          </div>
        );
      })}
      <div ref={anchor} style={{ height: 1, flex: "none" }}></div>
    </React.Fragment>
  );
}

/* ============================================================
   The plan: four answers in, one sized and priced study out.

   Both the chat and the rail read this, so the number MakerChat says out loud
   and the number in DECISION CONTEXT can never drift apart.

   Confidence is the one word for the axis — "certainty", "insight depth" and
   "fidelity" were all names for the same thing. The user can pick it outright
   or hand it back: "decide for me" is a real answer, derived from who reads
   the results and what the user will spend, because those are things a
   merchant can actually answer.
   ============================================================ */
const AT_BAR = {
  directional: { tier: "quick",    label: "Directional", blurb: "Enough to shortlist. Not enough to defend in a range review." },
  solid:       { tier: "standard", label: "Solid",       blurb: "Significance-checked. Holds up in the room." },
  defensible:  { tier: "extended", label: "Defensible",  blurb: "A full consumer panel behind every claim." }
};
const AT_BARS = ["directional", "solid", "defensible"];
/* What the deadline allows, in days. "A few days" carries 5 rather than 3
   because 5 is the deepest study we run — a merchant saying "a few days" is
   not asking us to cut depth, and the rail should not tell them they are late
   for the one tier that takes the longest. */
const AT_WHEN = { hours: 0, today: 0, days: 5, open: 999 };
/* what each tier needs */
/* Delivery is fielding + one day, at every depth. The gap between "fielding
   closed" and "results in your hands" is the whole end-to-end argument, so it
   is one day by construction rather than however long a queue happens to be. */
const AT_LEAD = { quick: [0, "about 30 minutes"], standard: [2, "48 hours"], extended: [5, "5 days"] };
/* Depth does not buy a different study, it buys a different mix. A deeper read
   leans harder on recruited shoppers; a fast read leans on Digital Twins. A
   same-day study is Twins either way — nobody recruits a shopper in half an
   hour. */
const AT_MIX = { quick: 1, standard: 0.48, extended: 0.20 };

function AT_PLAN(a) {
  a = a || {};
  const auto = AT_BARS.indexOf(a.conf) < 0;
  let bar = auto ? null : a.conf;
  if (auto) {
    /* Nobody can answer "what confidence interval do you need". Everyone can
       answer who reads it. */
    let k = { self: 0, team: 1, cross: 1, exec: 2 }[a.who];
    if (k === undefined) k = a.who ? 1 : null;   // a typed-in reader is still a reader
    if (k === null) return null;                 // not enough on the table to size on
    if (a.when === "today" || a.when === "hours") k = 0;   // same day can only be Twins
    bar = AT_BARS[Math.max(0, Math.min(2, k))];
  }
  const B = AT_BAR[bar];
  const info = ((window.AT_TIER_INFO || {})[B.tier]) || { total: 0, markets: "" };
  const share = AT_MIX[B.tier];
  const total = info.total;
  const twins = Math.round(total * (share === undefined ? 0.48 : share));
  const humans = total - twins;
  const lead = AT_LEAD[B.tier];
  const allow = AT_WHEN[a.when];
  return {
    bar: bar, tier: B.tier, auto: auto, label: B.label, blurb: B.blurb,
    total: total, twins: twins, humans: humans,
    leadLabel: lead[1], late: allow !== undefined && lead[0] > allow,
    markets: info.markets
  };
}
const atLower = s => s.charAt(0).toLowerCase() + s.slice(1);

/* ---------- scripts ---------- */
/* Results page: the same three specialists, now reading what came back. */
const AT_RESULTS_TURNS = [
  {
    me: "Results are in. What should I take into the range review?",
    steps: [
      { p: "brand", kind: "thought", label: "Thought for 2s",
        text: "You're going into a range review, so this has to end in a keep-and-cut list for the denim core — not a general read on how the line scored." },
      { p: "data", kind: "tools", label: "3 tools used",
        lines: ["Read 1,240 responses", "Ran 40,000 cuts for real differences", "Ranked 18 concepts on style potential", "Modelled incremental reach across the line"] },
      { p: "res", kind: "tools", label: "3 tools used",
        lines: ["Read 2,140 verbatims", "Coded the language by theme", "Checked each takeaway against 90,000 prior studies"] }
    ],
    reply: "All three objectives came back clean. The denim core carries the line — **Denim Pearl Snap** and **Denim Vest** lead on style potential and on incremental reach.",
    done: "Read 12 findings"
  },
  {
    me: "Anything that splits by customer type?",
    steps: [
      { p: "data", kind: "tools", label: "3 tools used",
        lines: ["Split all 18 concepts by customer type", "Tested loyalist vs non-customer gaps for significance", "Flagged 4 concepts with an inverted read"] },
      { p: "brand", kind: "thought", label: "Thought for 1s",
        text: "An inverted read isn't noise here. A piece only non-customers pick is an acquisition play, not a core buy — and it should be bought at a different depth." }
    ],
    reply: "Yes. Embellished pieces and the Rodeo Denim Jacket are **acquisition plays**: top-scoring with non-customers, bottom-tier with loyalists.",
    done: "Compared 4 segments"
  }
];

/* Intake page: the design work that produced the study, kept in the log. */
/* `moment` is the use case picked on home. Without it the thread replayed the
   sell-in study's tool calls and counts over every use case — it announced
   three objectives and seven questions whatever the objectives tab actually
   held, and told a Marketing demo it had read 38 key-account studies
   (Lucy, 2026-09-09). Everything the agent says it did is now counted from the
   study it actually built. */
const AT_UC = (moment) => (window.EI_MOMENTS && window.EI_MOMENTS[moment]) || window.EI_GENERIC || null;
const AT_NQS = (moment) => {
  const uc = AT_UC(moment);
  return uc && uc.objectives ? uc.objectives.reduce((n, o) => n + o.qs.length, 0) : 7;
};
const AT_NOBJ = (moment) => {
  const uc = AT_UC(moment);
  return uc && uc.objectives ? uc.objectives.length : 3;
};
const AT_WORDS = ["zero", "one", "two", "three", "four", "five", "six"];

const AT_INTAKE_TURNS = (brief, moment) => [
  {
    me: brief || (window.EI3 && window.EI3.chat[0].text) || "",
    steps: (window.IL2 && window.IL2.stepsFor ? window.IL2.stepsFor(moment) : (window.IL2 && window.IL2.steps)) || [],
    reply: "Got it. I've set up a study with **" + (AT_WORDS[AT_NOBJ(moment)] || AT_NOBJ(moment)) + " objectives** and drafted the pages it will hand back. Have a look on the right — change anything by telling me here.",
    done: AT_NOBJ(moment) + " objectives · " + AT_NQS(moment) + " questions · 5 days"
  },
  /* Confirm the framing before asking anything that sizes it.

     The questions decide sample and cost, so they must land after the user
     agrees this is the right study and before anything is sized. Gate first,
     then ask: that way an answer can only change the sizing, never invalidate
     a framing the user already signed off. */
  {
    cardTitle: "Confirm your study",
    card: [
      {
        qid: "ctx", icon: "bulb",
        label: "Does this study design meet your need?",
        opts: [
          { id: "yes", label: "Yes, that looks good", primary: true },
          { id: "no", label: "No, help me tweak it" }
        ]
      }
    ]
  },
  /* The confirmation is the hinge of the page, so it is worth watching the
     agent turn on it. It stops being a form that jumps to the next screen and
     becomes an assistant that took the answer and went and did something. */
  {
    steps: [
      { p: "brand", kind: "tools", label: "3 tools used",
        lines: ["Locked the " + (AT_WORDS[AT_NOBJ(moment)] || AT_NOBJ(moment)) + " pages as drafted",
          AT_UC(moment) && AT_UC(moment).runs
            ? "Read your last 38 " + AT_UC(moment).matched.name.toLowerCase() + " studies"
            : "Read your last 38 studies",
          "Listed what still has to be decided"] },
      { p: "data", kind: "thought", label: "Thought for 2s",
        text: "Nothing left to settle is about the study itself — it is about what happens to the results afterwards. Four things decide the sample and the price: when the decision gets made, how hard the read has to be defended, who is in the room, and what this is worth spending." }
    ],
    reply: a => a.ctx === "no"
      ? "I'll redraft those pages as soon as you tell me what to change. While you think about it, four things that decide the sample and the price."
      : "Good, that's the study settled. Four more things and I can size it and price it.",
    done: a => a.ctx === "no" ? "Waiting on your changes" : "Study confirmed"
  },
  /* One card, four questions, one Submit. Timeline and sign-off used to be a
     single question: they are two facts, and answering "Merch council · Sep 12"
     forced both at once with no way to correct only one. */
  {
    cardTitle: "Tell me more about your needs",
    card: [
      {
        qid: "when", tab: "Timeline", icon: "calendar-bolt",
        label: "When do you need the results by?",
        opts: [
          { id: "hours", label: "In a few hours", short: "In a few hours" },
          { id: "today", label: "Today", short: "Today" },
          { id: "days", label: "In a few days", short: "In a few days", inferred: true },
          { id: "open", label: "No hard deadline", short: "No deadline" }
        ]
      },
      {
        qid: "conf", tab: "Confidence", icon: "target-arrow",
        label: "How much confidence do you need?",
        opts: [
          { id: "directional", label: "Directional · gut check", short: "Directional" },
          { id: "solid", label: "Solid · balanced discovery", short: "Solid", inferred: true },
          { id: "defensible", label: "Defensible · deep research", short: "Defensible" },
          { id: "auto", label: "Decide for me", short: "My call on the bar" }
        ]
      },
      {
        qid: "who", tab: "Stakeholders", icon: "users-group",
        label: "Who will see the results?",
        opts: [
          { id: "exec", label: "Executive leadership", short: "Executive leadership" },
          { id: "team", label: "My team", short: "My team" },
          { id: "cross", label: "Other teams", short: "Other teams" },
          { id: "self", label: "Just myself", short: "Just me" }
        ]
      },
      {
        qid: "teams", tab: "Which teams", icon: "affiliate",
        when: a => a.who === "cross",
        label: "Which teams?",
        opts: [
          { id: "design", label: "Design and product", short: "Design and product" },
          { id: "merch", label: "Merchandising and planning", short: "Merch and planning" },
          { id: "sales", label: "Sales and marketing", short: "Sales and marketing" }
        ]
      }
    ]
  },
  {
    steps: [
      { p: "data", kind: "tools", label: "4 tools used",
        lines: ["Set the field window against your date", "Sized each market cell to hold at 95%", "Balanced Digital Twins against recruited shoppers"] },
      { p: "brand", kind: "thought", label: "Thought for 1s",
        text: "Cell size is what actually decides the sample. Below 248 in a market a split reads as noise, and a split that reads as noise is worse than no split at all." }
    ],
    reply: a => {
      const p = AT_PLAN(a);
      if (!p) return "Sized against what you've told me. It's in the rail on the right.";
      return (p.auto
        ? "You left the bar to me. Given who reads it, **" + p.label.toLowerCase() + "** is the right one — " + atLower(p.blurb)
        : "**" + p.label + "** it is — " + atLower(p.blurb))
        + " That's **" + p.total.toLocaleString() + "** respondents: " + p.twins.toLocaleString()
        + " Digital Twins and " + p.humans.toLocaleString() + " recruited shoppers, back in " + p.leadLabel + "."
        + (p.late ? " That is past the date you gave me — say the word and I'll drop the bar to fit." : "");
    },
    done: a => {
      const p = AT_PLAN(a);
      return p ? p.label + " · " + p.total.toLocaleString() + " respondents" : "Sized";
    }
  }
];

/* Offered, never played. Each one runs only when the user picks it. */
function AT_REPLY_TO(text) {
  return {
    me: text,
    steps: [{ p: "brand", kind: "tools", label: "2 tools used",
      lines: ["Read your message against the current draft", "Checked which objectives it touches"] }],
    reply: "Noted. I'll fold that into the draft — you'll see it on the right before anything is sent out.",
    done: "Noted"
  };
}

const AT_SUGGESTIONS = [
  {
    label: "Can we read it by region as well?",
    turn:
    {
    me: "Can we read it by region as well?",
    steps: [
      { p: "data", kind: "tools", label: "3 tools used",
        lines: ["Added region as a cut on all three pages", "Checked cell sizes per region", "Confirmed the sample still holds at 95%"] },
      { p: "res", kind: "thought", label: "Thought for 1s",
        text: "No new questions needed — region comes from the panel profile, so the cut is free and the survey stays at seven minutes." }
    ],
    reply: "Added **region** as a cut on all three pages. No new questions needed.",
    done: "1 cut added"
  }
  }
];

Object.assign(window, { AgentThread, ATDock, AT_PLAN, AT_RESULTS_TURNS, AT_INTAKE_TURNS, AT_SUGGESTIONS, AT_REPLY_TO });
