// ChatPanel — the create-project conversation surface (Slice 3). Composite,
// pure render: it turns a flat list of server `CreateEvent`s into the chat
// transcript (bubbles, brief card, status block, gates, revision list, phase
// dividers, errors) and pins a message input at the bottom. It NEVER fetches —
// the page (Task 11) polls and feeds `events`/`phase`/`busy` in and handles the
// `onSend` / `onGate` callbacks (including the 409-busy retry).
//
// Props:
//   events         — CreateEvent[] (server shape, verbatim): { seq, at, kind, ...fields }
//   phase          — CreatePhase string; gates the input (disabled on planning|done)
//   busy           — true while the page has a POST in flight
//   streamingText  — string|null; live agent text streamed in before its event lands
//   onSend         — (text) => void; the composer's Send
//   onGate         — (gateId, decision, note?) => void; gate + revision-list decisions
//   contextCount   — number badge shown on the "+ Context" toggle button
//   onToggleContext — () => void; opens/closes the context sidebar (Task 4)
//   target         — 'brief'|'plan'|'line'|null; when set, the composer shows an
//                    "Editing: <Step> ✕" chip and the page's onSend POSTs `target`.
//                    ORTHOGONAL to `phase` (persona/disabled): undefined for R4's
//                    phase="orchestrator" mount and every non-create caller, so the
//                    composer then renders + POSTs exactly as before.
//                    A set `target` also re-enables the composer at phase 'done'
//                    (the a4 route accepts targeted revisions until Launch, 14c);
//                    no-target 'done' stays the disabled terminal state.
//   onClearTarget  — () => void; the chip's ✕ — clears back to current-phase routing
//
// Two correlation rules over the flat event list (both look-ahead only):
//   • gate — a `gate` event WITHOUT a decision is an OPEN gate (Approve/Revise).
//     A LATER `gate` event with the SAME id AND a decision RESOLVES it: the open
//     one renders with disabled buttons + the decision, and the resolving event
//     itself renders nothing (it's the resolution signal, not a second card).
//   • revision_list — its "Confirm revisions" button is stale/disabled once ANY
//     later event is a `phase` of planning|plan_gate|done (the planner has moved
//     on), so an already-confirmed list can't be re-confirmed.

const PHASE_LABELS = {
  interviewing: 'Interviewing',
  brief_gate: 'Brief review',
  planning: 'Planning',
  reviewing: 'Reviewing',
  plan_gate: 'Plan review',
  done: 'Done',
  errored: 'Error',
};

// Human labels for the composer target chip (R9b-8). A `target` names which
// canvas step this create-flow message edits (brief/plan/line); the a4 messages
// route accepts the same keys.
const TARGET_LABELS = { brief: 'Brief', plan: 'Plan', line: 'Line' };

const Spinner = () => (
  <span className="inline-block w-3 h-3 border-2 border-neutral-300 border-t-neutral-600 rounded-full animate-spin" />
);

