
/* ===== LIVE CHAT WIDGET ===================================================
 *
 * Visitor-facing chat tied to a server-persisted conversation.
 *
 * Flow:
 *   1. Visitor opens the bubble → pre-chat form (name + email) unless
 *      localStorage already has { conversationId, token } from a prior visit.
 *   2. POST /api/chat/start returns { conversationId, token } — stored.
 *   3. GET  /api/chat/messages bootstraps the full thread.
 *   4. Supabase Realtime subscription on chat_messages (filtered by
 *      conversation_id) streams admin replies in with zero latency.
 *      If Supabase creds aren't configured, fall back to 4-second polling.
 *   5. POST /api/chat/message appends visitor messages. The token is
 *      HMAC-signed with ADMIN_SECRET on the server and verified on each call.
 *   6. Attachments: POST /api/chat/upload (token-gated raw body, 5 MB cap)
 *      returns { attachmentId }. The follow-up chat message carries the id
 *      and the server denormalises filename/MIME/size on the message row.
 *   7. Notification sound: a short Web Audio beep fires whenever a new
 *      non-visitor message arrives AFTER the initial bootstrap. Muted state
 *      is persisted in localStorage.
 *
 * The Supabase JS SDK is lazy-loaded the first time the widget opens (UMD
 * build from unpkg, exposes window.supabase). The anon key in ZIVERSE_PUBLIC
 * is safe to ship to the browser — RLS only permits SELECT on chat_messages.
 * ========================================================================*/

const LS_KEY = 'ziverse.chat.v1';
const LS_MUTE = 'ziverse.chat.mute.v1';
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024; // keep in sync with the API

function loadChatSession() {
  try {
    const raw = localStorage.getItem(LS_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || !parsed.conversationId || !parsed.token) return null;
    return parsed;
  } catch (_) { return null; }
}
function saveChatSession(sess) {
  try { localStorage.setItem(LS_KEY, JSON.stringify(sess)); } catch (_) {}
}
function clearChatSession() {
  try { localStorage.removeItem(LS_KEY); } catch (_) {}
}
function loadMuted() {
  try { return localStorage.getItem(LS_MUTE) === '1'; } catch (_) { return false; }
}
function saveMuted(v) {
  try { localStorage.setItem(LS_MUTE, v ? '1' : '0'); } catch (_) {}
}

// ── Notification sound ─────────────────────────────────────────────────────
// Two-tone beep synthesised on the fly via Web Audio API — no asset hosting,
// no preload, tiny payload. Autoplay policies are respected because the first
// beep only ever fires after the visitor has interacted with the widget.
let audioCtx = null;
function playBeep() {
  try {
    if (!audioCtx) {
      const Ctx = window.AudioContext || window.webkitAudioContext;
      if (!Ctx) return;
      audioCtx = new Ctx();
    }
    // Resume if suspended (some browsers start contexts suspended).
    if (audioCtx.state === 'suspended' && audioCtx.resume) audioCtx.resume();
    const now = audioCtx.currentTime;
    // Two short notes: G5 → C6. Soft envelope so it doesn't feel harsh.
    [
      { f: 784, t: now,        d: 0.11 },
      { f: 1046.5, t: now+0.12, d: 0.14 },
    ].forEach(({ f, t, d }) => {
      const osc = audioCtx.createOscillator();
      const g = audioCtx.createGain();
      osc.type = 'sine';
      osc.frequency.value = f;
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.14, t + 0.015);
      g.gain.exponentialRampToValueAtTime(0.0001, t + d);
      osc.connect(g).connect(audioCtx.destination);
      osc.start(t);
      osc.stop(t + d + 0.02);
    });
  } catch (_) { /* audio not available — silent fallback */ }
}

// Lazy-load @supabase/supabase-js UMD from unpkg. Resolves to the supabase
// namespace (or null if loading fails / credentials missing).
let supabaseLoader = null;
function loadSupabase() {
  if (supabaseLoader) return supabaseLoader;
  const pub = window.ZIVERSE_PUBLIC || {};
  if (!pub.supabaseUrl || !pub.supabaseAnonKey) {
    supabaseLoader = Promise.resolve(null);
    return supabaseLoader;
  }
  if (window.supabase && window.supabase.createClient) {
    supabaseLoader = Promise.resolve(window.supabase);
    return supabaseLoader;
  }
  supabaseLoader = new Promise((resolve) => {
    const s = document.createElement('script');
    s.src = 'https://unpkg.com/@supabase/supabase-js@2/dist/umd/supabase.js';
    s.crossOrigin = 'anonymous';
    s.onload = () => resolve(window.supabase || null);
    s.onerror = () => { supabaseLoader = null; resolve(null); };
    document.head.appendChild(s);
  });
  return supabaseLoader;
}

