/* ============================================================
   Vapi Live Ops Dashboard — App.jsx
   All components inline. React 18 + Babel standalone (no build).
   ============================================================ */

const { useState, useEffect, useRef, useCallback } = React;

const API = 'https://vapi-demo-yr92.onrender.com';
const VAPI_PUBLIC_KEY = '21550ec5-e633-4a02-800e-86f22e298929';

// ── Vapi SDK singleton ────────────────────────────────────────
let vapiInstance = null;

function getVapiClass() {
  // UMD bundle (balacodeio/Vapi-Web-UMD) exposes window.Vapi directly
  // html-script-tag exposes window.vapiSDK.run (different pattern, not used here)
  if (window.Vapi && typeof window.Vapi === 'function') return window.Vapi;
  return null;
}

function getVapi() {
  if (vapiInstance) return vapiInstance;
  const VapiClass = getVapiClass();
  if (VapiClass) {
    try {
      vapiInstance = new VapiClass(VAPI_PUBLIC_KEY);
      console.log('[Vapi] SDK instantiated successfully');
      return vapiInstance;
    } catch (e) {
      console.error('[Vapi] Failed to instantiate:', e);
      return null;
    }
  }
  return null;
}

// Polls until window.Vapi is available (script may load after React mounts)
function waitForVapi(timeoutMs = 12000) {
  return new Promise((resolve) => {
    // Already loaded?
    const immediate = getVapi();
    if (immediate) return resolve(immediate);

    const started = Date.now();
    const timer = setInterval(() => {
      const v = getVapi();
      if (v) {
        clearInterval(timer);
        resolve(v);
      } else if (Date.now() - started > timeoutMs) {
        clearInterval(timer);
        // Debug: dump what's on window so we know what name the SDK uses
        const vapiKeys = Object.keys(window).filter(k =>
          k.toLowerCase().includes('vapi') || k.toLowerCase().includes('sdk')
        );
        console.error('[Vapi] SDK not found after', timeoutMs, 'ms.');
        console.error('[Vapi] window keys that might be SDK:', vapiKeys);
        resolve(null);
      }
    }, 200);
  });
}

// ── Color tokens ──────────────────────────────────────────────
const C = {
  bg:      '#0f0f0f',
  surface: '#1a1a1a',
  border:  '#2a2a2a',
  teal:    '#00c4a1',
  red:     '#e05252',
  yellow:  '#f5c542',
  orange:  '#e89b4f',
  purple:  '#b48eed',
  blue:    '#4fa8e8',
  green:   '#4fcc6e',
  muted:   '#666',
  text:    '#e0e0e0',
  textDim: '#999',
};

// ── Helpers ───────────────────────────────────────────────────
const ts = () => new Date().toLocaleTimeString('en-GB', { hour12: false });

