// @replaces: function EchoDemo()
// Live demo widget for optusecho.com. Replaces the exported module (which relied on the design
// tool's preview runtime) with a real call engine: Turnstile -> /api/demo/session -> WebSocket relay.
// The UI (persona tabs, status, orb, visualiser, start/end) is kept from the export.
const { useState: useS, useEffect: useE, useRef: useR } = React;
const DI = window.IOptusIcons;

const PERSONAS = [
  { id: 'booking', tab: 'Table booking', agent: 'Antonio', org: 'Trattoria Optima', line: 'Reservations · English or Italian', hint: 'Book, change or cancel a table. Try switching to Italian mid-call.' },
  { id: 'support', tab: 'Customer support', agent: 'Eleanor', org: 'Clearpath IT', line: 'Support desk', hint: 'Report a problem or ask about an existing ticket. Try interrupting or changing your mind.' },
  { id: 'survey', tab: 'Outbound survey', agent: 'Hugo', org: 'Tallyhouse Research', line: 'Survey call', hint: 'Hugo calls you to run a short survey. Answer as yourself or play a difficult respondent.' },
];

const fmtT = (s) => String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
const STATUS = { idle: 'Ready', connecting: 'Connecting…', listening: 'Listening', thinking: 'Thinking…', speaking: 'Speaking', ended: 'Call ended', limited: 'Demo limit reached' };
const API = '/api/demo';

/* Best-effort browser fingerprint: lets the server recognise the same browser after cookies are cleared. */
function fingerprint() {
  const n = navigator, s = screen;
  const fp = { ua: n.userAgent, lang: n.language, langs: (n.languages || []).join(','), plat: n.platform, hc: n.hardwareConcurrency || 0, mem: n.deviceMemory || 0, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, scr: [s.width, s.height, s.colorDepth, window.devicePixelRatio].join('x'), touch: n.maxTouchPoints || 0 };
  try {
    const c = document.createElement('canvas'); c.width = 200; c.height = 40; const g = c.getContext('2d');
    g.textBaseline = 'top'; g.font = '16px Arial'; g.fillStyle = '#f60'; g.fillRect(10, 5, 60, 20); g.fillStyle = '#069'; g.fillText('OptusEcho ☺', 2, 2);
    const d = c.toDataURL(); let h = 0; for (let i = 0; i < d.length; i++) h = (h * 31 + d.charCodeAt(i)) | 0; fp.cv = String(h);
  } catch (e) {}
  return fp;
}

function int16ToBase64(int16) {
  const bytes = new Uint8Array(int16.buffer); let bin = '';
  for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  return btoa(bin);
}
function downsample(buffer, from, to) {
  if (to >= from) return buffer;
  const ratio = from / to, out = new Float32Array(Math.round(buffer.length / ratio));
  let o = 0, i = 0;
  while (o < out.length) { const next = Math.round((o + 1) * ratio); let sum = 0, cnt = 0; for (; i < next && i < buffer.length; i++) { sum += buffer[i]; cnt++; } out[o++] = cnt ? sum / cnt : 0; }
  return out;
}