// ── Human-readable byte size
function fmtSize(bytes) {
  if (bytes == null) return '';
  if (bytes < 1024) return bytes + ' B';
  if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
  return (bytes/1024/1024).toFixed(2) + ' MB';
}

// Renders an attachment inside a message bubble. Images preview inline;
// everything else shows as a download chip.
const Attachment = ({ message, side }) => {
  if (!message.attachmentId) return null;
  const mime = message.attachmentMime || '';
  const name = message.attachmentName || 'attachment';
  const size = message.attachmentSize;
  const url  = '/api/chat/attachment/' + message.attachmentId;
  const isImg = /^image\//.test(mime);
  if (isImg) {
    return (
      <a href={url} target="_blank" rel="noopener noreferrer" style={{display:'block',marginTop:message.body?6:0}}>
        <img src={url} alt={name} style={{
          maxWidth:'100%', maxHeight:220, borderRadius:8,
          display:'block', background:'rgba(0,0,0,0.04)',
        }}/>
      </a>
    );
  }
  return (
    <a href={url + '?download=1'} download={name} style={{
      display:'flex',alignItems:'center',gap:8,
      padding:'8px 10px',marginTop:message.body?6:0,
      background: side==='right' ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.06)',
      borderRadius:8, textDecoration:'none',
      color: side==='right' ? '#fff' : 'var(--text,#0f172a)',
      fontSize:12, fontFamily:"'DM Sans',sans-serif",
    }}>
      <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
        <path d="M9 2H4C3.4 2 3 2.4 3 3V13C3 13.6 3.4 14 4 14H12C12.6 14 13 13.6 13 13V6L9 2Z"
              stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinejoin="round"/>
        <path d="M9 2V6H13" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinejoin="round"/>
      </svg>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontWeight:600,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{name}</div>
        {size != null && <div style={{opacity:0.7,fontSize:11}}>{fmtSize(size)}</div>}
      </div>
    </a>
  );
};