async function api(path, opts = {}) {
  const res = await fetch(`${API}${path}`, {
    headers: { 'Content-Type': 'application/json' },
    ...opts,
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  if (!res.ok) {
    const text = await res.text().catch(() => res.statusText);
    throw new Error(text || res.statusText);
  }
  const ct = res.headers.get('content-type') || '';
  if (ct.includes('application/json')) return res.json();
  return null;
}

// ── Toast Component ───────────────────────────────────────────
function Toasts({ toasts }) {
  return (
    <div style={{
      position: 'fixed', top: 16, right: 16, zIndex: 9999,
      display: 'flex', flexDirection: 'column', gap: 8, pointerEvents: 'none',
    }}>
      {toasts.map(t => (
        <div key={t.id} style={{
          background: t.type === 'error' ? C.red : C.teal,
          color: '#fff', padding: '10px 16px', borderRadius: 6,
          fontFamily: 'Inter, sans-serif', fontSize: 13, fontWeight: 500,
          animation: 'slideInRight 0.25s ease-out',
          maxWidth: 340, wordBreak: 'break-word',
        }}>
          {t.message}
        </div>
      ))}
    </div>
  );
}

// ── Spinner ───────────────────────────────────────────────────
function Spinner({ size = 14, color = '#fff' }) {
  return (
    <span style={{
      display: 'inline-block', width: size, height: size,
      border: `2px solid ${color}33`, borderTopColor: color,
      borderRadius: '50%', animation: 'spin 0.6s linear infinite',
      verticalAlign: 'middle', marginRight: 6,
    }} />
  );
}

// ── Action Button (with loading state) ────────────────────────
function ActionBtn({ label, onClick, disabled, danger, style: sx }) {
  const [loading, setLoading] = useState(false);
  const mounted = useRef(true);
  useEffect(() => () => { mounted.current = false; }, []);

  const handleClick = async () => {
    setLoading(true);
    try { await onClick(); }
    finally { if (mounted.current) setLoading(false); }
  };

  const bg = danger ? C.red : C.surface;
  const hoverBg = danger ? '#c94444' : '#252525';

  return (
    <button
      disabled={disabled || loading}
      onClick={handleClick}
      style={{
        background: bg, color: C.text, border: `1px solid ${C.border}`,
        borderRadius: 6, padding: '8px 14px', fontSize: 13,
        fontFamily: 'Inter, sans-serif', fontWeight: 500,
        cursor: disabled || loading ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.4 : 1,
        transition: 'background 0.15s, opacity 0.15s',
        display: 'inline-flex', alignItems: 'center', gap: 4,
        whiteSpace: 'nowrap',
        ...sx,
      }}
      onMouseEnter={e => { if (!disabled && !loading) e.currentTarget.style.background = hoverBg; }}
      onMouseLeave={e => { e.currentTarget.style.background = bg; }}
    >
      {loading && <Spinner />}
      {label}
    </button>
  );
}

// ── Call State Badge (SECTION 4 — header) ─────────────────────
function HeaderBadge({ callState, callId, elapsed }) {
  const colors = { idle: C.muted, active: C.green, ended: C.red };
  const labels = { idle: 'IDLE', active: 'ACTIVE', ended: 'ENDED' };
  const color = colors[callState] || C.muted;

  const fmtElapsed = (s) => {
    const m = String(Math.floor(s / 60)).padStart(2, '0');
    const sec = String(s % 60).padStart(2, '0');
    return `${m}:${sec}`;
  };

  return (
    <header style={{
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '14px 24px', background: C.surface,
      borderBottom: `1px solid ${C.border}`,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
        <h1 style={{
          fontSize: 18, fontWeight: 700, color: C.text,
          fontFamily: 'Inter, sans-serif', letterSpacing: '-0.02em',
        }}>
          Vapi Live Demo
        </h1>
        <span style={{
          display: 'inline-block', padding: '3px 12px', borderRadius: 999,
          fontSize: 11, fontWeight: 700, letterSpacing: '0.08em',
          background: `${color}22`, color: color, border: `1px solid ${color}44`,
          transition: 'all 0.3s ease',
        }}>
          {labels[callState]}
        </span>
      </div>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 16,
        fontSize: 13, color: C.textDim, fontFamily: "'JetBrains Mono', monospace",
      }}>
        {callId && (
          <span title={callId}>
            ID: {callId.slice(0, 8)}
          </span>
        )}
        {callState === 'active' && (
          <span style={{ color: C.teal }}>
            ⏱ {fmtElapsed(elapsed)}
          </span>
        )}
      </div>
    </header>
  );
}

// ── Call Control Panel (SECTION 1) ────────────────────────────
function ControlPanel({ callState, webCallUrl, toast, backendReady }) {
  const [assistantId, setAssistantId] = useState(
    () => localStorage.getItem('vapi_assistantId') || 'c36f07da-55e4-46b8-8b35-d3e718f59523'
  );
  const [showSay, setShowSay] = useState(false);
  const [sayMsg, setSayMsg] = useState('');
  const [endAfter, setEndAfter] = useState(false);
  const [showInject, setShowInject] = useState(false);
  const [injectRole, setInjectRole] = useState('system');
  const [injectContent, setInjectContent] = useState('');
  const [injectTrigger, setInjectTrigger] = useState(true);
  const [showTransfer, setShowTransfer] = useState(false);
  const [transferNum, setTransferNum] = useState('');
  const [transferMsg, setTransferMsg] = useState('');

  const active = callState === 'active' && backendReady;

  const saveAssistant = (v) => {
    setAssistantId(v);
    localStorage.setItem('vapi_assistantId', v);
  };

  const doStart = async () => {
    if (!assistantId.trim()) { toast('Enter an Assistant ID first', 'error'); return; }
    const vapi = await waitForVapi();
    if (!vapi) {
      toast('Vapi SDK not loaded', 'error');
      return;
    }
    vapi.start(assistantId.trim());
  };

  const doSay = async () => {
    if (!sayMsg.trim()) return;
    await api('/say', { method: 'POST', body: { content: sayMsg.trim(), endCallAfterSpoken: endAfter } });
    toast('Message sent'); setSayMsg(''); setEndAfter(false); setShowSay(false);
  };

  const doInject = async () => {
    if (!injectContent.trim()) return;
    await api('/add-message', { method: 'POST', body: { role: injectRole, content: injectContent.trim(), triggerResponseEnabled: injectTrigger } });
    toast('Context injected'); setInjectContent(''); setShowInject(false);
  };

  const doTransfer = async () => {
    if (!transferNum.trim()) return;
    await api('/transfer', { method: 'POST', body: { type: 'number', number: transferNum.trim(), content: transferMsg } });
    toast('Transfer initiated'); setTransferNum(''); setTransferMsg(''); setShowTransfer(false);
  };

  const doEnd = async () => {
    if (!window.confirm('End the active call?')) return;
    // 1. Tell backend to send end-call control to Vapi + disconnect wsListener
    try { await api('/end-call', { method: 'POST' }); } catch {}
    // 2. Stop the Vapi SDK — closes the browser WebRTC/audio connection
    const vapi = getVapi();
    if (vapi) vapi.stop();
    toast('Call ended');
  };

  const inputStyle = {
    background: '#111', color: C.text, border: `1px solid ${C.border}`,
    borderRadius: 4, padding: '7px 10px', fontSize: 13,
    fontFamily: "'JetBrains Mono', monospace", outline: 'none', width: '100%',
  };

  const panelStyle = {
    background: '#141414', border: `1px solid ${C.border}`, borderRadius: 6,
    padding: 12, marginTop: 8, animation: 'fadeIn 0.2s ease-out',
  };

  return (
    <section style={{
      background: C.surface, border: `1px solid ${C.border}`, borderRadius: 8,
      padding: 20, margin: '0 24px',
    }}>
      {/* Assistant ID + Start */}
      <div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 14 }}>
        <input
          id="assistant-id-input"
          placeholder="Assistant ID"
          value={assistantId}
          onChange={e => saveAssistant(e.target.value)}
          style={{ ...inputStyle, maxWidth: 340 }}
        />
        <ActionBtn
          label="▶ Start Call"
          disabled={active || !assistantId.trim()}
          onClick={doStart}
          style={{ background: C.green, border: 'none', color: '#fff', fontWeight: 600 }}
        />
      </div>

      {/* Join banner */}
      {active && webCallUrl && (
        <a
          href={webCallUrl}
          target="_blank"
          rel="noopener noreferrer"
          style={{
            display: 'block', background: `${C.teal}18`, border: `1px solid ${C.teal}44`,
            borderRadius: 6, padding: '12px 18px', marginBottom: 14,
            color: C.teal, textDecoration: 'none', fontSize: 14, fontWeight: 600,
            transition: 'background 0.2s',
          }}
          onMouseEnter={e => e.currentTarget.style.background = `${C.teal}28`}
          onMouseLeave={e => e.currentTarget.style.background = `${C.teal}18`}
        >
          🟢 Riley is live. Click to join →
        </a>
      )}

      {/* Action buttons row */}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 4 }}>
        <ActionBtn label="💬 Say Message" disabled={!active} onClick={() => setShowSay(v => !v)} />
        <ActionBtn label="🔇 Mute" disabled={!active} onClick={async () => { await api('/mute', { method: 'POST' }); toast('Muted'); }} />
        <ActionBtn label="🔊 Unmute" disabled={!active} onClick={async () => { await api('/unmute', { method: 'POST' }); toast('Unmuted'); }} />
        <ActionBtn label="➕ Inject Context" disabled={!active} onClick={() => setShowInject(v => !v)} />
        {/* <ActionBtn label="↗ Transfer" disabled={!active} onClick={() => setShowTransfer(v => !v)} /> */}
        <ActionBtn label="📞 End Call" disabled={!active} danger onClick={doEnd} />
      </div>

      {/* Inline panels */}
      {showSay && active && (
        <div style={panelStyle}>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <input placeholder="Message to say…" value={sayMsg} onChange={e => setSayMsg(e.target.value)}
              style={{ ...inputStyle, flex: 1 }}
              onKeyDown={e => e.key === 'Enter' && doSay()}
            />
            <label style={{ fontSize: 12, color: C.textDim, display: 'flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap' }}>
              <input type="checkbox" checked={endAfter} onChange={e => setEndAfter(e.target.checked)} /> End after
            </label>
            <ActionBtn label="Send" onClick={doSay} />
          </div>
        </div>
      )}

      {showInject && active && (
        <div style={panelStyle}>
          <div style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
            <select value={injectRole} onChange={e => setInjectRole(e.target.value)}
              style={{ ...inputStyle, width: 'auto' }}>
              <option value="system">system</option>
              <option value="assistant">assistant</option>
              <option value="user">user</option>
            </select>
            <label style={{ fontSize: 12, color: C.textDim, display: 'flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap' }}>
              <input type="checkbox" checked={injectTrigger} onChange={e => setInjectTrigger(e.target.checked)} /> Trigger response
            </label>
          </div>
          <textarea placeholder="Context message…" value={injectContent} onChange={e => setInjectContent(e.target.value)}
            rows={3} style={{ ...inputStyle, resize: 'vertical', marginBottom: 8 }} />
          <ActionBtn label="Inject" onClick={doInject} />
        </div>
      )}

      {showTransfer && active && (
        <div style={panelStyle}>
          <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
            <input placeholder="Phone number" value={transferNum} onChange={e => setTransferNum(e.target.value)}
              style={{ ...inputStyle, flex: 1 }} />
          </div>
          <input placeholder="Transfer message (optional)" value={transferMsg} onChange={e => setTransferMsg(e.target.value)}
            style={{ ...inputStyle, marginBottom: 8 }} />
          <ActionBtn label="Transfer" onClick={doTransfer} />
        </div>
      )}
    </section>
  );
}

