/* global React, ReactDOM, Icon, initials, fmtDate, relTime, useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakSelect, TweakToggle, TweakButton, Drawer, api, useApi, fmtPhone */
const { useState, useEffect, useCallback } = React;

/* ---------------------------------------------------------------- *
 * Web Push helpers
 *
 * VAPID keys live on the server as base64url. The browser's
 * pushManager.subscribe() wants applicationServerKey as a Uint8Array
 * of the *raw* uncompressed P-256 public point (65 bytes), which is
 * what `urlBase64ToUint8Array` produces.
 *
 * Common gotcha: regular base64 uses + and /, but VAPID keys are
 * URL-safe base64 (- and _) AND have no padding. We pad to a multiple
 * of 4 and translate back to standard alphabet before atob().
 * ---------------------------------------------------------------- */
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(base64);
  const out = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
  return out;
}

/* usePushNotifications — encapsulates the entire subscribe/unsubscribe
 * lifecycle for the dashboard's notification toggle. Returns an object
 * with `supported` (whether the browser even has the APIs), the current
 * `permission` ("default" | "granted" | "denied"), `subscribed` (do we
 * have a live PushSubscription?), and the actions `enable()`,
 * `disable()`, `test()`.
 *
 * Calling enable() on an already-subscribed browser is a no-op (it just
 * re-POSTs the existing subscription so the server's `last_seen_at`
 * updates). disable() unsubscribes locally AND tells the server to
 * delete the row, otherwise the server would keep blasting pushes at a
 * dead endpoint until it gets a 410 from the push service.
 */
function usePushNotifications() {
  const supported = typeof window !== 'undefined'
    && 'serviceWorker' in navigator
    && 'PushManager' in window
    && 'Notification' in window;

  const [permission, setPermission] = useState(
    supported ? Notification.permission : 'unsupported'
  );
  const [subscribed, setSubscribed] = useState(false);
  const [busy, setBusy] = useState(false);

  // Re-check the browser's existing subscription whenever the SW is ready
  // — this catches the case where Bill enabled notifications in a prior
  // session and we just need to reflect that state in the UI.
  const refresh = useCallback(async () => {
    if (!supported) return;
    try {
      const reg = await navigator.serviceWorker.ready;
      const sub = await reg.pushManager.getSubscription();
      setSubscribed(!!sub);
    } catch (_) { /* swallow — UI just won't update */ }
  }, [supported]);

  useEffect(() => { refresh(); }, [refresh]);

  const enable = useCallback(async () => {
    if (!supported) return { ok: false, error: 'unsupported' };
    setBusy(true);
    try {
      // Step 1: ask the user. Firefox/Chrome will only show the prompt
      // once per origin per "no" — re-prompts after deny don't fire.
      const perm = await Notification.requestPermission();
      setPermission(perm);
      if (perm !== 'granted') return { ok: false, error: 'denied' };

      // Step 2: get the SW registration. ready resolves only after
      // activate, so the push handler is guaranteed installed.
      const reg = await navigator.serviceWorker.ready;

      // Step 3: subscribe with the server's VAPID public key. If we
      // already have a subscription, reuse it.
      let sub = await reg.pushManager.getSubscription();
      if (!sub) {
        const vapidB64 = await api.getVapidPublicKey();
        sub = await reg.pushManager.subscribe({
          userVisibleOnly: true,                          // required by Chrome
          applicationServerKey: urlBase64ToUint8Array(vapidB64),
        });
      }

      // Step 4: tell our server who's subscribed. The server stores
      // endpoint+keys so it can sendNotification() to this device later.
      await api.pushSubscribe(sub.toJSON(), navigator.userAgent.slice(0, 80));
      setSubscribed(true);
      return { ok: true };
    } catch (err) {
      console.error('[push] enable failed:', err);
      return { ok: false, error: String(err) };
    } finally {
      setBusy(false);
    }
  }, [supported]);

  const disable = useCallback(async () => {
    if (!supported) return;
    setBusy(true);
    try {
      const reg = await navigator.serviceWorker.ready;
      const sub = await reg.pushManager.getSubscription();
      if (sub) {
        // Tell the server first so it stops trying to push. If the
        // unsubscribe-on-server fails, we still proceed with the local
        // unsubscribe — better to be silent than to blast a dead device.
        try { await api.pushUnsubscribe(sub.endpoint); } catch (_) {}
        await sub.unsubscribe();
      }
      setSubscribed(false);
    } finally {
      setBusy(false);
    }
  }, [supported]);

  const test = useCallback(async () => {
    try {
      await api.pushTest('🧪 Test', 'If you see this, push is wired up.');
    } catch (err) {
      console.error('[push] test failed:', err);
      alert('Test push failed: ' + err.message);
    }
  }, []);

  return { supported, permission, subscribed, busy, enable, disable, test, refresh };
}
window.usePushNotifications = usePushNotifications;

