import { useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import * as api from '../api';
import type { MetaSession } from '../api';
import type { Cli, OverviewFilter, Session, SessionState, Tree } from '../types';
import { renderMarkdown } from '../lib/markdown';
import Logo from './Logo';

const fmtAgo = (ts: number) => {
  if (!ts) return '';
  const m = Math.round((Date.now() - ts) / 60000);
  if (m < 1) return 'now';
  if (m < 60) return `${m}m`;
  if (m < 48 * 60) return `${Math.round(m / 60)}h`;
  return `${Math.round(m / 1440)}d`;
};
const fmtTok = (n = 0) =>
  n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
const base = (p: string) => p.split('/').pop() || p;
const eligible = (s: Session) => s.cli !== 'shell' && s.cli !== 'files';
const bucket = (state: SessionState): OverviewFilter =>
  state === 'working' ? 'working' : state === 'waiting' ? 'waiting' : 'quiet';

const Caret = () => (
  <svg className="ov-caret" viewBox="0 0 10 10" aria-hidden="true"><path d="M1.8 3.2h6.4L5 7.4z" fill="currentColor" /></svg>
);

function Card({ s, color, pending, onOpen, onClose }: {
  s: MetaSession;
  color?: string;
  pending?: boolean; // digest still loading — show a shimmer instead of "no prompt yet"
  onOpen: (sid: string) => void;
  onClose?: () => void; // present when the card lives in the conversation window
}) {
  const d = s.digest;
  const [draft, setDraft] = useState('');
  const [sending, setSending] = useState(false);
  const [failed, setFailed] = useState(false);
  // Optimistic echo: the sent text becomes the prompt line the moment the
  // send succeeds — the digest round-trip (CLI writes transcript → rebuild →
  // poll) can take seconds, and a frozen card reads as "did that get lost?".
  const [sent, setSent] = useState<{ text: string; at: number } | null>(null);
  const [expanded, setExpanded] = useState(false);
  const [promptOpen, setPromptOpen] = useState(false); // click the prompt to read it fully
  const [histIdx, setHistIdx] = useState(0); // 0 = live view, n = n-th exchange back
  const inputRef = useRef<HTMLTextAreaElement>(null);

  // After you send (or when the transcript shows a prompt newer than the last
  // answer), the old answer is stale — a spinner takes its place.
  const digestCaughtUp = !!d && !!sent && d.lastPromptTs >= sent.at - 60_000;
  if (sent && digestCaughtUp) setSent(null);
  // Running: the agent's own task lifecycle when the transcript provides one
  // (codex task_started/complete), else the terminal-derived state.
  const running = !!d?.running || s.state === 'working';
  const awaiting = (!!sent && !digestCaughtUp) || (!!d && !!d.lastPromptText && d.lastPromptTs > d.lastAssistantTs && running);

  const hist = d?.turnsLog ?? [];
  const idx = Math.min(histIdx, hist.length);
  const entry = idx > 0 ? hist[idx - 1] : null;

  const send = async () => {
    const text = draft.trim();
    if (!text || sending) return;
    setSending(true);
    setFailed(false);
    try {
      await api.sendInput(s.id, text);
      setDraft('');
      setSent({ text, at: Date.now() });
      setHistIdx(0);
      setPromptOpen(false);
      if (inputRef.current) { inputRef.current.style.height = 'auto'; inputRef.current.blur(); }
    } catch {
      setFailed(true);
      setTimeout(() => setFailed(false), 4000);
    }
    setSending(false);
  };

  const ago = fmtAgo(Math.max(d?.lastAssistantTs || 0, d?.lastPromptTs || 0) || Date.parse(s.createdAt) || 0);
  const promptText = sent ? sent.text : d?.lastPromptText || '';
  const answerText = entry ? entry.answer : d?.lastAssistantText || '';
  const answerMd = entry ? entry.answerMd : d?.lastAssistantMd || '';
  // Chronological position: hist is newest-first, live text is the newest turn.
  const totalTurns = hist.length + (d?.lastAssistantText ? 1 : 0);
  const metaBits: string[] = [];
  if (d && (d.sinceTurns || d.sinceToolCalls)) {
    metaBits.push(`${d.sinceTurns} turn${d.sinceTurns === 1 ? '' : 's'}`, `${d.sinceToolCalls} tool${d.sinceToolCalls === 1 ? '' : 's'}`);
    if (d.sinceFiles.length) metaBits.push(d.sinceFiles.map(base).join(', '));
    if (d.sinceTokens > 0) metaBits.push(`${fmtTok(d.sinceTokens)} tok`);
  }
  const showLiveProgress = !entry && (running || awaiting);

  return (
    <div className="ov-card">
      <div className="ov-id" onClick={() => onOpen(s.id)} title="Open pane">
        <span className={`status ${s.state}`} />
        <Logo cli={s.cli} size={12} tint={color} />
        <span className="ov-name mono">{s.name}</span>
        {ago && <span className="ov-ago">· {ago}</span>}
        <span className="spacer" />
        <span className="ov-go">open ↗</span>
        {onClose && <button className="ov-x" onClick={(e) => { e.stopPropagation(); onClose(); }} title="Close">✕</button>}
      </div>

      {promptText ? (
        <div
          className={`ov-prompt${promptOpen ? ' open' : ''}`}
          title={promptOpen ? 'Collapse' : 'Show the full prompt'}
          onClick={() => setPromptOpen((v) => !v)}
        >{promptOpen ? (sent ? sent.text : (d?.lastPromptRaw || promptText)) : promptText}</div>
      ) : pending ? (
        <div className="ov-prompt-skel"><span className="skel" style={{ width: '70%' }} /></div>
      ) : (
        <div className="ov-prompt ov-prompt-none">no prompt yet</div>
      )}
      {(metaBits.length > 0 || hist.length > 0) && (
        <div className="ov-meta mono">
          <span className="ov-meta-bits">{metaBits.join(' · ')}</span>
          <span className="spacer" />
          {hist.length > 0 && (
            <span className="ov-nav">
              {idx > 0 && <span className="ov-nav-pos">turn {totalTurns - idx}/{totalTurns}</span>}
              <button
                className="ov-nav-btn" title="Earlier turn" disabled={idx >= hist.length}
                onClick={() => { setHistIdx(Math.min(idx + 1, hist.length)); setExpanded(false); }}
              >↑</button>
              <button
                className="ov-nav-btn" title="Later turn" disabled={idx === 0}
                onClick={() => { setHistIdx(Math.max(idx - 1, 0)); setExpanded(false); }}
              >↓</button>
            </span>
          )}
        </div>
      )}

      {showLiveProgress ? (
        <div className="ov-answer-wrap">
          {answerText && d && d.lastAssistantTs >= d.lastPromptTs && (
            <div className="ov-answer ov-answer-dim">{answerText}</div>
          )}
          <div className="ov-busy mono">running</div>
        </div>
      ) : answerText ? (
        <div className="ov-answer-wrap">
          {expanded ? (
            <div className="markdown ov-md" dangerouslySetInnerHTML={{ __html: renderMarkdown(answerMd || answerText) }} />
          ) : (
            <div className="ov-answer">{answerText}</div>
          )}
          <button className="ov-more" onClick={() => setExpanded((e) => !e)}>{expanded ? 'less' : 'more'}</button>
        </div>
      ) : null}

      <div className="ov-live">
        <span className="ov-p mono">❯</span>
        <textarea
          ref={inputRef}
          rows={1}
          value={draft}
          disabled={sending}
          placeholder={sending ? 'sending…' : 'reply…'}
          autoComplete="off"
          autoCorrect="off"
          autoCapitalize="off"
          spellCheck={false}
          onChange={(e) => { setDraft(e.target.value); e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`; }}
          // iOS doesn't resize the layout for the keyboard — scroll the
          // input into view once the keyboard has animated in.
          onFocus={(e) => { const el = e.currentTarget; setTimeout(() => el.scrollIntoView({ block: 'center', behavior: 'smooth' }), 300); }}
          onKeyDown={(e) => {
            if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
            if (e.key === 'Escape') { setDraft(''); inputRef.current?.blur(); }
          }}
        />
        {draft.trim() && <span className="ov-hint">↵ send · ⇧↵ newline</span>}
      </div>
      {failed && <div className="ov-note">failed to reach the agent</div>}
    </div>
  );
}

/** Compact tile: status + prompt + state; click opens the conversation window. */
function Tile({ s, color, dim, pending, onOpen }: { s: MetaSession; color?: string; dim?: boolean; pending?: boolean; onOpen: () => void }) {
  const d = s.digest;
  const running = !!d?.running || s.state === 'working';
  const last = Math.max(d?.lastAssistantTs || 0, d?.lastPromptTs || 0) || Date.parse(s.createdAt) || 0;
  // ring = waiting on you AND recent — a fleet where everything is "waiting
  // since last week" shouldn't glow everywhere
  const fresh = s.state === 'waiting' && Date.now() - last < 24 * 3600e3;
  return (
    <div className={`ovt-tile${fresh ? ' attn' : ''}${dim ? ' archived' : ''}`} onClick={onOpen}>
      <div className="ovt-head">
        <span className={`status ${s.state}`} />
        <Logo cli={s.cli} size={12} tint={color} />
        <span className="ovt-name mono">{s.name}</span>
        <span className="ovt-ago">{pending ? '' : fmtAgo(last)}</span>
      </div>
      {pending ? (
        <>
          <span className="skel" style={{ width: '82%' }} />
          <span className="skel" style={{ width: '38%', height: 7 }} />
        </>
      ) : (
        <>
          {d?.lastPromptText
            ? <div className="ovt-prompt" title={d.lastPromptText}>{d.lastPromptText}</div>
            : <div className="ovt-prompt none">no prompt yet</div>}
          {running
            ? <div className="ovt-state running mono">running</div>
            : s.state === 'stopped'
              ? <div className="ovt-state stopped mono">stopped</div>
              : d?.lastAssistantText
                ? <div className="ovt-state done mono">✓ done</div>
                : <div className="ovt-state idle mono">idle</div>}
        </>
      )}
    </div>
  );
}

/** Mission control: one reading column — group capsules with their agents as
 *  slabs, loose agents as standalone panels. */
export default function Overview({ clis, tree, filter, view, archived, showArchived, onOpen }: {
  clis: Cli[];
  tree: Tree;
  filter: OverviewFilter; // controlled by the bottom bar in App
  view: 'tiles' | 'list'; // controlled by the bottom bar in App
  archived: Set<string>;
  showArchived: boolean;
  onOpen: (sid: string) => void;
}) {
  const [meta, setMeta] = useState<Record<string, MetaSession>>({});
  // Progressive load: the layout renders immediately from the tree; digests
  // stream in per session (newest first) until the bulk pass lands and marks
  // everything loaded. A tile shows a shimmer until its id is in `loaded`.
  const [loaded, setLoaded] = useState<Set<string>>(new Set());
  const bulkDone = useRef(false);
  const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
  const [durs, setDurs] = useState<Record<string, number>>({});
  const [openId, setOpenId] = useState<string | null>(null); // conversation window
  useEffect(() => {
    if (!openId) return;
    const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpenId(null); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [openId]);

  // Skip the setState when the poll returns byte-identical data — otherwise
  // every tick re-renders every tile (and the conversation window) for nothing.
  const lastPayload = useRef('');
  useEffect(() => {
    let alive = true;
    const load = () => api.getMeta()
      .then((r) => {
        if (!alive) return;
        bulkDone.current = true;
        const payload = JSON.stringify(r.sessions);
        if (payload === lastPayload.current) return;
        lastPayload.current = payload;
        setMeta(Object.fromEntries(r.sessions.map((s) => [s.id, s])));
        setLoaded(new Set(r.sessions.map((s) => s.id)));
      })
      .catch(() => {});
    load();
    const t = setInterval(() => { if (!document.hidden) load(); }, 1000);
    return () => { alive = false; clearInterval(t); };
  }, []);

  // Per-session digests while the bulk build is still running, top of the
  // feed first, a few in flight at a time.
  const progressive = useRef(false);
  useEffect(() => {
    if (progressive.current || bulkDone.current || !tree.sessions.length) return;
    progressive.current = true;
    let alive = true;
    const ids: string[] = [];
    for (const ref of tree.order) {
      if (ref.startsWith('s:')) { const s = tree.sessions.find((x) => x.id === ref.slice(2)); if (s && eligible(s)) ids.push(s.id); }
      else { const g = tree.groups.find((x) => x.id === ref.slice(2)); if (g) for (const sid of g.sessionIds) { const s = tree.sessions.find((x) => x.id === sid); if (s && eligible(s)) ids.push(s.id); } }
    }
    const queue = [...ids];
    const worker = async () => {
      while (alive && !bulkDone.current && queue.length) {
        const id = queue.shift()!;
        try {
          const r = await api.getMetaOne(id);
          if (!alive || bulkDone.current) return;
          if (r.digest) {
            const s = tree.sessions.find((x) => x.id === id);
            if (s) setMeta((m) => ({ ...m, [id]: { ...s, digest: r.digest } }));
            setLoaded((l) => new Set(l).add(id));
          }
        } catch { /* bulk will cover it */ }
      }
    };
    Promise.all([worker(), worker(), worker()]).catch(() => {});
    return () => { alive = false; };
  }, [tree]);

  const colorOf = useMemo(() => Object.fromEntries(clis.map((c) => [c.id, c.color])), [clis]);
  const sessById = useMemo(() => Object.fromEntries(tree.sessions.map((s) => [s.id, s])), [tree.sessions]);
  const groupById = useMemo(() => Object.fromEntries(tree.groups.map((g) => [g.id, g])), [tree.groups]);
  const dataFor = (s: Session): MetaSession => meta[s.id] ?? { ...s, digest: null };
  const pending = (id: string) => !loaded.has(id); // digest not in yet — shimmer

  const visible = (s: MetaSession) =>
    (filter === 'all' || bucket(s.state) === filter) && (showArchived || !archived.has(s.id));

  // Collapse at constant velocity: duration follows the group's height.
  const toggleGroup = (gid: string, el: HTMLElement) => {
    const inner = el.closest('.ov-sec')?.querySelector('.ov-drawer-in') as HTMLElement | null;
    const h = inner?.scrollHeight || 180;
    setDurs((prev) => ({ ...prev, [gid]: Math.min(460, Math.max(170, Math.round(h * 1.4))) }));
    setCollapsed((c) => { const n = new Set(c); n.has(gid) ? n.delete(gid) : n.add(gid); return n; });
  };

  const renderItem = (s: Session) => {
    const m = dataFor(s);
    if (!visible(m)) return null;
    return (
      <div key={s.id} className="ov-panel">
        <Card s={m} color={colorOf[s.cli]} pending={pending(s.id)} onOpen={onOpen} />
      </div>
    );
  };

  // ---- tile view: loose sessions pack into grids, groups get a fine outline ----
  const tileFor = (s: Session) => {
    const m = dataFor(s);
    if (!visible(m)) return null;
    return <Tile key={s.id} s={m} color={colorOf[s.cli]} dim={archived.has(s.id)} pending={pending(s.id)} onOpen={() => setOpenId(s.id)} />;
  };
  const tileBlocks: ReactNode[] = [];
  let looseTiles: ReactNode[] = [];
  const flushLoose = () => {
    if (looseTiles.length) tileBlocks.push(<div className="ovt-grid" key={`loose-${tileBlocks.length}`}>{looseTiles}</div>);
    looseTiles = [];
  };
  for (const ref of tree.order) {
    if (ref.startsWith('s:')) {
      const s = sessById[ref.slice(2)];
      if (s && eligible(s)) { const t = tileFor(s); if (t) looseTiles.push(t); }
    } else {
      const g = groupById[ref.slice(2)];
      if (!g) continue;
      const members = g.sessionIds.map((id) => sessById[id]).filter(Boolean).filter(eligible) as Session[];
      const shown = members.map(tileFor).filter(Boolean);
      if (!shown.length) continue;
      flushLoose();
      tileBlocks.push(
        <div key={g.id} className="ovt-group">
          <span className="ovt-glabel mono">{g.name}<span className="ovt-gn"> {shown.length}</span></span>
          <div className="ovt-grid">{shown}</div>
        </div>,
      );
    }
  }
  flushLoose();

  const openSess = openId ? sessById[openId] : null;
  const windowEl = openSess && (
    <div className="ovw-backdrop" onClick={() => setOpenId(null)}>
      <div className="ovw-win" onClick={(e) => e.stopPropagation()}>
        <Card s={dataFor(openSess)} color={colorOf[openSess.cli]} pending={pending(openSess.id)} onOpen={onOpen} onClose={() => setOpenId(null)} />
      </div>
    </div>
  );

  if (view === 'tiles') {
    return (
      <div className="ov-wrap">
        <div className="ov-feed ovt-feed">
          {tileBlocks.length === 0 && <div className="usage-msg mono">{filter === 'all' ? 'no agents yet — shells and file panes don’t appear here.' : 'nothing in this state.'}</div>}
          {tileBlocks}
        </div>
        {windowEl}
      </div>
    );
  }

  const blocks: ReactNode[] = [];
  for (const ref of tree.order) {
    if (ref.startsWith('s:')) {
      const s = sessById[ref.slice(2)];
      if (s && eligible(s)) {
        const el = renderItem(s);
        if (el) blocks.push(el);
      }
    } else {
      const g = groupById[ref.slice(2)];
      if (!g) continue;
      const members = g.sessionIds.map((id) => sessById[id]).filter(Boolean).filter(eligible) as Session[];
      const shown = members.map((s) => ({ s, el: renderItem(s) })).filter((x) => x.el);
      if (!shown.length) continue;
      const open = !collapsed.has(g.id);
      blocks.push(
        <div key={g.id} className={`ov-sec${open ? '' : ' closed'}`} style={{ '--dur': `${durs[g.id] ?? 360}ms` } as CSSProperties}>
          <button className="ov-sechead" onClick={(e) => toggleGroup(g.id, e.currentTarget)}>
            <Caret />
            <span className="ov-sectitle">{g.name}</span>
            <span className="ov-secn mono">{shown.length}</span>
            <span className="ov-peek">
              {shown.map(({ s }) => <span key={s.id} className={`status ${dataFor(s).state}`} />)}
            </span>
          </button>
          <div className="ov-drawer"><div className="ov-drawer-in">
            <div className="ov-secbody">{shown.map(({ el }) => el)}</div>
          </div></div>
          <div className="ov-foot" />
        </div>,
      );
    }
  }

  return (
    <div className="ov-wrap">
      <div className="ov-feed">
        {blocks.length === 0 && <div className="usage-msg mono">{filter === 'all' ? 'no agents yet — shells and file panes don’t appear here.' : 'nothing in this state.'}</div>}
        {blocks}
      </div>
    </div>
  );
}