// ── Live Event Log (SECTION 2) ────────────────────────────────
function EventLog({ events, onClear }) {
  const scrollRef = useRef(null);

  useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [events]);

  const colorFor = (type) => {
    switch (type) {
      case 'call-started': return C.green;
      case 'call-ended': return C.red;
      case 'audio-progress': return C.blue;
      case 'vapi-event': return C.purple;
      case 'control-action': return C.orange;
      case 'keyword-detected': return C.yellow;
      case 'error': return C.red;
      case 'info': return C.textDim;
      default: return C.textDim;
    }
  };

  const iconFor = (type) => {
    switch (type) {
      case 'keyword-detected': return '⚡';
      case 'audio-progress': return '🎙';
      case 'error': return '❌';
      case 'call-started': return '📞';
      case 'call-ended': return '🔴';
      case 'control-action': return '🎮';
      case 'vapi-event': return '🤖';
      case 'info': return 'ℹ️';
      default: return '•';
    }
  };

  const formatData = (ev) => {
    const d = ev.data;
    switch (ev.type) {
      case 'audio-progress':
        return `Audio: ${d.chunks || 0} chunks, ${((d.bytes || 0) / 1024).toFixed(1)} KB`;
      case 'vapi-event':
        if (d.event?.transcript) return `[${d.event.role || '?'}] ${d.event.transcript}`;
        return `type: ${d.event?.type || 'unknown'}`;
      case 'control-action':
        return `${d.action || ''} ${d.payload ? JSON.stringify(d.payload) : ''}`;
      case 'keyword-detected':
        return `"${d.keyword}" → ${d.suggestion || d.action || ''}`;
      case 'error':
        return d.message || JSON.stringify(d);
      case 'info':
        return d.message || JSON.stringify(d);
      case 'call-started':
        return `Call ${(d.callId || '').slice(0, 8)} started`;
      case 'call-ended':
        return `Call ${(d.callId || '').slice(0, 8)} ended`;
      default:
        return JSON.stringify(d);
    }
  };

  return (
    <div style={{
      background: C.surface, border: `1px solid ${C.border}`, borderRadius: 8,
      display: 'flex', flexDirection: 'column', overflow: 'hidden', height: '100%',
    }}>
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        padding: '10px 16px', borderBottom: `1px solid ${C.border}`,
      }}>
        <span style={{ fontSize: 13, fontWeight: 600 }}>Live Event Log</span>
        <button onClick={onClear} style={{
          background: 'transparent', border: `1px solid ${C.border}`, borderRadius: 4,
          color: C.textDim, fontSize: 11, padding: '3px 10px', cursor: 'pointer',
          fontFamily: 'Inter, sans-serif',
        }}>
          Clear
        </button>
      </div>
      <div ref={scrollRef} style={{
        flex: 1, overflowY: 'auto', padding: '8px 12px',
        fontFamily: "'JetBrains Mono', monospace", fontSize: 12, lineHeight: 1.7,
      }}>
        {events.length === 0 && (
          <div style={{ color: C.muted, textAlign: 'center', marginTop: 40 }}>
            Waiting for events…
          </div>
        )}
        {events.map((ev, i) => (
          <div key={i} style={{ animation: 'fadeIn 0.15s ease-out', marginBottom: 2 }}>
            <span style={{ color: C.muted }}>{ev.time}</span>
            {' '}
            <span>{iconFor(ev.type)}</span>
            {' '}
            <span style={{ color: colorFor(ev.type), fontWeight: 500 }}>
              [{ev.type}]
            </span>
            {' '}
            <span style={{ color: C.text }}>{formatData(ev)}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ── Audio Monitor (SECTION 3) ─────────────────────────────────
function AudioMonitor({ audioStats, callState }) {
  const receiving = callState === 'active' && audioStats.chunks > 0;
  const durationSec = (audioStats.bytes / 32000).toFixed(1);
  const bars = 12;

  return (
    <div style={{
      background: C.surface, border: `1px solid ${C.border}`, borderRadius: 8,
      display: 'flex', flexDirection: 'column', overflow: 'hidden', height: '100%',
    }}>
      <div style={{
        padding: '10px 16px', borderBottom: `1px solid ${C.border}`,
        fontSize: 13, fontWeight: 600,
      }}>
        Audio Monitor
      </div>
      <div style={{ flex: 1, padding: 16 }}>
        {/* Connection status */}
        <div style={{ marginBottom: 16, fontSize: 13 }}>
          {receiving ? (
            <span style={{ color: C.green }}>🟢 Receiving audio stream…</span>
          ) : (
            <span style={{ color: C.red, animation: callState === 'active' ? 'pulse 1.5s ease infinite' : 'none' }}>
              🔴 Not connected
            </span>
          )}
        </div>

        {/* Stats */}
        <div style={{
          display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 20px',
          marginBottom: 20, fontFamily: "'JetBrains Mono', monospace", fontSize: 12,
        }}>
          <div>
            <div style={{ color: C.textDim, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 2 }}>Chunks</div>
            <div style={{ color: C.text, fontSize: 18, fontWeight: 600 }}>{audioStats.chunks}</div>
          </div>
          <div>
            <div style={{ color: C.textDim, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 2 }}>Buffer</div>
            <div style={{ color: C.text, fontSize: 18, fontWeight: 600 }}>{(audioStats.bytes / 1024).toFixed(1)} KB</div>
          </div>
          <div style={{ gridColumn: '1 / -1' }}>
            <div style={{ color: C.textDim, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 2 }}>Duration (est.)</div>
            <div style={{ color: C.text, fontSize: 18, fontWeight: 600 }}>{durationSec}s</div>
          </div>
        </div>

        {/* Waveform bars */}
        <div style={{
          display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
          gap: 3, height: 40, marginBottom: 20,
        }}>
          {Array.from({ length: bars }).map((_, i) => (
            <div key={i} style={{
              width: 4, borderRadius: 2,
              background: receiving ? C.teal : C.border,
              height: receiving ? undefined : 8,
              animation: receiving ? `waveBar ${0.5 + Math.random() * 0.6}s ease-in-out ${i * 0.07}s infinite alternate` : 'none',
              minHeight: 8,
              transition: 'background 0.3s',
            }} />
          ))}
        </div>

        {/* Download */}
        <a
          href={`${API}/audio`}
          download="recording.pcm"
          style={{
            display: 'inline-block', background: C.surface, color: C.teal,
            border: `1px solid ${C.teal}44`, borderRadius: 6,
            padding: '8px 16px', fontSize: 13, fontWeight: 500,
            textDecoration: 'none', marginBottom: 14,
            transition: 'background 0.2s',
          }}
          onMouseEnter={e => e.currentTarget.style.background = `${C.teal}15`}
          onMouseLeave={e => e.currentTarget.style.background = C.surface}
        >
          ⬇ Download PCM
        </a>

        {/* ffmpeg hint */}
        <div style={{
          marginTop: 8, padding: 10, background: '#111', borderRadius: 4,
          fontFamily: "'JetBrains Mono', monospace", fontSize: 11, color: C.textDim,
          overflowX: 'auto',
        }}>
          <div style={{ color: C.muted, fontSize: 10, marginBottom: 4 }}>Convert to WAV:</div>
          ffmpeg -f s16le -ar 16000 -ac 1 -i recording.pcm output.wav
        </div>
      </div>
    </div>
  );
}

// ── Main App ──────────────────────────────────────────────────
function App() {
  const [callState, setCallState] = useState('idle');   // idle | active | ended
  const [callId, setCallId] = useState(null);
  const [webCallUrl, setWebCallUrl] = useState(null);
  const [elapsed, setElapsed] = useState(0);
  const [events, setEvents] = useState([]);
  const [audioStats, setAudioStats] = useState({ chunks: 0, bytes: 0 });
  const [toasts, setToasts] = useState([]);
  const [backendReady, setBackendReady] = useState(false);

  const callStartRef = useRef(null);
  const timerRef = useRef(null);
  const registeredRef = useRef(false);

  // ── Toast helper ──
  const toast = useCallback((message, type = 'success') => {
    const id = Date.now() + Math.random();
    setToasts(prev => [...prev, { id, message, type }]);
    setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 3000);
  }, []);

  // ── Call start handler ──
  // Strategy: fetch call details directly from Vapi API in the browser.
  // This avoids the backend cold-start problem entirely — we get controlUrl + listenUrl
  // in the frontend, then push them to the backend in one fast call.
  const handleCallStart = async (id) => {
    if (registeredRef.current) return;
    registeredRef.current = true;

    console.log('[App] call-start, callId:', id);
    setCallState('active');
    setCallId(id);
    setAudioStats({ chunks: 0, bytes: 0 });
    setBackendReady(false);

    // When SDK gives us a callId, use /register-call which fetches monitor URLs server-side
    const fetchMonitorUrls = async (retries = 10, delay = 1500) => {
      for (let i = 0; i < retries; i++) {
        try {
          await api('/register-call', { method: 'POST', body: { callId: id } });
          setBackendReady(true);
          setEvents(prev => [...prev, {
            type: 'info',
            data: { message: '✅ Backend armed — all controls ready' },
            time: ts()
          }]);
          return;
        } catch (err) {
          console.warn(`[App] register-call attempt ${i+1}:`, err.message);
        }
        await new Promise(r => setTimeout(r, delay));
      }
      setEvents(prev => [...prev, {
        type: 'error',
        data: { message: 'Backend sync failed — control actions unavailable' },
        time: ts()
      }]);
    };

    setTimeout(() => fetchMonitorUrls(), 1000);
  };

  // ── Vapi SDK events ──
  useEffect(() => {
    let cancelled = false;

    waitForVapi(12000).then((vapi) => {
      if (cancelled) return;
      if (!vapi) {
        // Already logged in waitForVapi — nothing more to do
        return;
      }

      console.log('[Vapi] Attaching event listeners');

      // This UMD bundle fires call-start with undefined — id always comes from Vapi API
      vapi.on('call-start', (callObj) => {
        console.log('[SDK call-start] callObj:', callObj);
        setCallState('active');

        // Extract id if the SDK provides it (newer versions do)
        const sdkId =
          callObj?.id ||
          callObj?.call?.id ||
          callObj?.callId ||
          null;

        if (sdkId) {
          handleCallStart(sdkId);
          return;
        }

        // SDK gave us nothing — query Vapi API directly for the active call
        // This is reliable because the call was just started by this browser
        console.log('[App] callId not in SDK event, querying Vapi API...');
        setEvents(prev => [...prev, {
          type: 'info',
          data: { message: '🔍 Locating call ID from Vapi API...' },
          time: ts()
        }]);

        // Poll backend proxy — backend uses private key to find the active call
        // and arms itself (stores controlUrl+listenUrl) in one shot
        const findActiveCall = async (retries = 10, delay = 1500) => {
          for (let i = 0; i < retries; i++) {
            try {
              const result = await api('/find-active-call');
              if (result?.callId) {
                console.log('[App] Backend found and armed call:', result.callId);
                setCallId(result.callId);
                setBackendReady(true);
                setEvents(prev => [...prev, {
                  type: 'info',
                  data: { message: '✅ Backend armed — all controls ready' },
                  time: ts()
                }]);
                return;
              }
            } catch (err) {
              // 404 = not ready yet, keep retrying
              if (!err.message.includes('404') && !err.message.includes('No active')) {
                console.warn('[App] findActiveCall error:', err.message);
              }
            }
            console.log(`[App] Waiting for call to appear in Vapi (attempt ${i+1}/${retries})…`);
            await new Promise(r => setTimeout(r, delay));
          }
          setEvents(prev => [...prev, {
            type: 'error',
            data: { message: '❌ Could not locate active call — try refreshing' },
            time: ts()
          }]);
        };

        setTimeout(() => findActiveCall(), 2000);
      });

      vapi.on('call-end', () => {
        console.log('[SDK] call-end');

        registeredRef.current = false;

        setCallState('ended');
        setBackendReady(false);

        setEvents(prev => [...prev, {
          type: 'call-ended',
          data: { source: 'vapi-sdk' },
          time: ts()
        }]);
      });

      vapi.on('error', (err) => {
        const msg = err?.message || err?.error?.message || JSON.stringify(err) || 'Unknown Vapi error';
        console.error('[Vapi] error:', msg);
        setEvents(prev => [...prev, { type: 'error', data: { message: msg }, time: ts() }]);
      });

      vapi.on('speech-start', () => {
        setEvents(prev => [...prev, { type: 'info', data: { message: 'Riley is speaking…' }, time: ts() }]);
      });

      vapi.on('speech-end', () => {
        setEvents(prev => [...prev, { type: 'info', data: { message: 'Riley finished speaking' }, time: ts() }]);
      });

      vapi.on('message', (msg) => {
        // transcript messages come through here too
        if (msg?.type === 'transcript') {
          setEvents(prev => [...prev, {
            type: 'vapi-event',
            data: { event: { type: 'transcript', role: msg.role, transcript: msg.transcript } },
            time: ts()
          }]);
        }
      });
    });

    return () => { cancelled = true; };
  }, []);

  // ── Elapsed timer ──
  useEffect(() => {
    if (callState === 'active') {
      callStartRef.current = Date.now();
      timerRef.current = setInterval(() => {
        setElapsed(Math.floor((Date.now() - callStartRef.current) / 1000));
      }, 1000);
    } else {
      clearInterval(timerRef.current);
      if (callState === 'idle') setElapsed(0);
    }
    return () => clearInterval(timerRef.current);
  }, [callState]);

  // ── SSE ──
  useEffect(() => {
    const es = new EventSource(`${API}/events`);

    const addEvent = (type, data) => {
      setEvents(prev => [...prev, { type, data, time: ts() }]);
    };

    const eventTypes = [
      'call-started', 'call-ended', 'listener-connected', 'listener-closed',
      'audio-progress', 'vapi-event', 'control-action', 'keyword-detected',
      'error', 'info',
    ];

    eventTypes.forEach(type => {
      es.addEventListener(type, (e) => {
        let d = {};
        try { d = JSON.parse(e.data); } catch {}

        addEvent(type, d);

        if (type === 'call-started') {
          console.log('SSE call-started:', d);
          setCallState('active');
          setCallId(d.callId || null);
          setWebCallUrl(d.webCallUrl || null);
          setAudioStats({ chunks: 0, bytes: 0 });
        } else if (type === 'call-ended') {
          setCallState('ended');
        } else if (type === 'audio-progress') {
          setAudioStats({ chunks: d.chunks || 0, bytes: d.bytes || 0 });
        }
      });
    });

    es.onerror = () => {
      addEvent('error', { message: 'SSE connection lost — reconnecting…' });
    };

    return () => es.close();
  }, []);

  // ── Polling fallback (every 5s) ──
  useEffect(() => {
    const poll = async () => {
      try {
        const s = await api('/status');
        if (s.status === 'active' && callState !== 'active') {
          setCallState('active');
          setCallId(s.callId);
          setWebCallUrl(s.webCallUrl);
        }
        if (s.audioChunks !== undefined) {
          setAudioStats({ chunks: s.audioChunks, bytes: s.audioBufferBytes || 0 });
        }
      } catch {}
    };
    const id = setInterval(poll, 5000);
    poll();
    return () => clearInterval(id);
  }, [callState]);

  return (
    <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', background: C.bg }}>
      <Toasts toasts={toasts} />

      {/* SECTION 4 — Header badge */}
      <HeaderBadge callState={callState} callId={callId} elapsed={elapsed} />

      {/* SECTION 1 — Control Panel */}
      <div style={{ padding: '16px 0' }}>
        <ControlPanel 
          callState={callState} 
          webCallUrl={webCallUrl} 
          toast={toast} 
          backendReady={backendReady}
        />
      </div>

      {/* SECTIONS 2 + 3 — two column layout */}
      <div style={{
        flex: 1, display: 'grid', gridTemplateColumns: '1fr 380px',
        gap: 16, padding: '0 24px 24px', minHeight: 0,
      }}>
        {/* SECTION 2 — Event Log */}
        <EventLog events={events} onClear={() => setEvents([])} />

        {/* SECTION 3 — Audio Monitor */}
        <AudioMonitor audioStats={audioStats} callState={callState} />
      </div>
    </div>
  );
}

// ── Mount ─────────────────────────────────────────────────────
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);