import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { ArrowLeft, EllipsisVertical, WandSparkles, Download, X, Share, Bell, BellRing, BellOff, MessageSquarePlus } from 'lucide-react';
import { WsClient } from './src/lib/ws-client';
import { useBlobyChat } from './src/hooks/useBlobyChat';
import OnboardWizard from './OnboardWizard';
import LoginScreen from './src/components/LoginScreen';
import { getAuthToken, setAuthToken, clearAuthToken, authFetch, onAuthFailure } from './src/lib/auth';
import MessageList from './src/components/Chat/MessageList';
import InputBar from './src/components/Chat/InputBar';
import HeadphonesAnimation from './src/components/Chat/HeadphonesAnimation';
import './src/styles/globals.css';

// Injected at build time (vite.bloby.config.ts define) — the version this bundle shipped with.
declare const __BLOBY_VERSION__: string;

function BlobyApp() {
  const clientRef = useRef<WsClient | null>(null);
  const [connected, setConnected] = useState(false);
  // True between the supervisor's 'agent:updating' broadcast (self-update launching) and the
  // post-restart reconnect — the header shows "Updating…" instead of a mystery "Offline".
  const [updating, setUpdating] = useState(false);
  // Don't flash the offline input-block during the initial connect (~1s) on every open.
  const [connectGraceOver, setConnectGraceOver] = useState(false);
  const [botName, setBotName] = useState('Bloby');
  const [whisperEnabled, setWhisperEnabled] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const [confirmReset, setConfirmReset] = useState(false);
  const [showWizard, setShowWizard] = useState(false);
  const [reloadTrigger, setReloadTrigger] = useState(0);
  const menuRef = useRef<HTMLDivElement>(null);
  const wasConnected = useRef(false);

  // Backend health
  const [backendHealthy, setBackendHealthy] = useState(true);

  // Wallet balances (per network)
  const [walletTempo, setWalletTempo] = useState<string | null>(null);
  const [walletBase, setWalletBase] = useState<string | null>(null);
  const [walletOpen, setWalletOpen] = useState(false);

  // Recording state (for header animation)
  const [chatRecording, setChatRecording] = useState(false);

  // Push notifications
  const [pushState, setPushState] = useState<'loading' | 'unsupported' | 'denied' | 'subscribed' | 'unsubscribed'>('loading');

  function urlBase64ToUint8Array(base64String: string) {
    const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
    const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
    const raw = atob(base64);
    const arr = new Uint8Array(raw.length);
    for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
    return arr;
  }

  async function subscribePush() {
    try {
      const permission = await Notification.requestPermission();
      if (permission !== 'granted') { setPushState('denied'); return; }

      const reg = await navigator.serviceWorker.ready;
      const res = await fetch('/api/push/vapid-public-key');
      const { publicKey } = await res.json();

      const subscription = await reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(publicKey),
      });

      const sub = subscription.toJSON();
      const client = clientRef.current;
      if (client) {
        client.send('push:subscribe', { endpoint: sub.endpoint, keys: sub.keys });
      }

      setPushState('subscribed');
    } catch (err) {
      console.error('[push] Subscribe failed:', err);
    }
  }

  async function unsubscribePush() {
    try {
      const reg = await navigator.serviceWorker.ready;
      const subscription = await reg.pushManager.getSubscription();
      if (subscription) {
        const endpoint = subscription.endpoint;
        await subscription.unsubscribe();
        const client = clientRef.current;
        if (client) {
          client.send('push:unsubscribe', { endpoint });
        }
      }
      setPushState('unsubscribed');
    } catch (err) {
      console.error('[push] Unsubscribe failed:', err);
    }
  }

  // Install App (PWA)
  const [showIosModal, setShowIosModal] = useState(false);
  const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent);
  const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator as any).standalone;
  const isMobile = /Android|iPhone|iPad|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  const isInIframe = window.self !== window.top;

  // Listen for install result from parent (when in iframe)
  useEffect(() => {
    const handler = (e: MessageEvent) => {
      if (e.data?.type === 'bloby:show-ios-install') {
        setShowIosModal(true);
      }
    };
    window.addEventListener('message', handler);
    return () => window.removeEventListener('message', handler);
  }, []);

  // Auth state
  const [authChecked, setAuthChecked] = useState(false);
  const [authRequired, setAuthRequired] = useState(false);
  const [authenticated, setAuthenticated] = useState(false);
  const [totpEnabled, setTotpEnabled] = useState(false);

  // Check auth on mount
  useEffect(() => {
    (async () => {
      try {
        const res = await fetch('/api/onboard/status');
        const data = await res.json();
        if (data.totpEnabled) setTotpEnabled(true);

        if (!data.portalConfigured) {
          // No password set — skip auth entirely
          setAuthRequired(false);
          setAuthenticated(true);
          setAuthChecked(true);
          return;
        }

        setAuthRequired(true);

        // Check if we have a valid token in localStorage
        const token = getAuthToken();
        if (token) {
          const vRes = await fetch(`/api/portal/validate-token?token=${encodeURIComponent(token)}`);
          const vData = await vRes.json();
          if (vData.valid) {
            setAuthenticated(true);
            setAuthChecked(true);
            return;
          }
          clearAuthToken();
        }

        setAuthenticated(false);
        setAuthChecked(true);
      } catch {
        // Worker not ready — skip auth, let it retry later
        setAuthenticated(true);
        setAuthChecked(true);
      }
    })();
  }, []);

  const handleLogin = (token: string) => {
    setAuthToken(token);
    setAuthenticated(true);
  };

  // Handle mid-session token expiry (authFetch gets 401)
  useEffect(() => {
    onAuthFailure(() => {
      setAuthenticated(false);
      setAuthRequired(true);
    });
  }, []);

  // Backend health check
  useEffect(() => {
    if (!authenticated) return;
    const check = () => {
      fetch('/app/api/health')
        .then((r) => setBackendHealthy(r.ok))
        .catch(() => setBackendHealthy(false));
    };
    check();
    const id = setInterval(check, 10_000);
    return () => clearInterval(id);
  }, [authenticated]);

  // Check push state on mount
  useEffect(() => {
    if (!authenticated) return;
    (async () => {
      if (!('PushManager' in window) || !('serviceWorker' in navigator)) {
        setPushState('unsupported');
        return;
      }
      if (Notification.permission === 'denied') {
        setPushState('denied');
        return;
      }
      try {
        const reg = await navigator.serviceWorker.ready;
        const subscription = await reg.pushManager.getSubscription();
        if (subscription) {
          const res = await authFetch(`/api/push/status?endpoint=${encodeURIComponent(subscription.endpoint)}`);
          const data = await res.json();
          setPushState(data.subscribed ? 'subscribed' : 'unsubscribed');
        } else {
          setPushState('unsubscribed');
        }
      } catch {
        setPushState('unsupported');
      }
    })();
  }, [authenticated]);

  // Connect WebSocket only when authenticated
  useEffect(() => {
    if (!authenticated) return;

    const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    const host = location.host;
    const client = new WsClient(`${proto}//${host}/bloby/ws`, getAuthToken);
    clientRef.current = client;

    // Version handshake — this chat may be running inside the immortal shell, which (by
    // design) survives self-updates. After a reconnect, compare the server's version with the
    // one baked into this bundle; a mismatch means we're pre-update code → trigger ONE full
    // shell reload so widget + chat + workspace all pick up the new build together.
    const checkPlatformVersion = () => {
      fetch('/__bloby/version', { cache: 'no-store' })
        .then((r) => r.json())
        .then((v) => {
          if (!v?.version || v.version === 'unknown' || v.version === __BLOBY_VERSION__) return;
          try {
            // Throttle: one auto-reload per 5 min per tab — if something upstream keeps
            // serving a stale bundle, never loop.
            const last = Number(sessionStorage.getItem('bloby_version_reload') || 0);
            if (Date.now() - last < 5 * 60_000) return;
            sessionStorage.setItem('bloby_version_reload', String(Date.now()));
          } catch {}
          // Ask the shell (top window) to reload everything; fall back to reloading just
          // this chat iframe if the surrounding widget predates the handler.
          if (window.parent !== window) {
            window.parent.postMessage({ type: 'bloby:version-changed' }, location.origin);
            setTimeout(() => location.reload(), 1500);
          } else {
            location.reload();
          }
        })
        .catch(() => {});
    };

    const unsub = client.onStatus((isConnected) => {
      setConnected(isConnected);
      // A (re)connect means the supervisor is back — the update (if any) is over.
      if (isConnected) {
        setUpdating(false);
        checkPlatformVersion();
      }
      // On reconnect, trigger a reload from DB to catch missed messages
      if (isConnected && wasConnected.current) {
        setReloadTrigger((n) => n + 1);
      }
      wasConnected.current = isConnected;
    });

    // Self-update launching: the supervisor broadcasts this right before the npm install +
    // process restart, so the upcoming disconnect reads as "Updating…" instead of "Offline"
    // (and input is blocked — the agent can't receive messages mid-update).
    const unsubUpdating = client.on('agent:updating', () => setUpdating(true));

    // Forward rebuild/HMR events to parent (dashboard) via postMessage
    const unsubRebuilding = client.on('app:rebuilding', () => {
      window.parent?.postMessage({ type: 'bloby:rebuilding' }, '*');
    });
    const unsubRebuilt = client.on('app:rebuilt', () => {
      window.parent?.postMessage({ type: 'bloby:rebuilt' }, '*');
    });
    const unsubBuildError = client.on('app:build-error', (data: { error: string }) => {
      window.parent?.postMessage({ type: 'bloby:build-error', error: data.error }, '*');
    });
    const unsubHmr = client.on('app:hmr-update', () => {
      window.parent?.postMessage({ type: 'bloby:hmr-update' }, '*');
    });

    // Notify parent when agent finishes a response or a cross-device message arrives
    const unsubBotMsg = client.on('bot:response', () => {
      window.parent?.postMessage({ type: 'bloby:new-message' }, '*');
    });
    const unsubSyncMsg = client.on('chat:sync', () => {
      window.parent?.postMessage({ type: 'bloby:new-message' }, '*');
    });

    client.connect();

    return () => {
      unsub();
      unsubUpdating();
      unsubRebuilding();
      unsubRebuilt();
      unsubBuildError();
      unsubHmr();
      unsubBotMsg();
      unsubSyncMsg();
      client.disconnect();
    };
  }, [authenticated]);

  // Updating-state safety valve: a normal self-update reconnects within ~1-2 minutes. If the
  // update fails and the daemon never comes back, stop claiming "Updating…" — fall back to the
  // honest "Offline" so the user isn't told to wait forever.
  useEffect(() => {
    if (!updating) return;
    const t = setTimeout(() => setUpdating(false), 4 * 60_000);
    return () => clearTimeout(t);
  }, [updating]);

  // Grace window so the brief pre-connect moment on open doesn't flash the offline block.
  useEffect(() => {
    if (!authenticated) return;
    const t = setTimeout(() => setConnectGraceOver(true), 3000);
    return () => clearTimeout(t);
  }, [authenticated]);

  // Try to load settings (will work when worker is up, fail silently when down)
  useEffect(() => {
    if (!authenticated) return;
    authFetch('/api/settings')
      .then((r) => r.json())
      .then((s) => {
        if (s.agent_name) setBotName(s.agent_name);
        if (s.whisper_enabled === 'true') setWhisperEnabled(true);
      })
      .catch(() => {});

    // Fetch wallet balances (Tempo + Base)
    authFetch('/api/wallet/balance')
      .then((r) => r.json())
      .then((w) => {
        if (!w.address) return;
        setWalletTempo(w.tempo ?? '0.00');
        setWalletBase(w.base ?? '0.00');
      })
      .catch(() => {});
  }, [authenticated]);

  // Close menu on outside click
  useEffect(() => {
    if (!menuOpen) return;
    const handler = (e: MouseEvent) => {
      if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setMenuOpen(false); setConfirmReset(false); }
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [menuOpen]);

  const { messages, streaming, streamBuffer, tools, hasMore, loadOlder, sendMessage, addPendingAudio, stopStreaming, clearContext } =
    useBlobyChat(clientRef.current, reloadTrigger, authenticated);

  // Block input while the agent can't actually receive: mid-update, or disconnected (after the
  // initial connect grace so opening the chat doesn't flash the block for a moment).
  const chatBlocked = updating || (!connected && connectGraceOver);

  // Handle voice recordings sent from the widget bubble (long-press mic)
  useEffect(() => {
    const handler = async (e: MessageEvent) => {
      if (e.data?.type !== 'bloby:voice-record') return;
      if (chatBlocked) return; // bubble long-press mic bypasses the InputBar — block it too
      const { audio, transcript } = e.data as { audio?: string; transcript?: string };

      if (audio && whisperEnabled) {
        // Whisper path: transcribe via WebSocket, then send message
        const client = clientRef.current;
        if (!client?.connected) return;
        try {
          const data = await new Promise<{ transcript?: string }>((resolve, reject) => {
            const unsub = client.on('whisper:result', (d: { transcript?: string }) => {
              unsub();
              clearTimeout(timer);
              resolve(d);
            });
            const timer = setTimeout(() => { unsub(); reject(new Error('Timeout')); }, 30000);
            client.send('whisper:transcribe', { audio });
          });
          if (data.transcript?.trim()) {
            sendMessage(data.transcript.trim(), undefined, `data:audio/webm;base64,${audio}`);
          }
        } catch (err) {
          console.error('[BlobyApp] widget voice transcription error:', err);
        }
      } else if (transcript?.trim()) {
        // Web Speech path: send transcript directly
        sendMessage(transcript.trim());
      }
    };
    window.addEventListener('message', handler);
    return () => window.removeEventListener('message', handler);
  }, [sendMessage, whisperEnabled, chatBlocked]);

  // Auth gate: show spinner while checking, login screen if needed
  if (!authChecked) {
    return (
      <div className="flex items-center justify-center h-dvh">
        <div className="w-6 h-6 border-2 border-white/10 border-t-white/50 rounded-full animate-spin" />
      </div>
    );
  }

  if (authRequired && !authenticated) {
    return <LoginScreen onLogin={handleLogin} totpEnabled={totpEnabled} />;
  }

  return (
    <div className="flex flex-col h-dvh overflow-hidden">
      {/* Header */}
      <div className="flex items-center px-4 py-3.5 border-b border-white/[0.06] shrink-0">
        {/* Back */}
        <button
          onClick={() => window.parent?.postMessage({ type: 'bloby:close' }, '*')}
          className="flex items-center justify-center h-8 w-8 -ml-1 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
          aria-label="Close chat"
        >
          <ArrowLeft className="h-[18px] w-[18px]" />
        </button>

        {/* Mascot / headphones animation (nudged up 5px so it sits visually centered with the name) */}
        <div className="relative ml-1.5 shrink-0 -translate-y-[5px]">
          <HeadphonesAnimation recording={chatRecording} height={36} />
        </div>

        {/* Name + status line */}
        <div className="flex flex-col ml-2.5 min-w-0">
          <span className="text-[15px] font-semibold leading-tight tracking-[-0.01em]">{botName}</span>
          <div className="flex items-center gap-1.5 mt-0.5">
            <span className="flex items-center gap-1 text-[11px] text-muted-foreground">
              <span className={`h-1.5 w-1.5 rounded-full ${updating ? 'bg-amber-400 animate-pulse' : connected ? 'bg-emerald-500' : 'bg-red-500'}`} />
              {updating ? 'Updating…' : connected ? 'Online' : 'Offline'}
            </span>
            {/* Workspace indicator — uncomment when needed */}
            {/* <span className="flex items-center gap-1 text-[11px] text-muted-foreground">
              <span className={`h-1.5 w-1.5 rounded-full ${backendHealthy ? 'bg-emerald-500' : 'bg-orange-400 animate-pulse'}`} />
              Workspace
            </span> */}
          </div>
        </div>

        <div className="flex-1" />

        {/* Wallet */}
        {(walletTempo !== null || walletBase !== null) && (
          <div
            className="relative mr-1.5"
            onMouseEnter={() => setWalletOpen(true)}
            onMouseLeave={() => setWalletOpen(false)}
          >
            <div className="flex items-center rounded-xl bg-white/[0.04] border border-white/[0.06] pl-3 pr-1 py-1">
              <span className="text-[11px] text-muted-foreground/70 mr-1.5"><span className="hidden sm:inline">My </span>Wallet:</span>
              <span className="text-[13px] font-semibold font-mono tracking-tight">
                ${(Number(walletTempo ?? 0) + Number(walletBase ?? 0)).toFixed(2)}
              </span>
              <div className="w-px h-4 bg-white/[0.08] mx-2" />
              <a
                href="https://www.bloby.bot/dashboard"
                target="_blank"
                rel="noopener noreferrer"
                className="text-[11px] font-semibold text-primary bg-primary/10 hover:bg-primary/20 px-2.5 py-1 rounded-lg transition-colors"
              >
                <span className="sm:hidden">+</span><span className="hidden sm:inline">+ Add</span>
              </a>
            </div>

            {walletOpen && (
              <div className="absolute right-0 top-full pt-1.5 z-50">
                <div className="min-w-[220px] rounded-xl border border-white/[0.06] bg-popover py-2 shadow-xl">
                  <div className="px-3.5 pb-2 text-[10px] uppercase tracking-wider text-muted-foreground/60">Balances</div>
                  <div className="flex items-center justify-between px-3.5 py-1.5">
                    <div className="flex items-center gap-2">
                      <img
                        src="https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@master/svg/color/usdc.svg"
                        alt="USDC"
                        className="h-5 w-5"
                      />
                      <span className="text-[13px] font-semibold font-mono tracking-tight">${Number(walletTempo ?? 0).toFixed(2)}</span>
                    </div>
                    <span className="text-[11px] text-muted-foreground">on Tempo</span>
                  </div>
                  <div className="flex items-center justify-between px-3.5 py-1.5">
                    <div className="flex items-center gap-2">
                      <img
                        src="https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@master/svg/color/usdc.svg"
                        alt="USDC"
                        className="h-5 w-5"
                      />
                      <span className="text-[13px] font-semibold font-mono tracking-tight">${Number(walletBase ?? 0).toFixed(2)}</span>
                    </div>
                    <span className="text-[11px] text-muted-foreground">on Base</span>
                  </div>
                </div>
              </div>
            )}
          </div>
        )}

        {/* Right actions */}
        <div className="flex items-center gap-0.5">
          {pushState !== 'loading' && pushState !== 'unsupported' && (
            <button
              onClick={() => {
                if (pushState === 'subscribed') unsubscribePush();
                else if (pushState === 'unsubscribed') subscribePush();
              }}
              disabled={pushState === 'denied'}
              className="flex items-center justify-center h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
              title={
                pushState === 'denied' ? 'Notifications blocked — enable in browser settings' :
                pushState === 'subscribed' ? 'Disable push notifications' : 'Enable push notifications'
              }
            >
              {pushState === 'denied' ? (
                <BellOff className="h-[18px] w-[18px]" />
              ) : pushState === 'subscribed' ? (
                <BellRing className="h-[18px] w-[18px] text-primary" />
              ) : (
                <Bell className="h-[18px] w-[18px]" />
              )}
            </button>
          )}
          <div className="relative" ref={menuRef}>
            <button
              onClick={() => setMenuOpen((v) => { const next = !v; if (!next) setConfirmReset(false); return next; })}
              className="flex items-center justify-center h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
            >
              <EllipsisVertical className="h-[18px] w-[18px]" />
            </button>
            {menuOpen && (
              <div className="absolute right-0 top-full mt-1.5 min-w-[200px] rounded-xl border border-white/[0.06] bg-popover py-1.5 shadow-xl z-50">
                <button
                  onClick={() => { setShowWizard(true); setMenuOpen(false); setConfirmReset(false); }}
                  className="flex w-full items-center gap-2.5 px-3.5 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
                >
                  <WandSparkles className="h-4 w-4" />
                  Settings
                </button>
                {/* New conversation — resets the live agent session so heavy/long chats don't hit the context wall.
                    Non-destructive: past messages stay saved under the old conversation; this just starts a fresh one. */}
                {!confirmReset ? (
                  <button
                    onClick={() => setConfirmReset(true)}
                    className="flex w-full items-center gap-2.5 px-3.5 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
                  >
                    <MessageSquarePlus className="h-4 w-4" />
                    New conversation
                  </button>
                ) : (
                  <div className="px-3.5 py-2">
                    <p className="text-[12px] text-muted-foreground/70 mb-2 leading-snug">
                      Start fresh? This clears the agent's context — your past messages stay saved.
                    </p>
                    <div className="flex gap-2">
                      <button
                        onClick={() => { clearContext(); setConfirmReset(false); setMenuOpen(false); }}
                        className="flex-1 text-[12px] font-medium text-white bg-[#0069FE] hover:bg-[#0069FE]/90 px-2.5 py-1.5 rounded-lg transition-colors"
                      >
                        New chat
                      </button>
                      <button
                        onClick={() => setConfirmReset(false)}
                        className="flex-1 text-[12px] font-medium text-muted-foreground bg-white/[0.06] hover:bg-white/[0.1] px-2.5 py-1.5 rounded-lg transition-colors"
                      >
                        Cancel
                      </button>
                    </div>
                  </div>
                )}
                {isMobile && !isStandalone && (
                  <button
                    onClick={() => {
                      setMenuOpen(false);
                      setConfirmReset(false);
                      if (isInIframe) {
                        window.parent.postMessage({ type: 'bloby:install-app' }, '*');
                      } else {
                        setShowIosModal(true);
                      }
                    }}
                    className="flex w-full items-center gap-2.5 px-3.5 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-white/[0.06] transition-colors"
                  >
                    <Download className="h-4 w-4" />
                    Install App
                  </button>
                )}
              </div>
            )}
          </div>
        </div>
      </div>

      {/* Chat body */}
      <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
        <MessageList messages={messages} streaming={streaming} streamBuffer={streamBuffer} tools={tools} hasMore={hasMore} onLoadOlder={loadOlder} />
        <div className="relative">
        <InputBar
          onSend={(msg, attachments, audioData) => { if (!chatBlocked) sendMessage(msg, attachments, audioData); }}
          onStop={stopStreaming}
          streaming={streaming}
          whisperEnabled={whisperEnabled}
          onTranscribe={(audio) => {
            return new Promise((resolve, reject) => {
              const client = clientRef.current;
              if (!client?.connected) { reject(new Error('WebSocket not connected')); return; }
              const unsub = client.on('whisper:result', (data) => {
                unsub();
                clearTimeout(timer);
                resolve(data);
              });
              const timer = setTimeout(() => { unsub(); reject(new Error('Transcription timeout')); }, 30000);
              client.send('whisper:transcribe', { audio });
            });
          }}
          onAudioReady={addPendingAudio}
          onRecordingChange={setChatRecording}
        />
        {/* Input block — the agent can't receive messages mid-update / while disconnected.
            Overlay (not prop-threading through InputBar) so drafts/recordings underneath are
            preserved and usable the moment the agent is back. */}
        {chatBlocked && (
          <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/70 backdrop-blur-[2px]">
            <span className="flex items-center gap-2 text-[12px] text-muted-foreground bg-white/[0.05] border border-white/[0.08] rounded-full px-3.5 py-1.5 shadow-lg">
              <span className={`h-1.5 w-1.5 rounded-full animate-pulse ${updating ? 'bg-amber-400' : 'bg-red-500'}`} />
              {updating ? 'Updating — back in a moment…' : 'Offline — waiting for connection…'}
            </span>
          </div>
        )}
        </div>
      </div>

      {/* Setup Wizard overlay */}
      {showWizard && (
        <OnboardWizard
          onSave={(payload) => {
            return new Promise((resolve, reject) => {
              const client = clientRef.current;
              if (!client?.connected) {
                reject(new Error('WebSocket not connected'));
                return;
              }
              const unsub = client.on('settings:saved', (data) => {
                unsub();
                clearTimeout(timer);
                resolve(data);
              });
              const unsubErr = client.on('settings:save-error', (data) => {
                unsubErr();
                clearTimeout(timer);
                reject(new Error(data.error || 'Save failed'));
              });
              const timer = setTimeout(() => { unsub(); unsubErr(); reject(new Error('Save timeout')); }, 10000);
              client.send('settings:save', payload);
            });
          }}
          onTunnelSwitch={(newMode) => {
            return new Promise((resolve, reject) => {
              const client = clientRef.current;
              if (!client?.connected) { reject(new Error('Not connected')); return; }
              const unsub = client.on('tunnel:switched', (data) => { unsub(); unsubErr(); clearTimeout(t); resolve(data); });
              const unsubErr = client.on('tunnel:switch-error', (data) => { unsub(); unsubErr(); clearTimeout(t); reject(new Error(data.error)); });
              const t = setTimeout(() => { unsub(); unsubErr(); reject(new Error('Timeout')); }, 30000);
              client.send('tunnel:switch', { mode: newMode });
            });
          }}
          onComplete={() => {
            setShowWizard(false);
            // Reload settings (bot name, whisper, etc.)
            authFetch('/api/settings')
              .then((r) => r.json())
              .then((s) => {
                if (s.agent_name) setBotName(s.agent_name);
                setWhisperEnabled(s.whisper_enabled === 'true');
              })
              .catch(() => {});
            // Notify dashboard so it can refresh
            window.parent?.postMessage({ type: 'bloby:onboard-complete' }, '*');
          }}
        />
      )}

      {/* iOS Install Instructions Modal */}
      {showIosModal && (
        <div className="fixed inset-0 z-[300] flex items-end sm:items-center justify-center p-4">
          <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowIosModal(false)} />
          <div className="relative w-full max-w-[360px] bg-[#1c1c1e] border border-white/[0.08] rounded-2xl shadow-2xl overflow-hidden">
            <div className="flex items-center justify-between px-5 pt-5 pb-2">
              <h2 className="text-[17px] font-semibold text-white">Install App</h2>
              <button
                onClick={() => setShowIosModal(false)}
                className="h-7 w-7 rounded-full bg-white/[0.08] flex items-center justify-center text-white/50 hover:text-white/80 transition-colors"
              >
                <X className="h-4 w-4" />
              </button>
            </div>
            <div className="px-5 pb-6 pt-1">
              <p className="text-white/50 text-[13px] mb-5 leading-relaxed">
                Add Bloby to your home screen for a full-screen app experience.
              </p>
              {isIos ? (
                <div className="space-y-3.5">
                  <div className="flex items-start gap-3">
                    <span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">1</span>
                    <div className="flex-1">
                      <p className="text-white text-[14px] font-medium">
                        Tap the Share button
                      </p>
                      <p className="text-white/40 text-[12px] mt-0.5 flex items-center gap-1.5">
                        The <Share className="h-3.5 w-3.5 inline" /> icon at the bottom of Safari
                      </p>
                    </div>
                  </div>
                  <div className="flex items-start gap-3">
                    <span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">2</span>
                    <div className="flex-1">
                      <p className="text-white text-[14px] font-medium">
                        Scroll down and tap
                      </p>
                      <p className="text-white/40 text-[12px] mt-0.5">
                        "Add to Home Screen"
                      </p>
                    </div>
                  </div>
                  <div className="flex items-start gap-3">
                    <span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">3</span>
                    <div className="flex-1">
                      <p className="text-white text-[14px] font-medium">
                        Tap "Add"
                      </p>
                      <p className="text-white/40 text-[12px] mt-0.5">
                        Bloby will appear on your home screen
                      </p>
                    </div>
                  </div>
                </div>
              ) : (
                <div className="space-y-3.5">
                  <div className="flex items-start gap-3">
                    <span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">1</span>
                    <div className="flex-1">
                      <p className="text-white text-[14px] font-medium">
                        Open browser menu
                      </p>
                      <p className="text-white/40 text-[12px] mt-0.5">
                        Tap the three-dot menu in your browser
                      </p>
                    </div>
                  </div>
                  <div className="flex items-start gap-3">
                    <span className="flex-shrink-0 w-7 h-7 rounded-full bg-[#007AFF]/15 text-[#007AFF] text-[13px] font-semibold flex items-center justify-center mt-0.5">2</span>
                    <div className="flex-1">
                      <p className="text-white text-[14px] font-medium">
                        Tap "Install app" or "Add to Home screen"
                      </p>
                      <p className="text-white/40 text-[12px] mt-0.5">
                        Bloby will be installed as a standalone app
                      </p>
                    </div>
                  </div>
                </div>
              )}
              <button
                onClick={() => setShowIosModal(false)}
                className="w-full mt-5 py-2.5 bg-white/[0.06] hover:bg-white/[0.1] text-white text-[14px] font-medium rounded-xl transition-colors"
              >
                Got it
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <BlobyApp />
  </React.StrictMode>,
);