const LiveChat = () => {
  const site = (window.zData && window.zData('site')) || {};
  const brandName = (site.companyName || 'Ziverse Technologies')
    .replace(/\s+Technologies\s*$/i, '') || 'Ziverse';

  const [open, setOpen] = React.useState(false);
  const [session, setSession] = React.useState(() => loadChatSession());
  const [messages, setMessages] = React.useState([]); // server-authored messages
  const [input, setInput] = React.useState('');
  const [form, setForm] = React.useState({ name: '', email: '' });
  const [status, setStatus] = React.useState('idle'); // idle | starting | sending | error
  const [errorMsg, setErrorMsg] = React.useState('');
  const [unread, setUnread] = React.useState(0);
  const [connected, setConnected] = React.useState(false); // realtime status
  const [muted, setMuted] = React.useState(() => loadMuted());
  const [uploading, setUploading] = React.useState(null); // {name, size} while upload in-flight
  const endRef = React.useRef(null);
  const fileInputRef = React.useRef(null);
  const seenIdsRef = React.useRef(new Set()); // synchronous dedup gate
  const hydratedRef = React.useRef(false); // no beep during initial load

  // ── Single source of truth for appending a message. Every row that ever
  // appears came back from the server (POST response, realtime INSERT, or
  // a history refetch). The chat_messages.id column is a BIGSERIAL —
  // node-pg serializes that as a STRING, but Supabase Realtime publishes
  // it as a NUMBER. We coerce to string here so the two paths can never
  // produce two entries for the same underlying row.
  const appendMsg = React.useCallback((m) => {
    if (!m || m.id == null) return;
    const id = String(m.id);
    if (seenIdsRef.current.has(id)) return;
    seenIdsRef.current.add(id);
    const normalized = typeof m.id === 'string' ? m : { ...m, id };
    setMessages(prev => {
      // Reducer-level safety net: compare stringified ids in case something
      // upstream slipped a non-normalized row into the list.
      if (prev.some(x => String(x.id) === id)) return prev;
      return [...prev, normalized];
    });
    if (m.from && m.from !== 'visitor') {
      if (!open) setUnread(u => u + 1);
      if (hydratedRef.current && !loadMuted()) playBeep();
    }
  }, [open]);

  // ── Scroll to bottom on new messages / open
  React.useEffect(() => {
    if (endRef.current && open) endRef.current.scrollIntoView({ block: 'end', behavior: 'smooth' });
  }, [messages.length, open]);

  // Keep muted pref in sync with storage
  React.useEffect(() => { saveMuted(muted); }, [muted]);

  // ── Bootstrap history when session exists / widget opens
  const bootstrap = React.useCallback(async (sess) => {
    if (!sess) return;
    try {
      const res = await window.zChat.history(sess.conversationId, sess.token);
      const list = ((res && res.messages) || []).map(m =>
        (typeof m.id === 'string' ? m : { ...m, id: String(m.id) })
      );
      seenIdsRef.current = new Set(list.map(m => m.id));
      setMessages(list);
      // Mark hydrated after the paint so realtime/poll events can start
      // playing sounds.
      setTimeout(() => { hydratedRef.current = true; }, 200);
    } catch (e) {
      if (e && e.status === 401) {
        // Token expired or invalid — clear and force re-signup.
        clearChatSession();
        setSession(null);
      } else {
        setErrorMsg('Could not load history. Try again.');
      }
    }
  }, []);

  React.useEffect(() => {
    if (session && open) bootstrap(session);
  }, [session, open, bootstrap]);

  // ── Realtime subscription (Supabase) with polling fallback
  React.useEffect(() => {
    if (!session) return undefined;
    let channel = null;
    let pollTimer = null;
    let cancelled = false;

    (async () => {
      const sb = await loadSupabase();
      if (cancelled) return;
      if (sb) {
        try {
          const pub = window.ZIVERSE_PUBLIC || {};
          const client = sb.createClient(pub.supabaseUrl, pub.supabaseAnonKey, {
            realtime: { params: { eventsPerSecond: 5 } },
          });
          channel = client
            .channel('chat:' + session.conversationId)
            .on('postgres_changes', {
              event: 'INSERT',
              schema: 'public',
              table: 'chat_messages',
              filter: 'conversation_id=eq.' + session.conversationId,
            }, (payload) => {
              const r = payload && payload.new;
              if (!r) return;
              // Realtime row is raw DB shape (snake_case, no attachment join).
              // If the row carries an attachment id, refetch history to pick
              // up the denormalised filename/mime/size. Rare path; keeps code
              // simple.
              if (r.attachment_id) {
                window.zChat.history(session.conversationId, session.token)
                  .then(res => {
                    const list = (res && res.messages) || [];
                    list.forEach(appendMsg);
                  })
                  .catch(()=>{});
                return;
              }
              appendMsg({
                id: r.id,
                conversationId: r.conversation_id,
                from: r.from_role,
                body: r.body,
                attachmentId: r.attachment_id || null,
                readByAdmin: r.read_by_admin,
                createdAt: r.created_at,
              });
            })
            .subscribe((statusStr) => {
              if (cancelled) return;
              if (statusStr === 'SUBSCRIBED') setConnected(true);
              else if (statusStr === 'CLOSED' || statusStr === 'CHANNEL_ERROR') setConnected(false);
            });
        } catch (_) {
          // Fall through to polling.
        }
      }
      if (!channel) {
        // Fallback: poll for new messages every 4s while widget is active.
        // Once the first poll round-trips successfully, flip `connected` to
        // true so the header badge doesn't get stuck on "Connecting…" just
        // because Supabase Realtime is unavailable.
        const poll = async () => {
          if (cancelled) return;
          try {
            const res = await window.zChat.history(session.conversationId, session.token);
            const list = (res && res.messages) || [];
            list.forEach(appendMsg);
            if (!cancelled) setConnected(true);
          } catch (_) {}
          pollTimer = setTimeout(poll, 4000);
        };
        // Run the first poll immediately so the UI confirms "Live" quickly.
        poll();
      }
    })();

    return () => {
      cancelled = true;
      if (channel) try { channel.unsubscribe(); } catch (_) {}
      if (pollTimer) clearTimeout(pollTimer);
      setConnected(false);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [session && session.conversationId, appendMsg]);

  // ── Clear unread when opened
  React.useEffect(() => {
    if (open) setUnread(0);
  }, [open]);

  // ── Start a new conversation
  const startChat = async (e) => {
    e && e.preventDefault && e.preventDefault();
    const name = form.name.trim();
    const email = form.email.trim();
    if (!name || !email) return;
    setStatus('starting'); setErrorMsg('');
    try {
      const r = await window.zChat.start(name, email);
      const sess = {
        conversationId: r.conversationId,
        token: r.token,
        name: r.visitorName,
        email: r.visitorEmail,
      };
      saveChatSession(sess);
      setSession(sess);
      setStatus('idle');
    } catch (err) {
      setStatus('error');
      setErrorMsg((err && err.message) || 'Could not start chat.');
    }
  };

  // ── Send a message. No optimistic rendering: we POST and feed the server
  // response to appendMsg. If the realtime INSERT has already arrived,
  // appendMsg's id-based dedup silently drops the duplicate — either way,
  // exactly one row appears on screen.
  const sendMessage = async ({ body, attachmentId }) => {
    if (!session) return;
    setStatus('sending'); setErrorMsg('');
    try {
      const r = await window.zChat.send(session.conversationId, session.token, body || '', attachmentId || null);
      if (r && r.message) appendMsg(r.message);
      setStatus('idle');
    } catch (err) {
      setStatus('error');
      setErrorMsg((err && err.message) || 'Send failed.');
    }
  };

  const sendText = async () => {
    const body = input.trim();
    if (!body || status === 'sending') return;
    setInput('');
    await sendMessage({ body });
  };

  // ── File upload + send
  const onPickFile = () => { fileInputRef.current && fileInputRef.current.click(); };
  const onFileChosen = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = ''; // allow re-picking the same file
    if (!file || !session) return;
    if (file.size > MAX_UPLOAD_BYTES) {
      setErrorMsg('File too large (max 5 MB).');
      return;
    }
    setUploading({ name: file.name, size: file.size });
    setErrorMsg('');
    try {
      const up = await window.zChat.upload(session.conversationId, session.token, file);
      setUploading(null);
      await sendMessage({ body: input.trim(), attachmentId: up.attachmentId });
      setInput('');
    } catch (err) {
      setUploading(null);
      setErrorMsg((err && err.message) || 'Upload failed.');
    }
  };

  const endChat = () => {
    if (!confirm('End this chat? Your history on this device will be cleared.')) return;
    clearChatSession();
    setSession(null);
    setMessages([]);
    setForm({ name: '', email: '' });
    seenIdsRef.current = new Set();
    hydratedRef.current = false;
  };

  // ── Styles
  const bubbleBtn = {
    width: 56, height: 56, borderRadius: '50%',
    background: 'linear-gradient(135deg, #1d4ed8, #2563eb)',
    border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
    boxShadow: '0 6px 20px rgba(29,78,216,0.4)', transition: 'all 0.2s',
    position: 'relative',
  };
  const inputStyle = {
    width: '100%', padding: '10px 12px', borderRadius: 8,
    border: '1.5px solid var(--border, rgba(0,0,0,0.1))',
    background: 'var(--bg2, #f8fafc)', fontFamily: "'DM Sans', sans-serif",
    fontSize: 13, color: 'var(--text, #0f172a)', outline: 'none',
    boxSizing: 'border-box',
  };

  return (
    <div style={{ position: 'fixed', bottom: 24, right: 24, zIndex: 9000 }}>
      {open && (
        <div style={{
          position: 'absolute', bottom: 72, right: 0, width: 360, maxWidth: 'calc(100vw - 48px)',
          borderRadius: 16, background: 'var(--bg3, #fff)',
          border: '1.5px solid var(--border, rgba(0,0,0,0.08))',
          boxShadow: '0 20px 60px rgba(0,0,0,0.14)', overflow: 'hidden',
          display: 'flex', flexDirection: 'column', maxHeight: '70vh',
        }}>
          {/* Header */}
          <div style={{ padding: '14px 16px', background: 'linear-gradient(135deg, #1d4ed8, #2563eb)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <div style={{ width: 34, height: 34, borderRadius: '50%', background: 'rgba(255,255,255,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L14 5.5V10.5L8 14L2 10.5V5.5L8 2Z" stroke="white" strokeWidth="1.4" fill="none"/><circle cx="8" cy="8" r="2" fill="white"/></svg>
              </div>
              <div>
                <div style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: 13, fontWeight: 700, color: '#fff' }}>{brandName} Support</div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
                  <div style={{ width: 6, height: 6, borderRadius: '50%', background: connected ? '#4ade80' : (session ? '#fbbf24' : '#94a3b8') }}/>
                  <span style={{ fontFamily: "'DM Sans', sans-serif", fontSize: 11, color: 'rgba(255,255,255,0.82)' }}>
                    {connected ? 'Live' : session ? 'Connecting…' : 'Online'}
                  </span>
                </div>
              </div>
            </div>
            <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
              <button onClick={() => setMuted(m => !m)} title={muted ? 'Unmute notifications' : 'Mute notifications'} style={{ background: 'rgba(255,255,255,0.12)', border: 'none', cursor: 'pointer', color: '#fff', borderRadius: 6, padding: '5px 7px', display:'flex', alignItems:'center' }}>
                {muted
                  ? <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 6H2V10H4L7 13V3L4 6Z" fill="currentColor"/><path d="M11 6L14 9M14 6L11 9" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/></svg>
                  : <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 6H2V10H4L7 13V3L4 6Z" fill="currentColor"/><path d="M11 6C12 7 12 9 11 10" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" fill="none"/></svg>
                }
              </button>
              {session && (
                <button onClick={endChat} title="End chat" style={{ background: 'rgba(255,255,255,0.12)', border: 'none', cursor: 'pointer', color: '#fff', borderRadius: 6, padding: '4px 8px', fontSize: 11, fontFamily: "'DM Sans',sans-serif" }}>End</button>
              )}
              <button onClick={() => setOpen(false)} title="Minimize" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'rgba(255,255,255,0.88)', padding: 4 }}>
                <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 3L13 13M13 3L3 13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
              </button>
            </div>
          </div>

          {/* Body */}
          {!session ? (
            /* Pre-chat form */
            <form onSubmit={startChat} style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: 15, fontWeight: 600, color: 'var(--text, #0f172a)', marginBottom: 2 }}>Start a conversation</div>
              <div style={{ fontFamily: "'DM Sans', sans-serif", fontSize: 13, color: 'var(--muted, #64748b)', lineHeight: 1.5, marginBottom: 6 }}>We'll reply here and also send updates to your email.</div>
              <input required type="text" placeholder="Your name" value={form.name}
                onChange={e => setForm(p => ({ ...p, name: e.target.value }))}
                style={inputStyle}/>
              <input required type="email" placeholder="Email address" value={form.email}
                onChange={e => setForm(p => ({ ...p, email: e.target.value }))}
                style={inputStyle}/>
              {errorMsg && <div style={{ fontFamily: "'DM Sans', sans-serif", fontSize: 12, color: '#b91c1c' }}>{errorMsg}</div>}
              <button type="submit" disabled={status === 'starting'} style={{
                padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
                background: 'linear-gradient(135deg, #1d4ed8, #2563eb)', color: '#fff',
                fontFamily: "'DM Sans', sans-serif", fontSize: 13, fontWeight: 600,
                opacity: status === 'starting' ? 0.7 : 1, marginTop: 2,
              }}>
                {status === 'starting' ? 'Starting…' : 'Start chat →'}
              </button>
              <div style={{ fontFamily: "'DM Sans', sans-serif", fontSize: 11, color: 'var(--muted, #94a3b8)', textAlign: 'center', marginTop: 4 }}>
                By starting, you agree to be contacted about your inquiry.
              </div>
            </form>
          ) : (
            <>
              {/* Messages */}
              <div style={{ padding: '14px', flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 8, background: 'var(--bg2, #f8fafc)' }}>
                {messages.length === 0 && (
                  <div style={{ textAlign: 'center', color: 'var(--muted, #94a3b8)', fontSize: 12, padding: '40px 8px', fontFamily: "'DM Sans', sans-serif" }}>
                    Loading messages…
                  </div>
                )}
                {messages.map((m) => {
                  const side = m.from === 'visitor' ? 'right' : 'left';
                  const isSystem = m.from === 'system';
                  return (
                    <div key={m.id} style={{ display: 'flex', justifyContent: side === 'right' ? 'flex-end' : 'flex-start' }}>
                      <div style={{
                        maxWidth: '84%',
                        padding: '9px 13px',
                        borderRadius: side === 'right' ? '14px 14px 3px 14px' : '14px 14px 14px 3px',
                        background: side === 'right'
                          ? 'linear-gradient(135deg, #1d4ed8, #2563eb)'
                          : (isSystem ? 'rgba(37,99,235,0.08)' : 'var(--bg3, #fff)'),
                        color: side === 'right' ? '#fff' : 'var(--text, #0f172a)',
                        border: side === 'left' ? '1px solid var(--border, rgba(0,0,0,0.06))' : 'none',
                        fontFamily: "'DM Sans', sans-serif", fontSize: 13, lineHeight: 1.55,
                        whiteSpace: 'pre-wrap', wordBreak: 'break-word',
                      }}>
                        {m.body}
                        <Attachment message={m} side={side}/>
                      </div>
                    </div>
                  );
                })}
                <div ref={endRef}/>
              </div>
              {uploading && (
                <div style={{ padding: '8px 14px', fontSize: 12, color: 'var(--muted, #64748b)', fontFamily: "'DM Sans',sans-serif", background: 'rgba(29,78,216,0.05)', borderTop: '1px solid rgba(29,78,216,0.15)', display:'flex',alignItems:'center',gap:8 }}>
                  <svg width="14" height="14" viewBox="0 0 14 14" style={{animation:'spin 0.9s linear infinite'}}><circle cx="7" cy="7" r="5.5" stroke="#1d4ed8" strokeWidth="1.6" strokeLinecap="round" strokeDasharray="8 24" fill="none"/></svg>
                  <span>Uploading <strong>{uploading.name}</strong> ({fmtSize(uploading.size)})…</span>
                </div>
              )}
              {errorMsg && (
                <div style={{ padding: '6px 14px', fontSize: 11, color: '#b91c1c', fontFamily: "'DM Sans',sans-serif", background: 'rgba(220,38,38,0.06)', borderTop: '1px solid rgba(220,38,38,0.2)' }}>
                  {errorMsg}
                </div>
              )}
              {/* Input */}
              <div style={{ padding: '10px 12px', borderTop: '1px solid var(--border, rgba(0,0,0,0.07))', display: 'flex', gap: 6, flexShrink: 0, background: 'var(--bg3, #fff)', alignItems:'center' }}>
                <input ref={fileInputRef} type="file" onChange={onFileChosen} style={{display:'none'}}
                  accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,application/pdf,text/plain,text/csv,.doc,.docx,.xls,.xlsx,.ppt,.pptx,application/zip"/>
                <button onClick={onPickFile} disabled={!!uploading || status==='sending'} title="Attach a file"
                  style={{width:36,height:36,borderRadius:8,background:'var(--bg2,#f1f5f9)',border:'1px solid var(--border,rgba(0,0,0,0.08))',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',color:'#475569',flexShrink:0}}>
                  <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M11.5 7L7.5 11C6.7 11.8 5.3 11.8 4.5 11C3.7 10.2 3.7 8.8 4.5 8L9 3.5C9.6 3 10.4 3 11 3.5C11.6 4.1 11.6 5 11 5.5L6.8 9.7C6.5 10 6 10 5.7 9.7C5.4 9.4 5.4 8.9 5.7 8.6L9 5.3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" fill="none"/></svg>
                </button>
                <input value={input} onChange={e => setInput(e.target.value)}
                  onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendText(); } }}
                  placeholder="Type a message…" disabled={status === 'sending' || !!uploading}
                  style={{ ...inputStyle, padding: '10px 12px' }}/>
                <button onClick={sendText} disabled={!input.trim() || status === 'sending' || !!uploading} style={{
                  width: 40, height: 40, borderRadius: 8,
                  background: input.trim() ? 'linear-gradient(135deg, #1d4ed8, #2563eb)' : '#cbd5e1',
                  border: 'none', cursor: input.trim() ? 'pointer' : 'default',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  flexShrink: 0, transition: 'background 0.15s',
                }}>
                  <svg width="15" height="15" viewBox="0 0 14 14" fill="none"><path d="M12 7L2 2L4 7L2 12L12 7Z" fill="white"/></svg>
                </button>
              </div>
            </>
          )}
        </div>
      )}

      {/* Toggle bubble */}
      <button onClick={() => setOpen(!open)} style={bubbleBtn}
        onMouseEnter={e => { e.currentTarget.style.transform = 'scale(1.08)'; e.currentTarget.style.boxShadow = '0 8px 28px rgba(29,78,216,0.55)'; }}
        onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = '0 6px 20px rgba(29,78,216,0.4)'; }}>
        {open
          ? <svg width="20" height="20" viewBox="0 0 18 18" fill="none"><path d="M4 4L14 14M14 4L4 14" stroke="white" strokeWidth="2" strokeLinecap="round"/></svg>
          : <svg width="22" height="22" viewBox="0 0 20 20" fill="none"><path d="M2 4C2 2.9 2.9 2 4 2H16C17.1 2 18 2.9 18 4V13C18 14.1 17.1 15 16 15H6L2 18V4Z" stroke="white" strokeWidth="1.7" fill="none"/><path d="M6 7H14M6 10H11" stroke="white" strokeWidth="1.5" strokeLinecap="round"/></svg>
        }
        {!open && unread > 0 && (
          <span style={{
            position: 'absolute', top: -4, right: -4,
            minWidth: 20, height: 20, padding: '0 6px',
            borderRadius: 10, background: '#ef4444', color: '#fff',
            fontFamily: "'DM Sans', sans-serif", fontSize: 11, fontWeight: 700,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: '0 2px 6px rgba(239,68,68,0.5)',
          }}>{unread > 9 ? '9+' : unread}</span>
        )}
      </button>

      <style>{'@keyframes spin { 0%{transform:rotate(0)} 100%{transform:rotate(360deg)} }'}</style>
    </div>
  );
};

