// Procurement chat experience + animated progression tree + parallel
// research panel. The persona is institutional — no coach avatar / first
// name in the conversation; the user is chatting with "CABL Procurement
// Coach", a doctrine-grounded AI advisor.

const {
  useState: useStateChat,
  useEffect: useEffectChat,
  useRef: useRefChat,
  useMemo: useMemoChat,
} = React;

const IS_DEMO = new URLSearchParams(window.location.search).has("demo");

const DISCOVERY_NODE_KEYS = [
  "case",
  "relationship",
  "counterpart",
  "economics",
  "leverage",
  "priorities",
  "goals",
  "risk",
];

const NODE_LABEL = {
  case:         "Your case",
  relationship: "The supply relationship",
  counterpart:  "The other side",
  economics:    "The economics",
  leverage:     "Your alternatives",
  priorities:   "Your priorities",
  goals:        "Your goals",
  risk:         "The risks",
  plan:         "Your action plan",
};

const PHASE_GROUPS = [
  { label: "Context",     ids: ["case", "relationship"] },
  { label: "Counterpart", ids: ["counterpart", "economics"] },
  { label: "Levers",      ids: ["leverage", "priorities"] },
  { label: "Strategy",    ids: ["goals", "risk"] },
];

const LIVE_QUICK_FILL = [
  "Our supplier Acme Chemicals wants an 8% price increase on packaging resin for a 2-year renewal. We're on Net 60 today and the contract expires in six weeks.",
  "Acme is strategic for us — incumbent across Europe, hard to replace fast. We're a meaningful regional account for them, maybe top-5 in their book.",
  "Our account manager is fronting it, but the real decision sits with their VP for the segment. We think they're under quarter-end margin pressure.",
  "Our view of the cost reality: resin is up ~12% (their main driver, ~30% of their cost base) — so ~3.5% pass-through is supportable. Energy/labour have been stable. They didn't pass through declines in 2022.",
  "If we walk, two near-qualified alternatives could absorb 60% of the volume in 3–6 months. One smaller current supplier could scale with effort. Switching cost is real but not catastrophic.",
  "Price is first. Supply continuity second. Payment terms third. We can flex on contract length if the economics improve.",
  "Floor: 5% maximum on the resin-exposed segment only, temporary, with rollback. Target: 3% indexed + improved payment terms. Ambitious: hold flat with longer term + JBP.",
  "Honest risk: urgency. The team is nervous about switching friction and that could push us to cave on the 8%.",
];

function liveQuickFill(turnCount) {
  if (!LIVE_QUICK_FILL.length) return null;
  return LIVE_QUICK_FILL[Math.min(turnCount, LIVE_QUICK_FILL.length - 1)];
}