// First couple of sentences of a brief, as plain text: enough to recognise which
// brief the card refers to without reproducing the document in the transcript.
const briefExcerpt = (markdown, max = 180) => {
  const body = String(markdown ?? '')
    .replace(/```[\s\S]*?```/g, ' ')      // fenced code
    .replace(/^\s*#{1,6}\s+.*$/gm, ' ')   // headings
    .replace(/[*_`>#-]/g, ' ')            // inline marks and bullets
    .replace(/\s+/g, ' ')
    .trim();
  if (!body) return 'No brief text.';
  return body.length > max ? `${body.slice(0, max - 1).trimEnd()}…` : body;
};

const ChatPanel = ({ events = [], phase, busy = false, streamingText = null, onSend, onGate, contextCount = 0, onToggleContext, target = null, onClearTarget, onOpenBrief = null }) => {
  const scrollRef = React.useRef(null);
  const [draft, setDraft] = React.useState('');
  // seq of the gate whose one-line note input is currently revealed, and its text.
  const [reviseFor, setReviseFor] = React.useState(null);
  const [note, setNote] = React.useState('');

  // Collapse each consecutive run of planning `status` events to its latest one
  // (create-planning appends a new status per task-count tick). EVERY internal
  // consumer — the three look-ahead helpers below AND the render `.map` — reads
  // `list` so their by-index look-ahead stays consistent.
  const list = window.ChatEvents.collapseStatus(events);

  // Auto-scroll to the newest event whenever one is appended.
  const lastSeq = events.length ? events[events.length - 1].seq : 0;
  React.useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [events.length, lastSeq, busy, streamingText]);

  // 'done' is the legacy terminal state (composer off) — BUT the a4 route still
  // accepts targeted revisions there (the create flow isn't over until Launch, 14c),
  // so an active `target` re-enables the composer at done. No-target done stays
  // disabled; busy/planning disables are unchanged.
  const inputDisabled = busy || phase === 'planning' || (phase === 'done' && !target);

  const send = () => {
    const text = draft.trim();
    if (!text || inputDisabled) return;
    onSend?.(text);
    setDraft('');
  };

  // Look-ahead helpers over the flat list (see the correlation rules above).
  const resolvingGate = (idx, gateId) =>
    list.slice(idx + 1).find((e) => e.kind === 'gate' && e.gate?.id === gateId && e.gate?.decision);
  const hasEarlierOpenGate = (idx, gateId) =>
    list.slice(0, idx).some((e) => e.kind === 'gate' && e.gate?.id === gateId && !e.gate?.decision);
  const revisionStale = (idx) =>
    list.slice(idx + 1).some((e) => e.kind === 'phase' && ['planning', 'plan_gate', 'done'].includes(e.phase));

  const gateTitle = (id) =>
    id === 'brief' ? 'Review the project brief' : id === 'plan' ? 'Review the plan' : 'Your call needed';
  const decisionLabel = (d) => (d === 'approve' ? 'Approved' : d === 'revise' ? 'Revision requested' : d);

  const renderGate = (ev, idx) => {
    // The resolving event (decision set) is not its own card when an earlier open
    // gate exists to attach to. With NO earlier open gate this branch IS the normal
    // Gate-2 render path: approve-from-reviewing emits a single already-resolved
    // gate event (no prior open gate) — do not "clean up" this branch.
    if (ev.gate?.decision) {
      if (hasEarlierOpenGate(idx, ev.gate.id)) return null;
      return (
        <div key={ev.seq} className="rounded-xl bg-neutral-50 border border-neutral-200 p-3">
          <div className="text-sm font-semibold text-neutral-800 mb-2">{gateTitle(ev.gate.id)}</div>
          <div className="text-xs font-semibold uppercase tracking-widest text-neutral-500">
            {decisionLabel(ev.gate.decision)}
          </div>
        </div>
      );
    }

    const resolved = resolvingGate(idx, ev.gate?.id);
    const decided = resolved?.gate?.decision || null;
    const revising = reviseFor === ev.seq;
    return (
      <div key={ev.seq} className="rounded-xl bg-neutral-50 border border-neutral-200 p-3">
        <div className="text-sm font-semibold text-neutral-800 mb-2">{gateTitle(ev.gate?.id)}</div>
        <div className="flex gap-2">
          <button
            type="button"
            disabled={!!decided}
            onClick={() => onGate?.(ev.gate.id, 'approve')}
            className="px-3 py-1.5 text-xs font-semibold rounded-md bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:hover:bg-emerald-600"
          >Approve</button>
          <button
            type="button"
            disabled={!!decided}
            onClick={() => { setReviseFor(revising ? null : ev.seq); setNote(''); }}
            className="px-3 py-1.5 text-xs font-semibold rounded-md bg-white border border-neutral-300 text-neutral-700 hover:bg-neutral-100 disabled:opacity-40 disabled:hover:bg-white"
          >Revise</button>
        </div>
        {decided && (
          <div className="mt-2 text-xs font-semibold uppercase tracking-widest text-neutral-500">
            {decisionLabel(decided)}
          </div>
        )}
        {!decided && revising && (
          <div className="mt-2 flex gap-2">
            <input
              type="text"
              value={note}
              autoFocus
              onChange={(e) => setNote(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter' && note.trim()) { onGate?.(ev.gate.id, 'revise', note.trim()); setReviseFor(null); setNote(''); } }}
              placeholder="What should change?"
              className="flex-1 text-sm px-3 py-1.5 rounded-md border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-300"
            />
            <button
              type="button"
              disabled={!note.trim()}
              onClick={() => { onGate?.(ev.gate.id, 'revise', note.trim()); setReviseFor(null); setNote(''); }}
              className="px-3 py-1.5 text-xs font-semibold rounded-md bg-neutral-800 text-white hover:bg-neutral-900 disabled:opacity-40"
            >Send</button>
          </div>
        )}
      </div>
    );
  };

  const renderEvent = (ev, idx) => {
    switch (ev.kind) {
      case 'user_text':
        return (
          <div key={ev.seq} className="flex justify-end">
            <div className="max-w-[80%] rounded-2xl rounded-br-sm bg-neutral-800 text-white text-sm px-3 py-2 whitespace-pre-wrap">
              {ev.text}
            </div>
          </div>
        );
      case 'agent_text':
        return (
          <div key={ev.seq} className="flex justify-start">
            <div className="max-w-[80%] rounded-2xl rounded-bl-sm bg-neutral-100 text-neutral-800 text-sm px-3 py-2 whitespace-pre-wrap">
              {ev.text}
            </div>
          </div>
        );
      case 'brief_artifact':
        // When the surface has a dedicated Brief view (onOpenBrief supplied), the
        // transcript carries a REFERENCE to it, not a copy: embedding a whole
        // document in a chat bubble read as a glitch — a slab of brief text
        // sliced off mid-sentence, its overflow hidden behind a nested scrollbar
        // nobody finds. Without that view the full brief still renders, but in
        // normal flow so the transcript's own scrollbar reaches all of it.
        return (
          <div key={ev.seq} className="rounded-xl border border-neutral-200 bg-white overflow-hidden">
            <div className="px-3 py-2 border-b border-neutral-200 flex items-center justify-between gap-2">
              <span className="text-xs font-bold uppercase tracking-widest text-neutral-500 truncate">
                {ev.brief?.name || 'Brief'}
              </span>
              {onOpenBrief && (
                <button
                  type="button"
                  onClick={() => onOpenBrief()}
                  className="shrink-0 rounded-full bg-indigo-50 border border-indigo-200 px-2.5 py-0.5 text-[10px] font-bold text-indigo-700 hover:bg-indigo-100"
                >Open the Brief →</button>
              )}
            </div>
            <div className="px-3 py-2">
              {onOpenBrief
                ? <div className="text-xs text-neutral-500">{briefExcerpt(ev.brief?.markdown)}</div>
                : <window.MarkdownView markdown={ev.brief?.markdown} />}
            </div>
          </div>
        );
      case 'status': {
        const s = ev.status || {};
        return (
          <div key={ev.seq} className="rounded-xl bg-neutral-50 border border-neutral-200 p-3">
            <div className="flex items-center gap-2 mb-1">
              {!s.done && <Spinner />}
              <span className="text-sm font-semibold text-neutral-800">{s.label}</span>
            </div>
            <div className="text-xs text-neutral-500">
              {s.features} features · {s.tasks} tasks
            </div>
          </div>
        );
      }
      case 'gate':
        // A kind:"rereview" plan gate renders ONLY as the canvas plan-section
        // affordance (TimelineSection) — never as a transcript card. Additive
        // filter: every other gate renders as before. (Its resolving decision
        // still flows through the look-ahead helpers as an ordinary plan gate.)
        if (ev.gate?.kind === 'rereview') return null;
        return renderGate(ev, idx);
      case 'revision_list': {
        const stale = revisionStale(idx);
        return (
          <div key={ev.seq} className="rounded-xl bg-neutral-50 border border-neutral-200 p-3">
            <div className="text-xs font-bold uppercase tracking-widest text-neutral-500 mb-2">Revisions</div>
            <ol className="list-decimal list-inside text-sm text-neutral-800 space-y-1 mb-3">
              {(ev.items || []).map((item, i) => <li key={i}>{item}</li>)}
            </ol>
            <button
              type="button"
              disabled={stale}
              onClick={() => onGate?.('revisions', 'approve')}
              className="px-3 py-1.5 text-xs font-semibold rounded-md bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:hover:bg-emerald-600"
            >Confirm revisions</button>
          </div>
        );
      }
      case 'phase':
        return (
          <div key={ev.seq} className="flex items-center gap-3 py-1">
            <div className="flex-1 h-px bg-neutral-200" />
            <span className="text-[10px] font-bold uppercase tracking-widest text-neutral-400">
              {PHASE_LABELS[ev.phase] || ev.phase}
            </span>
            <div className="flex-1 h-px bg-neutral-200" />
          </div>
        );
      case 'attachment':
        return (
          <div key={ev.seq} className="flex justify-start">
            <div className="inline-flex items-center max-w-[80%] rounded-full bg-neutral-100 border border-neutral-200 text-xs text-neutral-600 px-3 py-1.5 whitespace-pre-wrap break-words">
              {window.ChatEvents.attachmentChipText(ev.attachment)}
            </div>
          </div>
        );
      case 'error':
        return (
          <div key={ev.seq} className="rounded-xl bg-red-50 border border-red-200 p-3">
            <div className="text-sm text-red-800 whitespace-pre-wrap">{ev.text}</div>
            <div className="mt-1 text-xs text-red-500">Use the Retry button below to continue.</div>
          </div>
        );
      default:
        return null;
    }
  };

  return (
    <div className="flex flex-col h-full">
      <div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-3">
        {list.map((ev, idx) => renderEvent(ev, idx))}
        {busy && !streamingText && (
          <div data-thinking-indicator className="flex justify-start">
            <div className="rounded-2xl rounded-bl-sm bg-neutral-100 px-4 py-3 flex items-center gap-1.5">
              <span className="w-1.5 h-1.5 rounded-full bg-neutral-400 animate-bounce" />
              <span className="w-1.5 h-1.5 rounded-full bg-neutral-400 animate-bounce" style={{ animationDelay: '120ms' }} />
              <span className="w-1.5 h-1.5 rounded-full bg-neutral-400 animate-bounce" style={{ animationDelay: '240ms' }} />
            </div>
          </div>
        )}
        {streamingText && (
          <div className="flex justify-start">
            <div className="max-w-[80%] rounded-2xl rounded-bl-sm bg-neutral-100 text-neutral-800 text-sm px-3 py-2 whitespace-pre-wrap">
              {streamingText}
              <span className="inline-block w-1 h-4 bg-neutral-400 ml-0.5 align-text-bottom animate-pulse" />
            </div>
          </div>
        )}
      </div>
      <div className="border-t border-neutral-200 p-3">
        {/* Target chip — a composer DECORATION owned by the create page (R9b-8):
            clicking a canvas section sets `target`, and the next Send edits that
            step. Orthogonal to `phase`; absent target ⟹ this block is skipped and
            the composer renders/POSTs exactly as before (R4's orchestrator chat). */}
        {target && (
          <div data-target-chip className="mb-2 inline-flex items-center gap-1.5 rounded-full bg-indigo-50 border border-indigo-200 px-2.5 py-1 text-xs font-semibold text-indigo-700">
            <span>Editing: {TARGET_LABELS[target] || target}</span>
            <button
              type="button"
              aria-label="Clear target"
              onClick={() => onClearTarget?.()}
              className="text-indigo-400 hover:text-indigo-700 font-bold leading-none"
            >✕</button>
          </div>
        )}
        <textarea
          rows={3}
          value={draft}
          disabled={inputDisabled}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
          placeholder={phase === 'planning' ? 'Planning in progress…' : phase === 'line_setup' ? 'Ask for changes to the station line…' : phase === 'done' ? (target ? `Ask for changes to the ${target}…` : 'This project is done.') : 'Describe what you want to build…'}
          className="w-full resize-none text-sm px-3 py-2 rounded-lg border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-300 disabled:bg-neutral-50 disabled:text-neutral-400"
        />
        <div data-composer-actions className="mt-2 flex items-center justify-between gap-2">
          <button
            type="button"
            onClick={() => onToggleContext?.()}
            className="px-3 py-2 text-sm font-semibold rounded-lg text-neutral-600 border border-neutral-200 hover:bg-neutral-100 flex items-center gap-1.5"
          >
            + Context
            {contextCount > 0 && (
              <span className="px-1.5 py-0.5 rounded-full bg-neutral-800 text-white text-[10px] font-bold">{contextCount}</span>
            )}
          </button>
          <button
            type="button"
            disabled={inputDisabled || !draft.trim()}
            onClick={send}
            className="px-4 py-2 text-sm font-semibold rounded-lg bg-neutral-800 text-white hover:bg-neutral-900 disabled:opacity-40 disabled:hover:bg-neutral-800"
          >Send</button>
        </div>
      </div>
    </div>
  );
};

if (typeof window !== 'undefined') {
  window.ChatPanel = ChatPanel;
}
