/* ========================================================================
   PURSUIT — AGENT PANEL (Drop 3, §5)
   Floating, movable, resizable chat overlay in its OWN React root
   (#agent-panel-root, sibling of #root) — the app's route-driven renders
   can never unmount it. Open state, position, size, collapse, scroll,
   draft, and message history all survive navigation; only explicit user
   close/collapse dismisses it.

   Talks to the gateway chat API via window.PursuitChat (SSE; local stub
   offline). Record chips ([[entity:key|Label]] in assistant text) open
   the detail view through the command registry. Sends fresh view context
   over the bridge on every user turn (PursuitBridge.sendContext).

   Testability (§8): data-testid="agent-panel-*", data-loading + aria-busy
   on the message list, window.__PUPPET_TEST_MODE__ -> data-test-mode.
   ======================================================================== */

const AGENT_PANEL_STORE = 'ps-agent-panel-v1';

function agentPanelLoadGeom() {
  try {
    const g = JSON.parse(localStorage.getItem(AGENT_PANEL_STORE)) || {};
    return {
      x: Number.isFinite(g.x) ? g.x : null,
      y: Number.isFinite(g.y) ? g.y : null,
      w: Number.isFinite(g.w) ? g.w : 380,
      h: Number.isFinite(g.h) ? g.h : 520,
      collapsed: !!g.collapsed,
      open: !!g.open,
    };
  } catch (e) { return { x: null, y: null, w: 380, h: 520, collapsed: false, open: false }; }
}
function agentPanelSaveGeom(patch) {
  try {
    const cur = JSON.parse(localStorage.getItem(AGENT_PANEL_STORE)) || {};
    localStorage.setItem(AGENT_PANEL_STORE, JSON.stringify({ ...cur, ...patch }));
  } catch (e) {}
}
function agentPanelClamp(x, y, w, h) {
  const vw = window.innerWidth, vh = window.innerHeight;
  return {
    x: Math.min(Math.max(x, 8 - w + 80), vw - 80),
    y: Math.min(Math.max(y, 8), vh - 48),
  };
}

/* ---- assistant text rendering: paragraphs + record chips ---------------- */
const AGENT_CHIP_RE = /\[\[(accounts|contacts|signals):([^|\]]+)\|([^\]]+)\]\]/g;
function AgentRichText({ text }) {
  const parts = [];
  let last = 0, m, k = 0;
  AGENT_CHIP_RE.lastIndex = 0;
  while ((m = AGENT_CHIP_RE.exec(text)) !== null) {
    if (m.index > last) parts.push(text.slice(last, m.index));
    const [, entity, key, label] = m;
    parts.push(
      <button
        key={'chip' + (k++)}
        type="button"
        className="agent-chip-record"
        data-testid={'agent-chip-record-' + entity}
        onClick={() => window.PursuitAgent && window.PursuitAgent.call('detail.open', { entity, key })}
      >
        <Icon name={entity === 'accounts' ? 'building-2' : entity === 'contacts' ? 'user' : 'radar'} size={12} />
        {label}
      </button>
    );
    last = m.index + m[0].length;
  }
  if (last < text.length) parts.push(text.slice(last));
  return <span>{parts}</span>;
}

/* ---- activity log (journal view) ------------------------------------------ */
function AgentActivityLog() {
  const [entries, setEntries] = React.useState(null);
  const load = React.useCallback(() => {
    window.PursuitJournal.list({ limit: 100 }).then(r => setEntries(r.entries || []));
  }, []);
  React.useEffect(() => {
    load();
    window.addEventListener('pursuit:journal-changed', load);
    return () => window.removeEventListener('pursuit:journal-changed', load);
  }, [load]);
  const fmtTime = (ts) => {
    try { return new Date(ts).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); }
    catch (e) { return ''; }
  };
  return (
    <div className="agent-panel-messages agent-log" data-testid="agent-panel-log"
      data-loading={entries === null ? 'true' : 'false'} aria-busy={entries === null ? 'true' : 'false'}>
      {entries === null ? null : entries.length === 0 ? (
        <div className="agent-empty">
          <Icon name="history" size={28} style={{ color: 'var(--ps-neutral-300)' }} />
          <div>No activity yet. Changes made by you or the agent will appear here.</div>
        </div>
      ) : entries.slice().reverse().map((e, i) => (
        <div key={e.id || i} className={'agent-log-entry' + (e.undone ? ' undone' : '')} data-testid="agent-log-entry">
          <span className={'agent-log-src ' + (e.source === 'agent' ? 'agent' : 'user')}>
            <Icon name={e.source === 'agent' ? 'sparkles' : 'user'} size={12} />
          </span>
          <span className="agent-log-body">
            <span className="agent-log-summary">{e.summary || e.command}</span>
            <span className="agent-log-meta">
              <span className="agent-log-cmd">{e.command}</span>
              {e.timestamp ? ' · ' + fmtTime(e.timestamp) : ''}
              {e.undone ? ' · reverted' : ''}
            </span>
          </span>
        </div>
      ))}
    </div>
  );
}