function EchoDemo() {
  const [pid, setPid] = useS('booking');
  const [status, setStatusS] = useS('idle');
  const [lines, setLines] = useS([]);
  const [events, setEvents] = useS([]);
  const [secs, setSecs] = useS(0);
  const [note, setNote] = useS('');
  const [limit, setLimit] = useS(null);
  const st = useR('idle'), live = useR(false), ws = useR(null), bars = useR(null), analyser = useR(null);
  const mic = useR({ ctx: null, stream: null, node: null, src: null });
  const play = useR({ ctx: null, next: 0, sources: [], rate: 16000 });
  const p = PERSONAS.find((x) => x.id === pid);
  const setStatus = (s) => { st.current = s; setStatusS(s); };

  useE(() => { if (status === 'idle' || status === 'ended' || status === 'limited') return; const t = setInterval(() => setSecs((s) => s + 1), 1000); return () => clearInterval(t); }, [status === 'idle' || status === 'ended' || status === 'limited']);
  useE(() => { const h = () => { if (!live.current) start(); }; window.addEventListener('echo:start', h); return () => window.removeEventListener('echo:start', h); });

  /* Visualiser: real mic level while listening, synthetic motion while the agent speaks. */
  useE(() => {
    let raf, buf; const n = 28;
    const loop = (t) => {
      const el = bars.current; if (el) {
        const kids = el.children; let lvl = 0;
        if (st.current === 'listening' && analyser.current) { buf = buf || new Uint8Array(analyser.current.frequencyBinCount); analyser.current.getByteFrequencyData(buf); for (let i = 0; i < 40; i++) lvl += buf[i]; lvl = Math.min(1, lvl / 40 / 110); }
        for (let i = 0; i < n; i++) {
          const c = Math.abs(i - n / 2) / (n / 2); const shape = 1 - c * 0.75; let v = 0.06;
          if (st.current === 'speaking') v = 0.15 + shape * (0.45 + 0.4 * Math.sin(t / 110 + i * 0.9) * Math.sin(t / 270 + i * 0.35));
          else if (st.current === 'listening') v = 0.06 + shape * lvl * (0.7 + 0.3 * Math.sin(t / 90 + i));
          else if (st.current === 'thinking' || st.current === 'connecting') v = 0.08 + 0.1 * (0.5 + 0.5 * Math.sin(t / 200 - i * 0.4));
          kids[i].style.transform = 'scaleY(' + Math.max(0.05, Math.min(1, v)).toFixed(3) + ')';
        }
      }
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf);
  }, []);

  /* ---- playback (PCM16 mono from the relay) ---- */
  const ensurePlayCtx = () => {
    const P = play.current;
    if (!P.ctx || P.ctx.state === 'closed') { P.ctx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: P.rate }); P.next = 0; }
    if (P.ctx.state === 'suspended') P.ctx.resume();
    return P.ctx;
  };
  const playAudio = (b64) => {
    const P = play.current; const ctx = ensurePlayCtx();
    const bin = atob(b64); const pcm = new Int16Array(bin.length / 2);
    for (let i = 0; i < pcm.length; i++) pcm[i] = (bin.charCodeAt(i * 2) | (bin.charCodeAt(i * 2 + 1) << 8));
    const buffer = ctx.createBuffer(1, pcm.length, P.rate); const ch = buffer.getChannelData(0);
    for (let i = 0; i < pcm.length; i++) ch[i] = pcm[i] / 32768;
    const src = ctx.createBufferSource(); src.buffer = buffer; src.connect(ctx.destination);
    const now = ctx.currentTime; if (P.next < now) P.next = now;
    src.start(P.next); P.next += buffer.duration; P.sources.push(src);
    if (live.current && st.current !== 'speaking') setStatus('speaking');
    src.onended = () => { P.sources = P.sources.filter((s) => s !== src); if (!P.sources.length && live.current && st.current === 'speaking') setStatus('listening'); };
  };
  const stopPlayback = () => { const P = play.current; P.sources.forEach((s) => { try { s.stop(); } catch (e) {} }); P.sources = []; if (P.ctx) P.next = P.ctx.currentTime; if (live.current && st.current === 'speaking') setStatus('listening'); };

  /* ---- microphone -> relay ---- */
  const openMic = async () => {
    const M = mic.current;
    M.stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
    const Ctx = window.AudioContext || window.webkitAudioContext;
    try { M.ctx = new Ctx({ sampleRate: 16000 }); } catch (e) { M.ctx = new Ctx(); }
    M.src = M.ctx.createMediaStreamSource(M.stream);
    const a = M.ctx.createAnalyser(); a.fftSize = 256; M.src.connect(a); analyser.current = a;
    M.node = M.ctx.createScriptProcessor(4096, 1, 1);
    M.node.onaudioprocess = (e) => {
      const sock = ws.current; if (!sock || sock.readyState !== WebSocket.OPEN) return;
      const input = e.inputBuffer.getChannelData(0);
      const data = M.ctx.sampleRate === 16000 ? input : downsample(input, M.ctx.sampleRate, 16000);
      const pcm = new Int16Array(data.length);
      for (let i = 0; i < data.length; i++) { const s = Math.max(-1, Math.min(1, data[i])); pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff; }
      sock.send(JSON.stringify({ a: int16ToBase64(pcm) }));
    };
    M.src.connect(M.node); M.node.connect(M.ctx.destination);
  };
  const closeMic = () => {
    const M = mic.current;
    if (M.node) { try { M.node.disconnect(); } catch (e) {} M.node = null; }
    if (M.src) { try { M.src.disconnect(); } catch (e) {} M.src = null; }
    if (M.stream) { M.stream.getTracks().forEach((t) => t.stop()); M.stream = null; }
    if (M.ctx) { try { M.ctx.close(); } catch (e) {} M.ctx = null; }
    analyser.current = null;
  };

  const send = (obj) => { const s = ws.current; if (s && s.readyState === WebSocket.OPEN) s.send(JSON.stringify(obj)); };

  const onMessage = (msg) => {
    switch (msg.type) {
      case 'ready': if (msg.rate) play.current.rate = msg.rate; break;
      case 'heard': if (msg.text) { setLines((l) => [...l, { who: 'caller', text: msg.text }].slice(-6)); if (st.current === 'listening') setStatus('thinking'); } break;
      case 'reply': if (msg.text) { setLines((l) => [...l, { who: 'agent', text: msg.text }].slice(-6)); if (st.current === 'thinking') setStatus('listening'); } break;
      case 'revise': if (msg.text) setLines((l) => { const c = l.slice(); for (let i = c.length - 1; i >= 0; i--) if (c[i].who === 'agent') { c[i] = { who: 'agent', text: msg.text }; break; } return c; }); break;
      case 'media': if (msg.m) playAudio(msg.m); break;
      case 'clear': stopPlayback(); break;
      case 'beat': setTimeout(() => send({ type: 'ack', id: msg.id }), msg.wait || 0); break;
      case 'action': setEvents((ev) => [...ev, { system: msg.system, label: msg.label, detail: msg.detail }].slice(-6)); break;
      case 'limit': setLimit({ title: msg.title, cta: msg.cta, message: msg.message }); end('limited'); break;
      case 'session_timeout': setNote(msg.message || 'Time limit for this demo call reached.'); end('ended'); break;
      default: break;
    }
  };

  const fail = (msg, finalStatus) => { setNote(msg); end(finalStatus || 'ended'); };

  const start = async () => {
    if (live.current) return;
    live.current = true; setLines([]); setEvents([]); setSecs(0); setNote(''); setLimit(null);
    setStatus('connecting');
    ensurePlayCtx(); // inside the click gesture: unlocks audio output
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.WebSocket) { fail('Voice calls aren’t supported in this browser. Try Chrome, Edge or Safari.'); return; }
    try { await openMic(); } catch (e) { fail('We couldn’t access your microphone. Allow microphone access and try again.'); return; }
    if (!live.current) return;

    let session;
    try {
      const token = window.__turnstileToken ? await window.__turnstileToken('demo') : null;
      const res = await fetch(API + '/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ persona: pid, fp: fingerprint(), turnstileToken: token }) });
      session = await res.json();
    } catch (e) { fail('We couldn’t reach the demo service. Please try again in a moment.'); return; }
    if (!live.current) return;
    if (!session.ok) {
      if (session.limited) { setLimit({ title: session.title, cta: session.cta, message: session.message }); end('limited'); }
      else fail(session.error || 'The demo isn’t available right now.');
      return;
    }

    // If the call never opens, hand the reservation back so it does not count against today's calls.
    const cancel = () => { try { fetch(API + '/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticket: session.ticket }), keepalive: true }); } catch (e) {} };
    let opened = false, sock;
    try { sock = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + API + '/rt/' + encodeURIComponent(session.ticket)); }
    catch (e) { cancel(); fail('We couldn’t connect the call. Please try again.'); return; }
    ws.current = sock;
    sock.onopen = () => { if (ws.current !== sock) return; opened = true; send({ type: 'hello' }); setStatus('listening'); };
    sock.onmessage = (ev) => { if (ws.current !== sock) return; try { onMessage(JSON.parse(ev.data)); } catch (e) {} };
    sock.onerror = () => { if (ws.current !== sock) return; if (!opened) { cancel(); fail('We couldn’t connect the call. Please try again.'); } };
    sock.onclose = () => { if (ws.current !== sock) return; ws.current = null; if (!opened) cancel(); if (live.current) end(st.current === 'limited' ? 'limited' : 'ended'); };
  };

  const end = (finalStatus) => {
    live.current = false;
    const sock = ws.current; ws.current = null;
    if (sock) { try { sock.close(1000, ''); } catch (e) {} }
    closeMic(); stopPlayback();
    setStatus(finalStatus || 'ended');
  };
  useE(() => () => end('idle'), []);

  const active = live.current && status !== 'ended' && status !== 'limited';
  const dot = { idle: 'var(--p400)', connecting: 'var(--amber-500)', listening: 'var(--blue-300)', thinking: 'var(--amber-500)', speaking: 'var(--green-500, #3fb27f)', ended: 'var(--p400)', limited: 'var(--amber-500)' }[status];

  return (
    <div className="console" id="demo-console">
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', padding: '16px 20px', borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }} role="group" aria-label="Choose a demo agent">
          {PERSONAS.map((x) => <button key={x.id} type="button" className="ptab" aria-pressed={pid === x.id} disabled={active} onClick={() => { setPid(x.id); setStatus('idle'); setLines([]); setEvents([]); setSecs(0); setNote(''); setLimit(null); }}>{x.tab}</button>)}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--p200)' }}>
          <span className={active ? 'pulse' : ''} style={{ width: 8, height: 8, borderRadius: '50%', background: dot, color: dot, display: 'inline-block' }} />
          <span>{STATUS[status]}</span><span className="tick" style={{ color: 'var(--p300)' }}>{fmtT(secs)}</span>
        </div>
      </div>

      <div style={{ padding: '26px 22px 20px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 22, width: '100%', justifyContent: 'center' }}>
          <div className={'orb' + (active ? ' live' : '') + (status === 'thinking' ? ' thinking' : '')} aria-hidden="true" />
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 18, color: '#fff' }}>{p.agent} <span style={{ color: 'var(--p300)', fontWeight: 500 }}>· {p.org}</span></div>
            <div className="kick" style={{ marginTop: 6 }}>{p.line} · AI voice agent</div>
          </div>
        </div>
        <div ref={bars} className={'viz' + (status === 'speaking' ? ' agent' : '')} aria-hidden="true" style={{ width: '100%', maxWidth: 360 }}>{Array.from({ length: 28 }).map((_, i) => <i key={i} />)}</div>

        {limit ? (
          <div style={{ width: '100%', maxWidth: 420, textAlign: 'center', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', borderRadius: 16, padding: '20px 22px' }}>
            <div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 18, color: '#fff' }}>{limit.title || 'Want to see more?'}</div>
            <p style={{ fontSize: 13.5, color: 'var(--p200)', marginTop: 8, lineHeight: 1.5 }}>{limit.message}</p>
            <button type="button" className="callbtn start" style={{ marginTop: 14 }} onClick={() => { if (window.openEchoDemo) window.openEchoDemo(); }}><DI.Calendar size={18} />{limit.cta || 'Book a demo'}</button>
          </div>
        ) : active
          ? <button type="button" className="callbtn end" onClick={() => end('ended')}><DI.PhoneOff size={18} />End call</button>
          : <button type="button" className="callbtn start" onClick={start}><DI.Phone size={18} />{status === 'ended' ? 'Call again' : 'Start a call'}</button>}
        {!active && status === 'idle' && <p style={{ fontSize: 13, color: 'var(--p300)', textAlign: 'center' }}>Allow your microphone and speak naturally. {p.hint}</p>}

        {lines.length > 0 && (
          <div aria-live="polite" style={{ width: '100%', maxWidth: 520, display: 'flex', flexDirection: 'column', gap: 6 }}>
            {lines.map((l, i) => <div key={i} style={{ fontSize: 13.5, lineHeight: 1.45, color: l.who === 'agent' ? '#fff' : 'var(--p200)', opacity: i === lines.length - 1 ? 1 : 0.7 }}><span className="kick" style={{ marginRight: 8 }}>{l.who === 'agent' ? p.agent : 'You'}</span>{l.text}</div>)}
          </div>
        )}
        {events.length > 0 && (
          <div style={{ width: '100%', maxWidth: 520, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
            {events.map((e, i) => <span key={i} title={e.detail} style={{ fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--p100)', background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', borderRadius: 999, padding: '4px 10px' }}>{e.system}: {e.label}</span>)}
          </div>
        )}
      </div>

      {note && <p style={{ padding: '0 20px 18px', textAlign: 'center', fontSize: 12.5, color: 'var(--amber-500)' }}>{note}</p>}
    </div>
  );
}

Object.assign(window, { EchoDemo });