const NAV = [
  { id: "today",        label: "Today",        icon: "home" },
  { id: "followups",    label: "Followups",    icon: "alert" },
  { id: "calls",        label: "Calls",        icon: "phone" },
  { id: "customers",    label: "Customers",    icon: "users" },
  { id: "appointments", label: "Schedule",     icon: "calendar" },
  { id: "messages",     label: "Messages",     icon: "message" },
  { id: "documents",    label: "Docs",         icon: "doc" },
  { id: "fleet",        label: "Fleet",        icon: "truck" },
  { id: "assistant",    label: "OpenClaw",     icon: "sparkle" },
];
// Debug nav lives in its own section below the main nav. Intentionally
// NOT in the main NAV array so the mobile bottom nav never surfaces it —
// only the desktop sidebar shows the Debug entry. Bill should never see
// this on his phone.
// Followups in the primary mobile nav — it's Bill's daily action queue
// (calls that need a callback). Putting it next to Today makes it the
// first thing his thumb reaches when he opens the app. Messages moved
// to overflow because most caller-side messages are auto-handled.
const MOBILE_PRIMARY = ["today","followups","calls","appointments"];
const MOBILE_OVERFLOW = ["messages","documents","customers","fleet","assistant"];

/* Customer drawer — now works with real data shape.
 *
 * Action buttons used to be no-ops ("Send SMS" + "Schedule visit"
 * with no onClick — fixed 2026-05-02). Now:
 *   - "Send SMS" opens an inline composer that POSTs to
 *     /api/messages/outbound (proxies to twilio_bridge → Twilio).
 *   - "Schedule visit" jumps to the Schedule tab and pre-opens the
 *     New-appointment form with this customer's phone+name+address.
 *   - A new "Notes" textarea below the address grid lets Bill capture
 *     dog warnings, gate codes, "owner is hard of hearing" etc. — saved
 *     via PATCH /api/customers/:phone.
 */