/* ===== NEWSLETTER ===== */
const Newsletter = () => {
  const [email, setEmail] = React.useState('');
  const [status, setStatus] = React.useState('idle');
  const [err, setErr] = React.useState('');
  const submit = async (e) => {
    e.preventDefault();
    if (!email) return;
    setStatus('sending');
    setErr('');
    try {
      await window.zSubscribe(email);
      setStatus('done');
    } catch (e2) {
      setStatus('idle');
      setErr(e2 && e2.message ? e2.message : 'Subscription failed.');
    }
  };
  return (
    <div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: 12, padding: '24px', border: '1px solid rgba(255,255,255,0.1)', marginBottom: 36 }}>
      <div style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: 15, fontWeight: 600, color: '#f1f5f9', marginBottom: 6 }}>Stay in the loop</div>
      <p style={{ fontFamily: "'DM Sans', sans-serif", fontSize: 13, color: '#64748b', margin: '0 0 14px', lineHeight: 1.6 }}>Engineering insights and case studies, twice a month.</p>
      {status === 'done' ? (
        <div style={{ fontFamily: "'DM Sans', sans-serif", fontSize: 13, color: '#4ade80', fontWeight: 500 }}>✓ You're subscribed!</div>
      ) : (
        <form onSubmit={submit} style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <input type="email" required placeholder="your@email.com" value={email} onChange={e => setEmail(e.target.value)}
            style={{ flex: 1, padding: '9px 12px', borderRadius: 7, border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.06)', color: '#f1f5f9', fontFamily: "'DM Sans', sans-serif", fontSize: 13, outline: 'none' }}
            onFocus={e => e.target.style.borderColor = '#3b82f6'}
            onBlur={e => e.target.style.borderColor = 'rgba(255,255,255,0.12)'}
          />
          <button type="submit" disabled={status === 'sending'} style={{ padding: '9px 16px', borderRadius: 7, background: '#1d4ed8', border: 'none', cursor: 'pointer', fontFamily: "'DM Sans', sans-serif", fontSize: 13, fontWeight: 600, color: '#fff', whiteSpace: 'nowrap', transition: 'background 0.2s' }}>
            {status === 'sending' ? '...' : 'Subscribe'}
          </button>
          {err && <div style={{ width: '100%', fontFamily: "'DM Sans', sans-serif", fontSize: 12, color: '#f87171', marginTop: 4 }}>{err}</div>}
        </form>
      )}
    </div>
  );
};

Object.assign(window, { LiveChat, Newsletter });