// ---------- Progression Tree (SVG) ----------
function ProgressionTree({ progress, completion }) {
  const w = 520;
  const h = 720;
  const nodes = TREE_NODES;
  const edges = TREE_EDGES;

  const nodeMap = useMemoChat(() => {
    const m = {};
    nodes.forEach(n => {
      m[n.id] = { ...n, px: n.x * w, py: n.y * h, fill: (progress[n.id] || 0) };
    });
    return m;
  }, [progress]);

  return (
    <div className="tree-wrap">
      <div className="tree-head">
        <div className="kicker">Discovery progress</div>
        <div className="tree-completion">
          <div className="tree-completion-num">{completion}%</div>
          <div className="tree-completion-bar"><div className="tree-completion-fill" style={{ width: `${completion}%` }}/></div>
          <div className="tree-completion-label">{completion < 100 ? "Mapping your case" : "Ready for the plan"}</div>
        </div>
      </div>

      <svg className="tree-svg" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet">
        <defs>
          <linearGradient id="edgegrad" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0%" stopColor="#E63946" stopOpacity="0.95"/>
            <stop offset="100%" stopColor="#E63946" stopOpacity="0.4"/>
          </linearGradient>
        </defs>

        {edges.map(([a, b], i) => {
          const A = nodeMap[a], B = nodeMap[b];
          if (!A || !B) return null;
          const both = Math.min(A.fill, B.fill) / 100;
          const mx = (A.px + B.px) / 2;
          const my = (A.py + B.py) / 2 - 14;
          return (
            <g key={i}>
              <path d={`M ${A.px} ${A.py} Q ${mx} ${my} ${B.px} ${B.py}`} stroke="rgba(30,30,90,0.14)" strokeWidth="1.4" fill="none" strokeDasharray="4 4"/>
              <path d={`M ${A.px} ${A.py} Q ${mx} ${my} ${B.px} ${B.py}`} stroke="url(#edgegrad)" strokeWidth="2.4" fill="none" strokeLinecap="round" strokeDasharray="260" strokeDashoffset={260 - both * 260} style={{ transition: "stroke-dashoffset 700ms cubic-bezier(.2,.7,.3,1)" }}/>
            </g>
          );
        })}

        {nodes.map(n => {
          const N = nodeMap[n.id];
          const filled = N.fill;
          const radius = n.terminal ? 38 : 28;
          const ringR = radius + 7;
          const active = filled > 0;
          const done = filled >= 100;
          return (
            <g key={n.id} transform={`translate(${N.px} ${N.py})`}>
              {done && <circle r={ringR + 7} fill="rgba(230,57,70,0.10)"/>}
              <circle r={ringR} fill="none" stroke="rgba(30,30,90,0.12)" strokeWidth="1.5"/>
              <circle r={ringR} fill="none" stroke={done ? "#E63946" : "#1E1E5A"} strokeWidth="2" strokeDasharray={`${(filled / 100) * 2 * Math.PI * ringR} ${2 * Math.PI * ringR}`} strokeLinecap="round" transform="rotate(-90)" style={{ transition: "stroke-dasharray 700ms cubic-bezier(.2,.7,.3,1)" }}/>
              <circle r={radius} fill={done ? "#E63946" : (active ? "#FFFFFF" : "#EAEAEE")} stroke={done ? "#E63946" : "#1E1E5A"} strokeWidth={done ? 0 : 1.5}/>
              {done ? (
                <path d="M -7 0 L -1 6 L 7 -5" stroke="white" strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
              ) : (
                <text textAnchor="middle" dy="0.35em" style={{ font: "700 11px/1 Nunito, system-ui" }} fill="#1E1E5A">{n.terminal ? "PLAN" : Math.round(filled) + "%"}</text>
              )}
              <text textAnchor="middle" y={ringR + 20} style={{ font: "600 12px/1 Nunito, system-ui" }} fill="#1E1E5A">{n.label}</text>
            </g>
          );
        })}
      </svg>

      <div className="tree-phases">
        {PHASE_GROUPS.map(p => {
          const avg = p.ids.reduce((s, k) => s + (progress[k] || 0), 0) / p.ids.length;
          return (
            <div key={p.label} className="tree-phase">
              <div className="tree-phase-bar"><div className="tree-phase-fill" style={{ width: `${avg}%` }}/></div>
              <div className="tree-phase-label">{p.label}</div>
            </div>
          );
        })}
      </div>

      <div className="tree-legend">
        <span><i className="legend-dot empty"/> not yet</span>
        <span><i className="legend-dot partial"/> in progress</span>
        <span><i className="legend-dot done"/> covered</span>
      </div>
    </div>
  );
}

// ---------- Research Panel ----------
function ResearchPanel({ research, status }) {
  if (status === "idle") {
    return (
      <div className="research-pane">
        <div className="kicker">Market research</div>
        <p className="research-empty">Once you share enough about the case, an external research pass runs in the background — alternative supplier signals, market pricing, comparable contracts.</p>
      </div>
    );
  }
  if (status === "loading") {
    return (
      <div className="research-pane">
        <div className="kicker">Market research</div>
        <div className="research-loading">
          <div className="research-loading-dots"><span/><span/><span/></div>
          <p>Scanning the market — alternative suppliers, pricing signals, public deal comparables.</p>
        </div>
      </div>
    );
  }
  if (status === "refreshing" && research) {
    return (
      <div className="research-pane">
        <div className="kicker">Market research</div>
        <p className="research-empty">Refreshing with the latest details from your conversation.</p>
        {research.agents.map((a) => (
          <div key={a.agent} className="research-agent">
            <div className="research-agent-name">{a.agent.replace(/_/g, " ")}</div>
            <p className="research-agent-summary">{a.summary}</p>
          </div>
        ))}
      </div>
    );
  }
  if (status === "error" || !research) {
    return (
      <div className="research-pane">
        <div className="kicker">Market research</div>
        <p className="research-empty">Research wasn't available for this case. The plan will work from what you've shared.</p>
      </div>
    );
  }
  return (
    <div className="research-pane">
      <div className="kicker">Market research</div>
      {research.brief?.overview && (
        <p className="research-empty" style={{ marginTop: 0 }}>
          Focus: {research.brief.overview}
        </p>
      )}
      {research.agents.map((a) => (
        <div key={a.agent} className="research-agent">
          <div className="research-agent-name">{a.agent.replace(/_/g, " ")}</div>
          <p className="research-agent-summary">{a.summary}</p>
          {a.findings && a.findings.length > 0 && (
            <ul className="research-findings">
              {a.findings.slice(0, 2).map((f, i) => (
                <li key={i}>
                  <span className="research-claim">{f.claim}</span>
                  <a href={f.source_url} target="_blank" rel="noopener noreferrer" className="research-source">{f.source_title}</a>
                </li>
              ))}
            </ul>
          )}
        </div>
      ))}
    </div>
  );
}