const CustomerDrawer = ({ customer, onClose, onScheduleVisit }) => {
  if (!customer) return null;
  const isWeekly = customer.last_service?.toLowerCase() === 'weekly service';
  const { data: detail, refetch } = useApi(
    () => customer.phone ? api.customer(customer.phone) : Promise.resolve(null),
    [customer.phone]
  );
  const cust = detail?.customer || customer;  // prefer the full row from /api/customers/:phone
  const calls = detail?.calls || [];
  const appointments = detail?.appointments || [];

  const [smsOpen, setSmsOpen] = useState(false);
  const [smsText, setSmsText] = useState('');
  const [smsBusy, setSmsBusy] = useState(false);

  const sendSms = async () => {
    if (!smsText.trim() || !customer.phone) return;
    setSmsBusy(true);
    try {
      await api.sendMessage(customer.phone, smsText.trim(), 'sms');
      toast('SMS sent');
      setSmsText('');
      setSmsOpen(false);
    } catch (e) { toast('Error: ' + e.message); }
    finally { setSmsBusy(false); }
  };

  // Notes editor state — local until saved.
  const [notesEditing, setNotesEditing] = useState(false);
  const [notesValue, setNotesValue] = useState('');
  useEffect(() => {
    setNotesValue(cust.notes || '');
    setNotesEditing(false);
  }, [cust.phone, cust.notes]);

  const saveNotes = async () => {
    try {
      await api.updateCustomer(customer.phone, { notes: notesValue });
      toast('Notes saved');
      setNotesEditing(false);
      refetch();
    } catch (e) { toast('Error: ' + e.message); }
  };

  return (
    <Drawer
      title={cust.name || 'Unknown'}
      subtitle={`${fmtPhone(cust.phone)} · ${isWeekly ? "Weekly client" : cust.last_service || "On-call"}`}
      onClose={onClose}
      actions={[
        { label: smsOpen ? "Cancel SMS" : "Send SMS", primary: !smsOpen, onClick: () => setSmsOpen(o => !o) },
        { label: "Schedule visit", onClick: () => { onScheduleVisit && onScheduleVisit(cust); onClose(); } },
      ]}
    >
      {smsOpen && (
        <div style={{marginBottom: 16, padding: 12, border: '1px solid var(--line)', borderRadius: 'var(--r-sm)', background: 'var(--paper-2, transparent)'}}>
          <div className="eyebrow" style={{marginBottom: 6}}>Send SMS to {fmtPhone(cust.phone)}</div>
          <textarea
            className="if-value"
            value={smsText}
            onChange={e => setSmsText(e.target.value)}
            rows={3}
            placeholder="Type your message…"
            style={{width: '100%', resize: 'vertical'}}
            autoFocus
          />
          <div style={{display: 'flex', gap: 6, marginTop: 8, justifyContent: 'flex-end'}}>
            <button className="btn ghost xs" onClick={() => { setSmsText(''); setSmsOpen(false); }}>Cancel</button>
            <button className="btn primary xs" onClick={sendSms} disabled={smsBusy || !smsText.trim()}>
              {smsBusy ? 'Sending…' : 'Send'}
            </button>
          </div>
        </div>
      )}
      <div className="intake-grid">
        <div className="intake-field"><div className="if-label">Address</div><div className="if-value">{cust.address || '—'}</div></div>
        <div className="intake-field"><div className="if-label">Pool</div><div className="if-value">{cust.pool_size || '—'}</div></div>
        <div className="intake-field"><div className="if-label">Last seen</div><div className="if-value">{cust.last_seen ? fmtDate(cust.last_seen) : "—"}</div></div>
        <div className="intake-field"><div className="if-label">First seen</div><div className="if-value">{cust.first_seen ? fmtDate(cust.first_seen) : "—"}</div></div>
        <div className="intake-field" style={{gridColumn: '1 / -1'}}>
          <div className="if-label" style={{display:'flex', justifyContent:'space-between', alignItems:'center'}}>
            <span>Notes</span>
            {!notesEditing && <button className="btn ghost xs" onClick={() => setNotesEditing(true)}>Edit</button>}
          </div>
          {!notesEditing ? (
            <div className="if-value" style={{whiteSpace:'pre-wrap', minHeight: 24}}>
              {cust.notes || <span style={{color:'var(--ink-3)'}}>No notes yet — gate codes, dogs, owner preferences, etc.</span>}
            </div>
          ) : (
            <div style={{display:'flex', flexDirection:'column', gap:6}}>
              <textarea
                className="if-value"
                value={notesValue}
                onChange={e => setNotesValue(e.target.value)}
                rows={4}
                placeholder="Gate code 4471, dog at the back, owner is hard of hearing…"
                style={{width:'100%', resize:'vertical'}}
              />
              <div style={{display:'flex', gap:6, justifyContent:'flex-end'}}>
                <button className="btn ghost xs" onClick={() => { setNotesValue(cust.notes || ''); setNotesEditing(false); }}>Cancel</button>
                <button className="btn primary xs" onClick={saveNotes}>Save</button>
              </div>
            </div>
          )}
        </div>
      </div>
      <div className="eyebrow" style={{marginBottom: 8}}>Recent calls ({calls.length})</div>
      <div style={{display:"flex", flexDirection:"column", gap:10}}>
        {calls.slice(0, 5).map(k => (
          <div key={k.call_id} className="inbox-item" style={{padding:"10px 12px", borderRadius:"var(--r-sm)", border:"1px solid var(--line)", borderBottom:"1px solid var(--line)"}}>
            <div className="avatar sm"><Icon name="phone" size={14} /></div>
            <div>
              <div className="inbox-name">{k.service || 'Call'} · {k.duration_s ? Math.round(k.duration_s/60) + 'm' : '—'}</div>
              <div className="inbox-preview">{relTime(k.started_at)}</div>
            </div>
          </div>
        ))}
        {calls.length === 0 && <div style={{color:'var(--ink-3)', fontSize:13, padding:'8px 0'}}>No recent calls.</div>}
      </div>
      {appointments.length > 0 && (
        <>
          <div className="eyebrow" style={{marginBottom: 8, marginTop: 16}}>Appointments ({appointments.length})</div>
          <div style={{display:"flex", flexDirection:"column", gap:10}}>
            {appointments.slice(0, 5).map(a => (
              <div key={a.id} className="inbox-item" style={{padding:"10px 12px", borderRadius:"var(--r-sm)", border:"1px solid var(--line)", borderBottom:"1px solid var(--line)"}}>
                <div className="avatar sm sage"><Icon name="calendar" size={14} /></div>
                <div>
                  <div className="inbox-name">{a.service || 'Appointment'}</div>
                  <div className="inbox-preview">{fmtDate(a.scheduled_at)} · {a.status}</div>
                </div>
              </div>
            ))}
          </div>
        </>
      )}
    </Drawer>
  );
};

/* ====================================================================
   Global search (top bar) — debounced fetch to /api/search, dropdown
   below the input with mixed customer/call/appointment/document hits.
   Real bug 2026-05-02: search input had NO onChange handler; typing
   did nothing.
   ==================================================================== */