function AgentSegment({ seg }) {
  if (seg.type === 'thinking') {
    return <div className="agent-seg-thinking" data-testid="agent-seg-thinking">{seg.content}</div>;
  }
  if (seg.type === 'tool_use') {
    return (
      <div className="agent-seg-step" data-testid="agent-seg-tool-use">
        <Icon name="wrench" size={12} />
        <span className="step-cmd">{seg.tool}</span>
        <span>{seg.input && seg.input.entity ? seg.input.entity : ''}</span>
      </div>
    );
  }
  if (seg.type === 'tool_result') {
    return (
      <div className={'agent-seg-step' + (seg.isError ? ' error' : '')} data-testid="agent-seg-tool-result">
        <Icon name={seg.isError ? 'circle-alert' : 'check'} size={12} />
        <span>{typeof seg.content === 'string' ? seg.content : JSON.stringify(seg.content)}</span>
      </div>
    );
  }
  if (seg.type === 'system') {
    return <div className="agent-seg-thinking">{seg.content}</div>;
  }
  return null; // 'text' is rendered from the streamed buffer; 'result' is meta
}

function AgentPanel() {
  const geom0 = React.useMemo(agentPanelLoadGeom, []);
  const [open, setOpen] = React.useState(geom0.open);
  const [collapsed, setCollapsed] = React.useState(geom0.collapsed);
  const [rect, setRect] = React.useState(() => {
    const w = geom0.w, h = geom0.h;
    const x = geom0.x != null ? geom0.x : window.innerWidth - w - 24;
    const y = geom0.y != null ? geom0.y : window.innerHeight - h - 24;
    return { x, y, w, h };
  });
  const [messages, setMessages] = React.useState([]);   // {role:'user',text} | {role:'assistant',segments:[],text,streaming} | {role:'error',text}
  const [draft, setDraft] = React.useState('');
  const [attached, setAttached] = React.useState(null);  // selection context pending on next turn
  const [confirms, setConfirms] = React.useState([]);     // pending export-tier confirm requests
  const [view, setView] = React.useState('chat');         // 'chat' | 'log'
  const [busy, setBusy] = React.useState(false);
  const [bridgeState, setBridgeState] = React.useState(window.__bridgeState || (window.PursuitChat && window.PursuitChat.mode === 'stub' ? 'stub' : 'closed'));
  const scrollRef = React.useRef(null);
  const inputRef = React.useRef(null);
  const streamRef = React.useRef(null);

  /* open/close API for the Ask AI CTA + keyboard shortcut */
  React.useEffect(() => {
    window.PursuitAgentPanel = {
      open: () => {
        setOpen(true); setCollapsed(false);
        agentPanelSaveGeom({ open: true, collapsed: false });
        // Keyboard-only flow: opening the panel while a selection is active
        // attaches it (§6 acceptance).
        if (window.__agentSelection) setAttached(window.__agentSelection);
        setTimeout(() => inputRef.current && inputRef.current.focus(), 30);
      },
      close: () => { setOpen(false); agentPanelSaveGeom({ open: false }); },
      toggle: () => setOpen(o => { agentPanelSaveGeom({ open: !o }); return !o; }),
    };
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); window.PursuitAgentPanel.open(); }
    };
    window.addEventListener('keydown', onKey);
    const onBridge = (e) => setBridgeState(e.detail);
    window.addEventListener('pursuit:bridge-state', onBridge);
    const onAskSelection = (e) => {
      window.PursuitAgentPanel.open();
      if (e.detail) setAttached(e.detail);
    };
    window.addEventListener('pursuit:agent-ask-selection', onAskSelection);
    // Export-tier confirm requests (bridge §7): open the panel and queue a chip.
    const onConfirm = (e) => {
      window.PursuitAgentPanel.open();
      setView('chat');
      setConfirms(cs => [...cs, e.detail]);
    };
    window.addEventListener('pursuit:agent-confirm', onConfirm);
    return () => {
      window.removeEventListener('keydown', onKey);
      window.removeEventListener('pursuit:bridge-state', onBridge);
      window.removeEventListener('pursuit:agent-ask-selection', onAskSelection);
      window.removeEventListener('pursuit:agent-confirm', onConfirm);
    };
  }, []);

  /* restore thread history once (gateway mode only) */
  React.useEffect(() => {
    if (!window.PursuitChat || window.PursuitChat.mode !== 'gateway') return;
    window.PursuitChat.history().then(items => {
      if (!items || !items.length) return;
      setMessages(items.map(it => ({
        role: it.role === 'user' ? 'user' : 'assistant',
        text: String(it.content || it.text || ''),
        segments: [],
      })));
    });
  }, []);

  /* keep scrolled to bottom while streaming */
  React.useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [messages, busy]);

  /* ---- drag / resize ---------------------------------------------------- */
  const dragFrom = (e, mode) => {
    if (e.button !== 0) return;
    e.preventDefault();
    const start = { mx: e.clientX, my: e.clientY, ...rect };
    const move = (ev) => {
      if (mode === 'drag') {
        const p = agentPanelClamp(start.x + ev.clientX - start.mx, start.y + ev.clientY - start.my, start.w, start.h);
        setRect(r => ({ ...r, ...p }));
      } else {
        setRect(r => ({
          ...r,
          w: Math.max(320, start.w + ev.clientX - start.mx),
          h: Math.max(280, start.h + ev.clientY - start.my),
        }));
      }
    };
    const up = () => {
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
      setRect(r => { agentPanelSaveGeom({ x: r.x, y: r.y, w: r.w, h: r.h }); return r; });
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  /* ---- send -------------------------------------------------------------- */
  const send = () => {
    const text = draft.trim();
    if (!text || busy) return;
    setDraft('');
    setBusy(true);
    // Piggyback fresh view context + any attached selection (bridge contract §4/§6).
    const selection = attached ? { text: attached.text, refs: attached.refs } : undefined;
    try { window.PursuitBridge && window.PursuitBridge.sendContext(selection); } catch (e) {}
    setMessages(ms => [...ms, { role: 'user', text, attachment: attached }, { role: 'assistant', text: '', segments: [], streaming: true }]);
    setAttached(null);
    const patchLast = (fn) => setMessages(ms => {
      const out = ms.slice();
      const last = { ...out[out.length - 1] };
      fn(last);
      out[out.length - 1] = last;
      return out;
    });
    streamRef.current = window.PursuitChat.send(text, {
      onToken: (t) => patchLast(m => { m.text = (m.text || '') + t; }),
      onSegment: (seg) => {
        if (seg.type === 'text') patchLast(m => { m.text = seg.content; });
        else patchLast(m => { m.segments = [...(m.segments || []), seg]; });
      },
      onError: (err) => {
        patchLast(m => { m.streaming = false; });
        setMessages(ms => [...ms, { role: 'error', text: (err && err.error) || 'Something went wrong.' }]);
        setBusy(false);
      },
      onDone: (payload) => {
        patchLast(m => { m.streaming = false; if (payload && payload.response) m.text = payload.response; });
        setBusy(false);
      },
    });
  };

  const onInputKey = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
  };

  const settleConfirm = (req, action) => {
    setConfirms(cs => cs.filter(c => c !== req));
    try { action(); } catch (e) {}
  };

  const collapse = () => { setCollapsed(true); agentPanelSaveGeom({ collapsed: true }); };
  const expand = () => { setCollapsed(false); agentPanelSaveGeom({ collapsed: false }); };
  const close = () => { setOpen(false); agentPanelSaveGeom({ open: false }); };

  if (!open) return null;

  const statusLabel = bridgeState === 'open' ? 'Connected'
    : bridgeState === 'connecting' ? 'Connecting…'
    : (window.PursuitChat && window.PursuitChat.mode === 'stub') ? 'Local stub' : 'Offline';
  const statusClass = bridgeState === 'open' ? 'open' : bridgeState === 'connecting' ? 'connecting' : (window.PursuitChat && window.PursuitChat.mode === 'stub') ? 'stub' : 'closed';

  if (collapsed) {
    return (
      <button
        type="button"
        className="agent-pill"
        style={{ left: Math.min(rect.x + rect.w - 44, window.innerWidth - 60), top: Math.min(rect.y + rect.h - 44, window.innerHeight - 60) }}
        data-testid="agent-panel-pill"
        aria-label="Expand agent panel"
        onClick={expand}
      >
        <Icon name="sparkles" size={18} />
      </button>
    );
  }

  return (
    <div
      className="agent-panel"
      style={{ left: rect.x, top: rect.y, width: rect.w, height: rect.h }}
      data-testid="agent-panel"
      role="dialog"
      aria-label="Agent"
    >
      <div className="agent-panel-header" data-testid="agent-panel-header" onPointerDown={(e) => { if (!e.target.closest('button')) dragFrom(e, 'drag'); }}>
        <div className="agent-panel-title">
          <span className="icon-frame"><Icon name="sparkles" size={14} /></span>
          Agent
        </div>
        <span className={'agent-status-dot ' + statusClass} data-testid="agent-panel-status" title={statusLabel}></span>
        <span className="agent-status-label">{statusLabel}</span>
        <div className="agent-panel-header-btns">
          <button type="button" className={'agent-hbtn' + (view === 'log' ? ' active' : '')} data-testid="agent-panel-log-toggle" aria-label="Activity log" onClick={() => setView(v => v === 'log' ? 'chat' : 'log')}>
            <Icon name="history" size={14} />
          </button>
          <button type="button" className="agent-hbtn" data-testid="agent-panel-collapse" aria-label="Collapse to pill" onClick={collapse}>
            <Icon name="minus" size={14} />
          </button>
          <button type="button" className="agent-hbtn" data-testid="agent-panel-close" aria-label="Close agent panel" onClick={close}>
            <Icon name="x" size={14} />
          </button>
        </div>
      </div>

      {view === 'log' ? <AgentActivityLog /> : (
      <div
        className="agent-panel-messages"
        ref={scrollRef}
        data-testid="agent-panel-messages"
        data-loading={busy ? 'true' : 'false'}
        aria-busy={busy ? 'true' : 'false'}
      >
        {messages.length === 0 ? (
          <div className="agent-empty">
            <Icon name="sparkles" size={28} style={{ color: 'var(--ps-neutral-300)' }} />
            <div>Ask about your accounts, signals, or worksheets — or tell the agent what to do. It can navigate and filter the app for you.</div>
          </div>
        ) : messages.map((m, i) => {
          if (m.role === 'user') return (
            <div key={i} className="agent-msg-user" data-testid="agent-msg-user">
              {m.attachment ? (
                <span className="agent-attach-note" data-testid="agent-msg-attachment">
                  <Icon name="text-select" size={11} />
                  {(m.attachment.refs || []).length
                    ? (m.attachment.refs.length + ' record' + (m.attachment.refs.length === 1 ? '' : 's') + ' attached')
                    : 'selection attached'}
                </span>
              ) : null}
              {m.text}
            </div>
          );
          if (m.role === 'error') return <div key={i} className="agent-msg-error" data-testid="agent-msg-error">{m.text}</div>;
          return (
            <div key={i} className="agent-msg-assistant" data-testid="agent-msg-assistant">
              {(m.segments || []).map((seg, j) => <AgentSegment key={j} seg={seg} />)}
              {m.text ? <AgentRichText text={m.text} /> : null}
              {m.streaming && !m.text ? <span className="agent-typing"><span></span><span></span><span></span></span> : null}
            </div>
          );
        })}
      </div>
      )}

      <div className="agent-panel-composer">
        {confirms.map((req, i) => (
          <div key={i} className="agent-confirm-chip" data-testid="agent-confirm-chip" role="alertdialog" aria-label="Confirm agent action">
            <Icon name="shield-alert" size={14} />
            <span className="agent-confirm-summary">{req.summary || (req.command + ' — confirm to run')}</span>
            <button type="button" className="agent-confirm-run" data-testid="confirm-chip-run" onClick={() => settleConfirm(req, req.accept)}>Run</button>
            <button type="button" className="agent-confirm-cancel" data-testid="confirm-chip-cancel" onClick={() => settleConfirm(req, req.decline)}>Cancel</button>
          </div>
        ))}
        {attached ? (
          <div className="agent-attach" data-testid="agent-panel-attachment">
            <Icon name="text-select" size={12} />
            <span className="agent-attach-label">
              {(attached.refs || []).length
                ? attached.refs.length + ' record' + (attached.refs.length === 1 ? '' : 's') + ' · '
                : ''}
              “{attached.text.slice(0, 60)}{attached.text.length > 60 ? '…' : ''}”
            </span>
            <button type="button" className="agent-hbtn" data-testid="agent-panel-attachment-remove" aria-label="Remove attached selection" onClick={() => setAttached(null)}>
              <Icon name="x" size={12} />
            </button>
          </div>
        ) : null}
        <textarea
          ref={inputRef}
          className="agent-panel-input"
          data-testid="agent-panel-input"
          rows={1}
          placeholder="Ask or direct the agent…"
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={onInputKey}
        />
        <button
          type="button"
          className="agent-panel-send"
          data-testid="agent-panel-send"
          aria-label="Send"
          disabled={busy || !draft.trim()}
          onClick={send}
        >
          <Icon name="arrow-up" size={16} />
        </button>
      </div>

      <div className="agent-panel-resize" data-testid="agent-panel-resize" onPointerDown={(e) => dragFrom(e, 'resize')}>
        <Icon name="grip" size={12} />
      </div>
    </div>
  );
}

/* ---- mount: own root, outside the app tree ------------------------------- */
(function mountAgentPanel() {
  let host = document.getElementById('agent-panel-root');
  if (!host) {
    host = document.createElement('div');
    host.id = 'agent-panel-root';
    host.className = 'agent-panel-layer';
    if (window.__PUPPET_TEST_MODE__) host.setAttribute('data-test-mode', '');
    document.body.appendChild(host);
  }
  ReactDOM.createRoot(host).render(<AgentPanel />);
})();