// ---------- Attachment card (briefing PDF) ----------
function AttachmentCard({ attachment, text }) {
  const [open, setOpen] = useStateChat(false);
  // Strip the synthetic header from the body so the expanded view only shows
  // the actual briefing text. The header was already presented in the card.
  const body = (text || "").replace(/^\[Briefing document attached[^\]]*\]\s*/, "");
  const sizeLabel = attachment.sizeBytes
    ? attachment.sizeBytes < 1024 * 1024
      ? `${Math.round(attachment.sizeBytes / 1024)} KB`
      : `${(attachment.sizeBytes / (1024 * 1024)).toFixed(1)} MB`
    : null;
  return (
    <div className="chat-attachment">
      <div className="chat-attachment-head">
        <span className="chat-attachment-mark"><Icon name="file-text" size={20}/></span>
        <div className="chat-attachment-meta">
          <div className="chat-attachment-name">{attachment.fileName}</div>
          <div className="chat-attachment-sub">
            Briefing document attached · {attachment.pageCount} {attachment.pageCount === 1 ? "page" : "pages"}
            {sizeLabel ? ` · ${sizeLabel}` : ""}
          </div>
        </div>
        <button className="link-btn" onClick={() => setOpen((v) => !v)}>
          <IconText name={open ? "eye-off" : "eye"}>{open ? "Hide extracted text" : "Show extracted text"}</IconText>
        </button>
      </div>
      {open && (
        <pre className="chat-attachment-body">{body}</pre>
      )}
    </div>
  );
}

// ---------- Coach badge (institutional) ----------
function CoachBadge({ size = 32 }) {
  return (
    <div className="coach-badge" style={{ width: size, height: size }} aria-hidden="true">
      <div className="coach-badge-mark" style={{ fontSize: size * 0.42 }}>C</div>
    </div>
  );
}

// ---------- Counterpart profile (Kahneman stage) ----------
const COLOR_DOT = {
  "Fiery Red": "#E63946",
  "Sunshine Yellow": "#F4B740",
  "Earth Green": "#6A9A7A",
  "Cool Blue": "#6A8FB5",
};

function ProfileResult({ handoff }) {
  if (!handoff || !handoff.blendLabel) return null;
  const d = handoff.colorDistribution || {};
  const bars = [
    ["Fiery Red", d.fieryRed],
    ["Sunshine Yellow", d.sunshineYellow],
    ["Earth Green", d.earthGreen],
    ["Cool Blue", d.coolBlue],
  ];
  return (
    <div className="counterpart-result">
      <div className="counterpart-result-head">
        <div>
          <div className="counterpart-blend">{handoff.blendLabel}</div>
          <div className="counterpart-sub">
            {handoff.counterpartName}{handoff.counterpartRole ? ` · ${handoff.counterpartRole}` : ""}
          </div>
        </div>
        <div className="counterpart-conf" title="Confidence in this read">
          {handoff.overallConfidence}% · {String(handoff.assessmentTier || "").toLowerCase()}
        </div>
      </div>

      <div className="counterpart-bars">
        {bars.map(([name, val]) => (
          <div key={name} className="counterpart-bar-row" title={`${name}: ${val ?? 0}%`}>
            <span className="counterpart-bar-track">
              <span className="counterpart-bar-fill" style={{ width: `${val || 0}%`, background: COLOR_DOT[name] }}/>
            </span>
          </div>
        ))}
      </div>

      {handoff.profileSummary && <p className="counterpart-summary">{handoff.profileSummary}</p>}

      {Array.isArray(handoff.engagementGuidance) && handoff.engagementGuidance.length > 0 && (
        <ul className="counterpart-guidance">
          {handoff.engagementGuidance.map((g, i) => <li key={i}>{g}</li>)}
        </ul>
      )}

      {Array.isArray(handoff.scenarioBranches) && handoff.scenarioBranches.length > 0 && (
        <div className="counterpart-branches">
          <div className="kicker">If they show up as…</div>
          {handoff.scenarioBranches.map((b, i) => (
            <div key={i} className="counterpart-branch">
              <div className="counterpart-branch-head">{b.branch} <span className="counterpart-branch-lk">{b.likelihood}</span></div>
              <div className="counterpart-branch-body">{b.engagement}</div>
            </div>
          ))}
        </div>
      )}

      {handoff.identityWarning && (
        <p className="counterpart-warn">
          <Icon name="alert-triangle" size={14}/> {handoff.identityWarning}
        </p>
      )}
      <p className="counterpart-foot">This read will shape the other-side analysis and scenarios in your plan.</p>
    </div>
  );
}