const GlobalSearch = ({ onPick }) => {
  const [q, setQ] = useState('');
  const [results, setResults] = useState([]);
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [activeIdx, setActiveIdx] = useState(0);
  const inputRef = React.useRef(null);
  const wrapRef = React.useRef(null);

  // Debounce — 200 ms after the last keystroke, fire the search.
  useEffect(() => {
    if (!q.trim()) { setResults([]); return; }
    const handle = setTimeout(async () => {
      setLoading(true);
      try {
        const data = await api.search(q);
        setResults(data || []);
        setActiveIdx(0);
      } catch (_) { setResults([]); }
      finally { setLoading(false); }
    }, 200);
    return () => clearTimeout(handle);
  }, [q]);

  // ⌘K / Ctrl+K to focus
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
        e.preventDefault();
        inputRef.current?.focus();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  // Click outside closes the dropdown
  useEffect(() => {
    const onClick = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', onClick);
    return () => document.removeEventListener('mousedown', onClick);
  }, []);

  const pick = (item) => {
    setQ('');
    setResults([]);
    setOpen(false);
    onPick(item);
  };

  const onKeyDown = (e) => {
    if (!open || results.length === 0) return;
    if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIdx(i => Math.min(i + 1, results.length - 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIdx(i => Math.max(i - 1, 0)); }
    else if (e.key === 'Enter') { e.preventDefault(); pick(results[activeIdx]); }
    else if (e.key === 'Escape') { setOpen(false); inputRef.current?.blur(); }
  };

  const typeIcon = (t) => ({
    customer: 'users', call: 'phone', appointment: 'calendar', document: 'doc',
  })[t] || 'search';

  return (
    <div ref={wrapRef} className="search-global" style={{position: 'relative'}}>
      <Icon name="search" size={16} />
      <input
        ref={inputRef}
        placeholder="Search anything…"
        value={q}
        onChange={(e) => { setQ(e.target.value); setOpen(true); }}
        onFocus={() => setOpen(true)}
        onKeyDown={onKeyDown}
      />
      <span className="kbd">⌘K</span>
      {open && (q.trim() || loading) && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
          background: 'var(--paper)', border: '1px solid var(--line)',
          borderRadius: 'var(--r-sm)', boxShadow: '0 8px 24px rgba(0,0,0,0.08)',
          maxHeight: 380, overflowY: 'auto', zIndex: 100,
        }}>
          {loading && <div style={{padding: 12, color: 'var(--ink-3)', fontSize: 13}}>Searching…</div>}
          {!loading && results.length === 0 && q.trim() && (
            <div style={{padding: 12, color: 'var(--ink-3)', fontSize: 13}}>No matches for "{q}".</div>
          )}
          {results.map((item, i) => (
            <div
              key={`${item.type}-${i}`}
              onMouseDown={(e) => e.preventDefault()}  // don't blur the input on click
              onClick={() => pick(item)}
              onMouseEnter={() => setActiveIdx(i)}
              style={{
                padding: '8px 12px',
                display: 'flex', gap: 10, alignItems: 'center',
                cursor: 'pointer',
                background: i === activeIdx ? 'var(--line)' : 'transparent',
                borderBottom: i < results.length - 1 ? '1px solid var(--line)' : 'none',
              }}
            >
              <Icon name={typeIcon(item.type)} size={14} />
              <div style={{flex: 1, minWidth: 0}}>
                <div style={{fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{item.label}</div>
                {item.sub && <div style={{fontSize: 11, color: 'var(--ink-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{item.sub}</div>}
              </div>
              <span style={{fontSize: 10, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: 0.5}}>{item.type}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

const App = () => {
  const [tweakValues, setTweak] = useTweaks(/*EDITMODE-BEGIN*/{
    "mood": "clean",
    "density": "default",
    "theme": "light",
  }/*EDITMODE-END*/);

  const [view, setView] = useState("today");
  const [openCustomer, setOpenCustomer] = useState(null);
  const [openCallId, setOpenCallId] = useState(null);
  const [openThreadId, setOpenThreadId] = useState(null);
  const [openAppointment, setOpenAppointment] = useState(null);
  const [settingsOpen, setSettingsOpen] = useState(false);
  // Prefill payload for AppointmentsView's New-appointment form, set
  // when the user clicks "Schedule visit" from CustomerDrawer. The view
  // consumes it on first render and resets it back to null.
  const [prefilledAppointment, setPrefilledAppointment] = useState(null);
  const [moreOpen, setMoreOpen] = useState(false);

  // Connection status check
  const [connected, setConnected] = useState(false);
  useEffect(() => {
    const check = () => fetch('/health').then(r => r.json()).then(d => setConnected(d.ok)).catch(() => setConnected(false));
    check();
    const iv = setInterval(check, 15000);
    return () => clearInterval(iv);
  }, []);

  // Service worker registration — installs our /hub/sw.js so push
  // notifications work. Scope is /hub/ so the SW only intercepts hub
  // routes; the chat widget at /widget.html stays untouched.
  // Registering after mount (not at module-load) keeps the React paint
  // unblocked. Errors here are non-fatal — the app still works without
  // push.
  useEffect(() => {
    if (!('serviceWorker' in navigator)) return;
    navigator.serviceWorker.register('/hub/sw.js', { scope: '/hub/' })
      .then(reg => console.log('[sw] registered, scope:', reg.scope))
      .catch(err => console.warn('[sw] registration failed:', err));
    // Listen for navigate messages from the SW (notification taps).
    const handleMsg = (ev) => {
      if (ev.data && ev.data.type === 'navigate' && typeof ev.data.url === 'string') {
        // Route by hash so we don't full-reload.
        try {
          const u = new URL(ev.data.url, location.href);
          if (u.hash) location.hash = u.hash;
          else if (u.pathname !== location.pathname) location.href = ev.data.url;
        } catch (_) { /* ignore */ }
      }
    };
    navigator.serviceWorker.addEventListener('message', handleMsg);
    return () => navigator.serviceWorker.removeEventListener('message', handleMsg);
  }, []);

  // Push notification subscription state — exposed to the tweaks panel
  // so Bill can toggle pushes on/off and fire a test push.
  const push = usePushNotifications();

  // Live calls feed — Server-Sent Events from the bridge (proxied through
  // bill-pool-web's /api/live). One entry per currently-active call. Auto
  // reconnects if the SSE drops; entries are removed when call_ended fires.
  const [liveCalls, setLiveCalls] = useState([]);
  useEffect(() => {
    let es = null;
    let reconnectTimer = null;
    const open = () => {
      es = new EventSource('/api/live');
      es.onmessage = (ev) => {
        let m;
        try { m = JSON.parse(ev.data); } catch { return; }
        if (m.type === 'call_started') {
          setLiveCalls(prev => {
            if (prev.some(c => c.call_id === m.call_id)) return prev;
            return [...prev, { call_id: m.call_id, started_at: m.started_at, caller_phone: m.caller_phone }];
          });
        } else if (m.type === 'call_ended') {
          setLiveCalls(prev => prev.filter(c => c.call_id !== m.call_id));
        }
      };
      es.onerror = () => {
        if (es) { es.close(); es = null; }
        // Backoff a bit before reconnect so we don't hammer the bridge if it's down.
        reconnectTimer = setTimeout(open, 3000);
      };
    };
    open();
    return () => {
      if (es) es.close();
      if (reconnectTimer) clearTimeout(reconnectTimer);
    };
  }, []);

  /* apply tweak attributes to root */
  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute("data-mood", tweakValues.mood);
    root.setAttribute("data-density", tweakValues.density);
    root.setAttribute("data-theme", tweakValues.theme);
  }, [tweakValues.mood, tweakValues.density, tweakValues.theme]);

  // Hash-based deep links for push-notification taps. We pack
  // /hub/#<view> into the notification's data.url so a tap routes
  // directly to the relevant tab (appointments/followups/messages).
  useEffect(() => {
    const route = () => {
      const hash = (location.hash || '').replace(/^#/, '').toLowerCase();
      if (!hash) return;
      // Map common deep-link names to view ids. Keep this list short —
      // adding routes is a one-line change here.
      const map = {
        appointments: 'appointments',
        schedule: 'appointments',
        followups: 'followups',
        followup: 'followups',
        messages: 'messages',
        sms: 'messages',
        calls: 'calls',
        today: 'today',
      };
      const v = map[hash];
      if (v) setView(v);
    };
    route();
    window.addEventListener('hashchange', route);
    return () => window.removeEventListener('hashchange', route);
  }, []);

  const isDark = tweakValues.theme === "dark";
  const toggleTheme = () => setTweak("theme", isDark ? "light" : "dark");

  // Messages badge from real data
  const { data: stats } = useApi(() => api.stats(), []);
  // Followups badge — count of open callback items. Polls every 30s
  // so Bill sees new ones land without refreshing.
  const { data: openFollowups } = useApi(() => api.followups('open'), []);
  const navBadges = {
    messages: stats?.needs_reply || null,
    followups: openFollowups?.length || null,
  };

  const goMessages = (phone) => { setOpenThreadId(phone); setView("messages"); };

  const ViewBody = () => {
    switch (view) {
      case "today":        return <TodayView onJump={setView} onOpenCustomer={setOpenCustomer} onOpenCall={(id) => { setOpenCallId(id); setView("calls"); }} onOpenThread={goMessages} />;
      case "followups":    return <FollowupsView onOpenCall={(id) => { setOpenCallId(id); setView("calls"); }} onOpenCustomer={setOpenCustomer} />;
      case "calls":        return <CallsView onOpenCustomer={setOpenCustomer} openCallId={openCallId} />;
      case "customers":    return <CustomersView onOpenCustomer={setOpenCustomer} />;
      case "appointments": return <AppointmentsView
        onOpenCustomer={setOpenCustomer}
        onOpenAppointment={setOpenAppointment}
        prefilled={prefilledAppointment}
        onPrefilledConsumed={() => setPrefilledAppointment(null)}
      />;
      case "messages":     return <MessagesView openThreadId={openThreadId} setOpenThreadId={setOpenThreadId} />;
      case "documents":    return <DocumentsView />;
      case "fleet":        return <FleetView />;
      case "assistant":    return <AssistantView />;
      case "debug":        return <DebugView />;
      default:             return null;
    }
  };

  return (
    // data-view exposes the active view to CSS so individual views
    // can opt into custom chrome on mobile (e.g. the OpenClaw chat
    // hides topbar/page-head and runs full-bleed for an app-like feel).
    <div className="app" data-view={view}>
      <div className="brand">
        <div className="brand-mark"></div>
        <div className="brand-wordmark">
          <div className="brand-name">Bill Snak</div>
          <div className="brand-sub">Pool Services · Hub</div>
        </div>
      </div>

      <div className="topbar">
        <div className="greeting">
          {/* Debug isn't in the main NAV array (intentionally — it lives in
           * a separate sidebar section and isn't in the mobile bottom nav).
           * Look it up explicitly so the topbar title doesn't fall back to
           * "Today" when you're on the Debug page. */}
          <h1>{view === "debug" ? "Debug" : (NAV.find(n => n.id === view)?.label || "Today")}</h1>
          <div className="date">{new Date().toLocaleDateString([], { weekday: "long", month: "long", day: "numeric" })}</div>
        </div>
        <div className="topbar-spacer"></div>
        <GlobalSearch
          onPick={(item) => {
            // Each result type knows how to "open itself" — drive the
            // existing app-state hooks instead of a hard navigation.
            if (item.type === 'customer') {
              setOpenCustomer(item.ref);
              setView('customers');
            } else if (item.type === 'call') {
              setOpenCallId(item.ref.call_id);
              setView('calls');
            } else if (item.type === 'appointment') {
              setOpenAppointment(item.ref);
              setView('appointments');
            } else if (item.type === 'document') {
              setView('documents');
            }
          }}
        />
        <button
          className="theme-toggle"
          onClick={toggleTheme}
          aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
          title={isDark ? "Switch to light mode" : "Switch to dark mode"}
        >
          <Icon name={isDark ? "sun" : "moon"} size={16} />
        </button>
        {/* Settings now lives in the bottom-nav More sheet (mobile) +
            via 'View > Settings' on desktop (handled by the More sheet
            too). Removed the redundant topbar ... button so the topbar
            holds only the theme toggle + live-call indicator. */}
        {/* Live-call indicator — shows + click takes you to that call's
            detail page on the Calls tab. Hidden when no active calls. */}
        {liveCalls.length > 0 && (
          <button
            className="conn live"
            onClick={() => {
              if (liveCalls.length === 1) {
                setOpenCallId(liveCalls[0].call_id);
              }
              setView("calls");
            }}
            title={liveCalls.map(c => `${c.caller_phone || 'unknown'} since ${new Date(c.started_at).toLocaleTimeString()}`).join('\n')}
          >
            <span className="conn-dot"></span>
            {liveCalls.length === 1 ? `Live · ${liveCalls[0].caller_phone || 'caller'}` : `${liveCalls.length} live calls`}
          </button>
        )}
        <div className={`conn ${connected ? "ok" : ""}`}>
          <span className="conn-dot"></span>
          {connected ? "Connected" : "Offline"}
        </div>
      </div>

      <nav className="nav">
        <div className="nav-section">Workspace</div>
        {NAV.slice(0, 7).map(n => (
          <button key={n.id} className={`nav-item ${view === n.id ? "active" : ""}`} onClick={() => setView(n.id)}>
            <span className="nav-icon"><Icon name={n.icon} /></span>
            {n.label}
            {navBadges[n.id] && <span className="nav-badge">{navBadges[n.id]}</span>}
          </button>
        ))}
        <div className="nav-section">Assistant</div>
        <button className={`nav-item ${view === "assistant" ? "active" : ""}`} onClick={() => setView("assistant")}>
          <span className="nav-icon"><Icon name="sparkle" /></span>
          OpenClaw
        </button>
        <div style={{flex: 1}}></div>
        <div className="nav-section">Settings</div>
        {/* Desktop entry to the Settings drawer. On mobile the same
            drawer is opened from the bottom-nav "More" sheet. Keeping
            them as one drawer (instead of a full-page view) avoids
            URL-state coupling for what's just preferences. */}
        <button className="nav-item" onClick={() => setSettingsOpen(true)}>
          <span className="nav-icon"><Icon name="more" /></span>
          Settings
        </button>
        <div className="nav-section">Internal</div>
        <button className={`nav-item ${view === "debug" ? "active" : ""}`} onClick={() => setView("debug")}>
          <span className="nav-icon"><Icon name="bug" /></span>
          Debug
        </button>
        <div style={{padding: "12px 10px", fontSize: 11.5, color: "var(--ink-3)", lineHeight: 1.4}}>
          <div style={{display:"flex", alignItems:"center", gap:6, marginBottom: 4}}>
            <span className={`dot ${connected ? 'sage' : 'clay'}`}></span>
            <span style={{fontWeight: 600, color: "var(--ink-2)"}}>{connected ? 'Voice agent online' : 'Connecting…'}</span>
          </div>
          Calls answered 24/7 · Tailscale active
        </div>
      </nav>

      <main className="main">
        <ViewBody />
      </main>

      {/* Mobile bottom nav */}
      <div className="mobile-nav">
        <div className="mobile-nav-inner">
          {MOBILE_PRIMARY.map(id => {
            const n = NAV.find(x => x.id === id);
            return (
              <button key={id} className={`mobile-nav-item ${view === id ? "active" : ""}`} onClick={() => setView(id)}>
                <span className="mn-icon"><Icon name={n.icon} size={22} /></span>
                {n.label}
                {navBadges[id] && <span className="mn-badge">{navBadges[id]}</span>}
              </button>
            );
          })}
          <button
            className={`mobile-nav-item ${MOBILE_OVERFLOW.includes(view) ? "active" : ""}`}
            onClick={() => setMoreOpen(true)}
            aria-label="More"
          >
            <span className="mn-icon"><Icon name="more" size={22} /></span>
            More
          </button>
        </div>
      </div>

      {moreOpen && (
        <>
          <div className="sheet-backdrop" onClick={() => setMoreOpen(false)}></div>
          <div className="sheet" role="dialog" aria-label="More">
            <div className="sheet-handle" aria-hidden="true"></div>
            <div className="sheet-title">More</div>
            <div className="sheet-grid">
              {MOBILE_OVERFLOW.map(id => {
                const n = NAV.find(x => x.id === id);
                return (
                  <button
                    key={id}
                    className={`sheet-tile ${view === id ? "active" : ""}`}
                    onClick={() => { setView(id); setMoreOpen(false); }}
                  >
                    <span className="sheet-icon"><Icon name={n.icon} size={22} /></span>
                    <span className="sheet-label">{n.label === "Docs" ? "Documents" : n.label}</span>
                  </button>
                );
              })}
              {/* Settings tile — Bill's notification toggle, theme, density.
                  Lives in the More sheet so the topbar stays a single
                  theme-toggle and live-call indicator (especially helpful
                  on mobile where the topbar is already crowded). */}
              <button
                className="sheet-tile"
                onClick={() => { setMoreOpen(false); setSettingsOpen(true); }}
              >
                <span className="sheet-icon"><Icon name="more" size={22} /></span>
                <span className="sheet-label">Settings</span>
              </button>
            </div>
          </div>
        </>
      )}

      <CustomerDrawer
        customer={openCustomer}
        onClose={() => setOpenCustomer(null)}
        onScheduleVisit={(c) => {
          // Jump to Schedule tab and open the New-appointment form
          // pre-filled with this customer's info. AppointmentsView reads
          // this via the `prefilledAppointment` prop and clears it once
          // the form is opened.
          setPrefilledAppointment({
            customer_phone: c.phone || '',
            customer_name: c.name || '',
            address: c.address || '',
          });
          setView('appointments');
        }}
      />
      <AppointmentDrawer appointment={openAppointment} onClose={() => setOpenAppointment(null)} onOpenCustomer={(c) => { setOpenAppointment(null); setOpenCustomer(c); }} />

      {settingsOpen && (
        <Drawer
          title="Settings"
          subtitle="Preferences for this device"
          onClose={() => setSettingsOpen(false)}
          actions={[]}
        >
          <div className="eyebrow" style={{marginBottom: 8}}>Notifications</div>
          {!push.supported ? (() => {
            // iOS exposes Web Push ONLY to standalone-installed PWAs on
            // 16.4+. Detect the specific not-yet-standalone case so we
            // can show actionable steps ("close Safari, tap the icon")
            // instead of a generic "not supported" wall.
            const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
            const isStandalone = (typeof navigator.standalone === 'boolean' && navigator.standalone)
              || window.matchMedia('(display-mode: standalone)').matches;
            return (
              <div style={{color: 'var(--ink-3)', fontSize: 13, padding: '6px 0 16px', lineHeight: 1.5}}>
                {isIOS && !isStandalone ? (
                  <>
                    <strong style={{color: 'var(--ink-1)'}}>You're in Safari, not the installed app.</strong>
                    <ol style={{margin: '6px 0 0 18px', padding: 0}}>
                      <li>Close this Safari tab.</li>
                      <li>Find the <strong>Bill Snak</strong> icon on your home screen.</li>
                      <li>Tap it to launch the app, then come back to Settings — the "Turn on" button will appear here.</li>
                    </ol>
                  </>
                ) : isIOS ? (
                  <>
                    Your iOS version doesn't support Web Push. Notifications need iOS 16.4 or newer (current iPhones are typically iOS 17+).
                  </>
                ) : (
                  <>This browser doesn't support push notifications.</>
                )}
              </div>
            );
          })() : (
            <div style={{display:'flex', flexDirection:'column', gap:10, paddingBottom: 14}}>
              <div style={{display:'flex', justifyContent:'space-between', alignItems:'center'}}>
                <div>
                  <div style={{fontWeight: 600, fontSize: 14}}>Push alerts</div>
                  <div style={{color:'var(--ink-3)', fontSize: 12}}>
                    Get a buzz on this device for: new SMS from a customer,
                    new bookings, callback alerts, and OpenClaw replies you'd
                    miss while on another tab.
                  </div>
                </div>
                <button
                  className={`btn ${push.subscribed ? 'ghost' : 'primary'} xs`}
                  onClick={() => push.subscribed ? push.disable() : push.enable()}
                  style={{minWidth: 78}}
                >
                  {push.subscribed ? 'Turn off' : 'Turn on'}
                </button>
              </div>
              {push.permission === 'denied' && (
                <div style={{fontSize: 12, color: 'var(--clay)'}}>
                  Browser permission was denied. Open this page's site
                  settings (lock icon → Permissions) and allow Notifications,
                  then come back and tap "Turn on".
                </div>
              )}
              {push.subscribed && (
                <button className="btn ghost xs" onClick={push.test}>
                  Send test notification
                </button>
              )}
              {push.supported && !push.subscribed && push.permission !== 'denied' && (
                <div style={{fontSize: 11, color: 'var(--ink-3)'}}>
                  iOS tip: notifications only work after you've added this
                  app to your Home Screen via Safari and opened it from the
                  icon at least once.
                </div>
              )}
            </div>
          )}

          <div className="eyebrow" style={{marginBottom: 8, marginTop: 6}}>Appearance</div>
          <div className="intake-grid">
            <div className="intake-field">
              <div className="if-label">Theme</div>
              <select
                className="if-value"
                value={tweakValues.theme}
                onChange={(e) => setTweak('theme', e.target.value)}
              >
                <option value="light">Light</option>
                <option value="dark">Dark</option>
              </select>
            </div>
            <div className="intake-field">
              <div className="if-label">Density</div>
              <select
                className="if-value"
                value={tweakValues.density}
                onChange={(e) => setTweak('density', e.target.value)}
              >
                <option value="compact">Compact</option>
                <option value="default">Default</option>
                <option value="cozy">Cozy</option>
              </select>
            </div>
          </div>
          <div style={{fontSize: 11, color: 'var(--ink-3)', marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--line)'}}>
            VAPID public key (for support): <code style={{wordBreak: 'break-all'}}>BCw67tdXSgVdtgox1Pbop3E1oBik3RCm1xZdWAzFzldV2Ku8azHX17AMq0RQV2fQqMzVPBBEDni6W1wxbdXHHSM</code>
          </div>
        </Drawer>
      )}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Theme" />
        <TweakRadio
          label="Mode"
          value={tweakValues.theme}
          onChange={v => setTweak("theme", v)}
          options={["light", "dark"]}
        />
        <TweakSection label="Color mood" />
        <TweakRadio
          label="Mood"
          value={tweakValues.mood}
          onChange={v => setTweak("mood", v)}
          options={["clean", "deep", "sky"]}
        />
        <TweakSection label="Density" />
        <TweakRadio
          label="Spacing"
          value={tweakValues.density}
          onChange={v => setTweak("density", v)}
          options={["compact", "default", "cozy"]}
        />
        {/* Notifications toggle — Web Push subscription state. Hidden
            entirely on browsers without ServiceWorker/PushManager
            (older iOS, anything below iOS 16.4 in standalone mode). */}
        {push.supported && (
          <>
            <TweakSection label="Notifications" />
            <TweakToggle
              label="Push alerts"
              value={push.subscribed}
              onChange={(v) => { v ? push.enable() : push.disable(); }}
            />
            {push.permission === 'denied' && (
              <div style={{fontSize: 11, color: '#a35', padding: '2px 0'}}>
                Browser permission denied — re-enable in site settings.
              </div>
            )}
            {push.subscribed && (
              <TweakButton label="Send test notification" onClick={push.test} secondary />
            )}
          </>
        )}
      </TweaksPanel>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