function CounterpartPanel({ counterpart, setCounterpart, onProfile, status, error, handoff, show, setShow, disabled }) {
  const fileRef = useRefChat(null);
  const [pdfBusy, setPdfBusy] = useStateChat(false);
  const [pdfName, setPdfName] = useStateChat(null);

  const set = (k) => (e) => setCounterpart((c) => ({ ...c, [k]: e.target.value }));

  const onPickPdf = async (e) => {
    const file = e.target.files?.[0];
    if (!file || !window.PdfIntake) return;
    setPdfBusy(true);
    try {
      const res = await window.PdfIntake.extractText(file);
      if (res.ok) {
        setCounterpart((c) => ({ ...c, document: res.text }));
        setPdfName(res.fileName);
      } else {
        setPdfName(res.error || "Couldn't read that PDF.");
      }
    } finally {
      setPdfBusy(false);
      if (fileRef.current) fileRef.current.value = "";
    }
  };

  const hasInput = !!(
    counterpart.name?.trim() || counterpart.role?.trim() ||
    counterpart.relationship?.trim() || counterpart.notes?.trim() || counterpart.document?.trim()
  );

  if (!show) {
    return (
      <button className="counterpart-toggle link-btn" onClick={() => setShow(true)} disabled={disabled}>
        <IconText name="plus">Profile the other side (optional)</IconText>
      </button>
    );
  }

  return (
    <div className="counterpart-panel">
      <div className="counterpart-panel-head">
        <div>
          <div className="kicker">Profile the other side — optional</div>
          <p className="counterpart-help">
            Tell us who you're negotiating against and we'll read their likely style and how to engage them. Paste a LinkedIn page, an email thread, or just describe how they've behaved. Skip this if you'd rather not.
          </p>
        </div>
        <button className="link-btn" onClick={() => setShow(false)}>
          <IconText name="chevron-left">Hide</IconText>
        </button>
      </div>

      <div className="counterpart-fields">
        <div className="counterpart-field-row">
          <input className="counterpart-input" placeholder="Name (optional)" value={counterpart.name} onChange={set("name")}/>
          <input className="counterpart-input" placeholder="Role & company (optional)" value={counterpart.role} onChange={set("role")}/>
        </div>
        <input className="counterpart-input" placeholder="Your prior relationship with them (optional)" value={counterpart.relationship} onChange={set("relationship")}/>
        <textarea
          className="counterpart-input"
          placeholder="How have they behaved? Anything you know about their style, pressures, or how they negotiate."
          rows={3}
          value={counterpart.notes}
          onChange={set("notes")}
        />
        <div className="counterpart-pdf">
          <input ref={fileRef} type="file" accept="application/pdf" style={{ display: "none" }} onChange={onPickPdf}/>
          <button className="btn-ghost btn-sm" onClick={() => fileRef.current?.click()} disabled={pdfBusy}>
            <IconText name="paperclip">{pdfBusy ? "Reading..." : "Attach a profile / email PDF"}</IconText>
          </button>
          {pdfName && <span className="counterpart-pdf-name">{pdfName}</span>}
          {counterpart.document && (
            <span className="counterpart-pdf-ok">
              <Icon name="check" size={13}/> {Math.round(counterpart.document.length / 1000)}K chars captured
            </span>
          )}
        </div>
      </div>

      <div className="counterpart-actions">
        <button className="btn-secondary btn-sm" onClick={onProfile} disabled={!hasInput || status === "loading" || disabled}>
          <IconText name={status === "loading" ? "rotate-cw" : "message-square"}>
            {status === "loading" ? "Reading the other side..." : handoff ? "Re-profile" : "Profile the other side"}
          </IconText>
        </button>
        {status === "error" && <span className="counterpart-err">{error || "Profiling failed — you can still build the plan."}</span>}
      </div>

      {status === "ready" && <ProfileResult handoff={handoff}/>}
    </div>
  );
}

// ---------- Chat ----------
function ChatScreen({ situation, coach, user, caseId, byoKeyStatus, creditStatus, onCreditsUpdate, chatInitial, onComplete, onBack, onOpenSettings, onSignOut }) {
  // Shared state — seed from chatInitial when present (briefing-intake path).
  const [messages, setMessages] = useStateChat(() => chatInitial?.messages || []);
  const [input, setInput] = useStateChat("");
  const [progress, setProgress] = useStateChat(() => chatInitial?.progress || {});
  const [thinking, setThinking] = useStateChat(false);
  const [done, setDone] = useStateChat(() => !!chatInitial?.ready_for_plan);
  const [planning, setPlanning] = useStateChat(false);
  const scrollRef = useRefChat(null);

  // Live-mode state
  const [research, setResearch] = useStateChat(null);
  const [researchStatus, setResearchStatus] = useStateChat("idle"); // idle | loading | refreshing | ready | error
  const researchStageRef = useRefChat(0);
  const researchRequestIdRef = useRefChat(0);
  const [archetypeClassification, setArchetypeClassification] = useStateChat(() => chatInitial?.archetypeClassification || null);
  const [briefingExtract, setBriefingExtract] = useStateChat(() => chatInitial?.briefingExtract || null);

  // Counterpart profiling (the Kahneman stage — opt-in, default off). Fires only
  // when the user supplies counterpart input below the discovery-complete panel.
  const [counterpart, setCounterpart] = useStateChat({ name: "", role: "", relationship: "", notes: "", document: "" });
  const [profileStatus, setProfileStatus] = useStateChat("idle"); // idle | loading | ready | error
  const [profileError, setProfileError] = useStateChat(null);
  const [kahnemanHandoff, setKahnemanHandoff] = useStateChat(() => chatInitial?.kahnemanHandoff || null);
  const [showCounterpart, setShowCounterpart] = useStateChat(false);
  const [strategiqBrief, setStrategiqBrief] = useStateChat(() => chatInitial?.strategiqBrief || null);
  const [strategiqStatus, setStrategiqStatus] = useStateChat("idle"); // idle | loading | ready | skipped | error

  // Init: open the chat. Skip the synthetic opening call when we already
  // have messages (briefing-intake path — the first turn ran in App).
  useEffectChat(() => {
    if (chatInitial?.messages?.length) {
      // Briefing path — research may want to kick off based on initial facts.
      maybeKickResearch(chatInitial.messages, chatInitial.progress || {}).catch(() => {});
      return;
    }
    setThinking(true);
    (async () => {
      try {
        const resp = await fetch("/api/chat", {
          method: "POST",
          headers: await window.Auth.apiHeaders(),
          body: JSON.stringify({
            coachId: coach.id,
            situation: situation.id,
            caseId: caseId || null,
            messages: [],
            progress: {},
            ...(window.ByoKey?.requestFields?.() || {}),
          }),
        });
        const data = await resp.json();
        if (data.credits) onCreditsUpdate?.(data.credits);
        if (data.error) throw new Error(data.error);
        window.SessionCost?.addChatCost(data._costBreakdown);
        setMessages([{ who: "coach", text: data.reply }]);
        setProgress(data.progress || {});
        setDone(!!data.ready_for_plan);
      } catch (err) {
        console.error(err);
        setMessages([
          { who: "coach", text: `(We couldn't reach the coaching backend just now — ${err.message}. Please try again, or check your network / API key in Settings.)` },
        ]);
      } finally {
        setThinking(false);
      }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffectChat(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
    }
  }, [messages, thinking]);

  const completion = useMemoChat(() => {
    const sum = DISCOVERY_NODE_KEYS.reduce((s, k) => s + Math.min(100, progress[k] || 0), 0);
    return Math.round(sum / DISCOVERY_NODE_KEYS.length);
  }, [progress]);

  const treeProgress = useMemoChat(() => {
    return { ...progress, plan: done ? 100 : 0 };
  }, [progress, done]);

  // Research firing policy: fire ONCE when the user has shared enough
  // context for the agents to do useful work, then never re-fire until the
  // user clicks "Build my plan" (where buildPlan() calls us with force: true,
  // forceFinal: true). Previously we fired three times — stages 1/2/3 each
  // re-ran the full agent panel from scratch, which roughly tripled research
  // spend with marginal quality gain. Web-search agents are the most
  // expensive calls in the system, so this is the highest-ROI cost lever.
  function desiredResearchStage(allMessages, progressMap = {}, forceFinal = false) {
    const userCount = allMessages.filter((m) => m.who === "user").length;
    if (userCount < 1) return 0;
    if (forceFinal) return 1;
    // Single trigger threshold: user has shared at least 2 turns OR a
    // briefing-derived turn that already covers several nodes.
    const hasMeaningfulContext =
      userCount >= 2 ||
      (progressMap.case || 0) >= 50 ||
      (progressMap.counterpart || 0) >= 40 ||
      (progressMap.economics || 0) >= 40;
    return hasMeaningfulContext ? 1 : 0;
  }

  function researchPayload(allMessages, progressMap = {}) {
    return {
      coachId: coach.id,
      situation: situation.id,
      caseId: caseId || null,
      messages: allMessages,
      progress: progressMap,
      ...(window.ByoKey?.requestFields?.() || {}),
      archetypeClassification: archetypeClassification || null,
      seedFacts: {
        firstUserMessage: allMessages.find((m) => m.who === "user")?.text || null,
      },
    };
  }

  const maybeKickResearch = async (allMessages, progressMap = progress, opts = {}) => {
    const stage = desiredResearchStage(allMessages, progressMap, !!opts.forceFinal);
    if (!stage) return research;
    if (!opts.force && stage <= researchStageRef.current) return research;

    const requestId = ++researchRequestIdRef.current;
    setResearchStatus(research ? "refreshing" : "loading");

    try {
      const resp = await fetch("/api/research", {
        method: "POST",
        headers: await window.Auth.apiHeaders(),
        body: JSON.stringify(researchPayload(allMessages, progressMap)),
      });
      const data = await resp.json();
      if (data.credits) onCreditsUpdate?.(data.credits);
      if (data.error) throw new Error(data.error);
      if (requestId !== researchRequestIdRef.current) return data;
      researchStageRef.current = Math.max(researchStageRef.current, stage);
      window.SessionCost?.addResearchCost(data);
      setResearch(data);
      setResearchStatus("ready");
      return data;
    } catch (err) {
      console.error("research failed", err);
      if (requestId === researchRequestIdRef.current) {
        setResearchStatus(research ? "ready" : "error");
      }
      throw err;
    }
  };

  const submit = async (rawText) => {
    const text = (rawText ?? input).trim();
    if (!text || thinking || done || planning) return;

    const userMsg = { who: "user", text };
    const nextMessages = [...messages, userMsg];
    setMessages(nextMessages);
    setInput("");
    setThinking(true);

    maybeKickResearch(nextMessages, progress).catch(() => {});

    try {
      const resp = await fetch("/api/chat", {
        method: "POST",
        headers: await window.Auth.apiHeaders(),
        body: JSON.stringify({
          coachId: coach.id,
          situation: situation.id,
          caseId: caseId || null,
          messages: nextMessages,
          progress,
          research: research || null,
          ...(window.ByoKey?.requestFields?.() || {}),
          archetypeClassification: archetypeClassification || null,
          briefingExtract: briefingExtract || null,
        }),
      });
      const data = await resp.json();
      if (data.credits) onCreditsUpdate?.(data.credits);
      if (data.error) throw new Error(data.error);
      window.SessionCost?.addChatCost(data._costBreakdown);
      setMessages((m) => [...m, { who: "coach", text: data.reply, final: !!data.ready_for_plan }]);
      const mergedProgress = { ...progress };
      Object.entries(data.progress || {}).forEach(([k, v]) => {
        mergedProgress[k] = Math.max(mergedProgress[k] || 0, v || 0);
      });
      setProgress(mergedProgress);
      if (data.archetypeClassification) setArchetypeClassification(data.archetypeClassification);
      if (data.briefingExtract) setBriefingExtract(data.briefingExtract);
      if (data.ready_for_plan) {
        setDone(true);
        maybeKickResearch(nextMessages, mergedProgress, { forceFinal: true }).catch(() => {});
      } else {
        maybeKickResearch(nextMessages, mergedProgress).catch(() => {});
      }
    } catch (err) {
      console.error(err);
      setMessages((m) => [...m, { who: "coach", text: `(We hit an error from the coaching backend — ${err.message}. Try again, or verify your API key in Settings.)` }]);
    } finally {
      setThinking(false);
    }
  };

  const quickFill = () => {
    const turnCount = messages.filter((m) => m.who === "user").length;
    const sample = liveQuickFill(turnCount);
    if (sample) submit(sample);
  };

  const hasCounterpartInput = () => {
    const c = counterpart;
    return !!(c.name?.trim() || c.role?.trim() || c.relationship?.trim() || c.notes?.trim() || c.document?.trim());
  };

  // Run the Kahneman behavioural profiler. Opt-in: only fires when the user
  // gave us counterpart input. Returns the handoff (also stored in state) or null.
  const runProfile = async () => {
    if (!hasCounterpartInput() || profileStatus === "loading") return kahnemanHandoff;
    setProfileStatus("loading");
    setProfileError(null);
    try {
      const resp = await fetch("/api/profile", {
        method: "POST",
        headers: await window.Auth.apiHeaders(),
        body: JSON.stringify({
          coachId: coach.id,
          caseId: caseId || null,
          counterpart,
          caseContext: {
            caseId: caseId || null,
            summary: messages.find((m) => m.who === "user")?.text?.slice(0, 600) || null,
          },
          ...(window.ByoKey?.requestFields?.() || {}),
        }),
      });
      const data = await resp.json();
      if (data.credits) onCreditsUpdate?.(data.credits);
      if (data.error) throw new Error(data.error);
      if (data._skipped) {
        setProfileStatus("idle");
        return null;
      }
      window.SessionCost?.addProfileCost?.(data);
      setKahnemanHandoff(data);
      setProfileStatus("ready");
      return data;
    } catch (err) {
      console.error("counterpart profiling failed", err);
      setProfileError(err.message || String(err));
      setProfileStatus("error");
      return null;
    }
  };

  // Run the StrategIQ strategic-sourcing brief. The endpoint self-skips when the
  // case archetype isn't a sourcing-strategy one, so we can call it for any
  // Giuseppe case and let the server decide. Returns the brief (also stored in
  // state) or null.
  const runStrategiqBrief = async (latestResearch) => {
    if (coach.id !== "giuseppe" || strategiqStatus === "loading") return strategiqBrief;
    setStrategiqStatus("loading");
    try {
      const resp = await fetch("/api/strategiq", {
        method: "POST",
        headers: await window.Auth.apiHeaders(),
        body: JSON.stringify({
          coachId: coach.id,
          caseId: caseId || null,
          messages,
          research: latestResearch || research || null,
          archetypeLabel: archetypeClassification?.archetypeLabel || null,
          kahnemanHandoff: kahnemanHandoff || null,
          contractPresent: archetypeClassification?.contractPresent || false,
          contractPharma: archetypeClassification?.contractPharma || false,
          caseContext: { caseId: caseId || null },
          ...(window.ByoKey?.requestFields?.() || {}),
        }),
      });
      const data = await resp.json();
      if (data.credits) onCreditsUpdate?.(data.credits);
      if (data.error) throw new Error(data.error);
      if (data._skipped) {
        setStrategiqStatus("skipped");
        return null;
      }
      window.SessionCost?.addStrategiqCost?.(data);
      setStrategiqBrief(data);
      setStrategiqStatus("ready");
      return data;
    } catch (err) {
      console.error("strategic sourcing brief failed", err);
      setStrategiqStatus("error");
      return null;
    }
  };

  const buildPlan = async () => {
    if (planning) return;
    setPlanning(true);
    try {
      // Run the profiler (if the user supplied counterpart input and we haven't
      // already) in parallel with the final research refresh.
      const profilePromise =
        hasCounterpartInput() && !kahnemanHandoff ? runProfile().catch(() => null) : Promise.resolve(kahnemanHandoff);
      let latestResearch = research;
      latestResearch = await maybeKickResearch(messages, progress, { force: true, forceFinal: true }).catch(() => research);
      const handoff = await profilePromise;
      // Run StrategIQ after research + profiler so it can fold both into the
      // brief. Self-skips server-side for non-sourcing archetypes.
      const brief = strategiqBrief || (await runStrategiqBrief(latestResearch).catch(() => null));
      onComplete({
        progress,
        coach,
        situation,
        messages,
        research: latestResearch || research,
        archetypeClassification,
        kahnemanHandoff: handoff || kahnemanHandoff || null,
        strategiqBrief: brief || strategiqBrief || null,
      });
    } catch (err) {
      console.error("final research refresh failed", err);
      onComplete({ progress, coach, situation, messages, research, archetypeClassification, kahnemanHandoff, strategiqBrief });
      setPlanning(false);
    }
  };

  return (
    <div className="screen chat-screen">
      <AppTopBar
        user={user}
        active="chat"
        onNewCase={null}
        onOpenWorkspace={onBack}
        onOpenSettings={onOpenSettings}
        onSignOut={onSignOut}
        byoKeyStatus={byoKeyStatus}
        creditStatus={creditStatus}
      />

      <div className="chat-substrate">
        <div className="chat-substrate-l">
          <div className="kicker">{situation.kicker}</div>
          <div className="chat-substrate-meta">
            {messages.filter((m) => m.who === "user").length} {messages.filter((m) => m.who === "user").length === 1 ? "turn" : "turns"}
            {archetypeClassification?.archetypeLabel && (
              <>
                <span className="chat-substrate-sep">·</span>
                <span className="archetype-pill" title={`Classified with ${(archetypeClassification.confidence * 100).toFixed(0)}% confidence`}>
                  {archetypeClassification.archetypeLabel}
                </span>
              </>
            )}
          </div>
        </div>
        <div className="chat-substrate-r">
          <button className="btn-ghost btn-sm" onClick={onBack}>
            <IconText name="arrow-left">Back to workspace</IconText>
          </button>
        </div>
      </div>

      <div className="chat-layout">
        {/* LEFT: chat */}
        <div className="chat-pane">
          <div className="chat-scroll" ref={scrollRef}>
            {messages.map((m, i) => (
              <div key={i} className={`bubble bubble-${m.who} ${m.final ? "bubble-final" : ""}`}>
                {m.who === "coach" && <div className="bubble-avatar"><CoachBadge size={32}/></div>}
                <div className="bubble-body">
                  {m.who === "coach" && <div className="bubble-name">Coach</div>}
                  {m.attachment?.kind === "pdf" ? (
                    <AttachmentCard attachment={m.attachment} text={m.text}/>
                  ) : (
                    <div className="bubble-text">{m.text}</div>
                  )}
                </div>
              </div>
            ))}
            {thinking && (
              <div className="bubble bubble-coach">
                <div className="bubble-avatar"><CoachBadge size={32}/></div>
                <div className="bubble-body">
                  <div className="bubble-name">Coach</div>
                  <div className="bubble-text typing"><span/><span/><span/></div>
                </div>
              </div>
            )}
          </div>

          {!done ? (
            <div className="chat-composer">
              <div className="chat-composer-meta">
                <span>Your reply</span>
                <button className="link-btn" onClick={quickFill}>
                  <IconText name="message-square">Use a sample answer</IconText>
                </button>
              </div>
              <textarea
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="Type your reply…"
                rows={3}
                onKeyDown={(e) => {
                  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); submit(); }
                }}
              />
              <div className="chat-composer-actions">
                <span className="chat-hint">⌘ + Enter to send</span>
                <button className="btn-primary" disabled={!input.trim() || thinking || planning} onClick={() => submit()}>
                  <IconText name="send" after>Send</IconText>
                </button>
              </div>
            </div>
          ) : (
            <div className="chat-done">
              <div className="chat-done-meta">
                <div className="kicker">Discovery complete</div>
                <div className="chat-done-title">All eight areas covered — ready to build your plan.</div>
              </div>
              <CounterpartPanel
                counterpart={counterpart}
                setCounterpart={setCounterpart}
                onProfile={runProfile}
                status={profileStatus}
                error={profileError}
                handoff={kahnemanHandoff}
                show={showCounterpart}
                setShow={setShowCounterpart}
                disabled={planning}
              />
              <button className="btn-primary big pulse" disabled={planning} onClick={buildPlan}>
                <IconText name={planning ? "rotate-cw" : "arrow-right"} after>
                  {planning ? (profileStatus === "loading" ? "Reading the other side..." : "Refreshing research...") : "Build my plan"}
                </IconText>
              </button>
            </div>
          )}
        </div>

        {/* RIGHT: tree + research */}
        <div className="tree-pane">
          <ProgressionTree progress={treeProgress} completion={completion}/>
          <ResearchPanel research={research} status={researchStatus}/>
        </div>
      </div>
    </div>
  );
}

window.ChatScreen = ChatScreen;
