/* Dispatch — app shell
 * Sidebar, header + dark toggle, and DemoApp which renders the tab component for the selected sidebar item.
 * Loaded by index.html as a <script type="text/babel"> module. Shared symbols
 * (React hook aliases, api, cn, UI primitives, helpers) live in lib.jsx
 * and are declared once there; other modules use them without redeclaring. */
// ── Dashboard shell (sidebar + header), shadcn-style ─────────────────────
// Lane icons mirror the original app's LaneIcon set.
function NavIcon({ name }) {
  const paths = {
    hub: <><path d="M3 11l9-7 9 7" /><path d="M5 10v10h14V10" /></>,
    helpdesk: <><circle cx="12" cy="12" r="9" /><path d="M9 10a3 3 0 1 1 4.5 2.6c-.9.4-1.5 1-1.5 2" /><circle cx="12" cy="17" r="0.6" fill="currentColor" /></>,
    netadmin: <><rect x="3" y="4" width="18" height="6" rx="1" /><rect x="3" y="14" width="18" height="6" rx="1" /><circle cx="6" cy="7" r="0.8" fill="currentColor" /><circle cx="6" cy="17" r="0.8" fill="currentColor" /></>,
    architect: <><path d="M3 21V8l9-5 9 5v13" /><path d="M9 21v-7h6v7" /></>,
    dev: <><path d="M9 17l-5-5 5-5" /><path d="M15 7l5 5-5 5" /></>,
    qa: <><path d="M9 12l2.5 2.5L17 9" /><circle cx="12" cy="12" r="9" /></>,
    board: <><rect x="3" y="4" width="5" height="16" rx="1" /><rect x="10" y="4" width="5" height="10" rx="1" /><rect x="17" y="4" width="4" height="13" rx="1" /></>,
    reporting: <><path d="M3 3v18h18" /><rect x="7" y="12" width="3" height="6" /><rect x="12" y="8" width="3" height="10" /><rect x="17" y="5" width="3" height="13" /></>,
    slack: <><path d="M9 3v18M15 3v18M3 9h18M3 15h18" /></>,
    integrations: <><circle cx="12" cy="12" r="3" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2" /></>,
    settings: <><line x1="4" y1="8" x2="20" y2="8" /><line x1="4" y1="16" x2="20" y2="16" /><circle cx="9" cy="8" r="2.5" /><circle cx="15" cy="16" r="2.5" /></>,
    sun: <><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19" /></>,
    moon: <><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></>,
    bell: <><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" /><path d="M13.7 21a2 2 0 0 1-3.4 0" /></>,
    logout: <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><path d="M16 17l5-5-5-5" /><path d="M21 12H9" /></>,
  };
  return <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">{paths[name]}</svg>;
}

// The six lanes + the original rail's other buttons, with the same names.
const LANES = [
  { id: 'qa', label: 'My Tasks', icon: 'qa' },
  { id: 'hub', label: 'Hub', icon: 'hub' },
  { id: 'helpdesk', label: 'Help Desk', icon: 'helpdesk' },
  { id: 'netadmin', label: 'Net Admin', icon: 'netadmin' },
  { id: 'architect', label: 'Architects', icon: 'architect' },
  { id: 'dev', label: 'Developers', icon: 'dev' },
];
const TOOLS = [
  { id: 'qa-board', label: 'QA Board', icon: 'board' },
  { id: 'reporting', label: 'Reporting', icon: 'reporting' },
  { id: 'slack-inbox', label: 'Slack inbox', icon: 'slack' },
  { id: 'integrations', label: 'Integrations', icon: 'integrations' },
  { id: 'settings', label: 'Settings', icon: 'settings' },
];
const ALL_NAV = [...LANES, ...TOOLS];
const THEME_KEY = 'dispatch-demo-theme';
// Sections whose page has a collapsible overview card — only these show the
// header Hide/Show toggle. (Settings + Integrations have no overview.)
const SECTIONS_WITH_OVERVIEW = new Set(['hub', 'qa', 'helpdesk', 'netadmin', 'architect', 'dev', 'qa-board', 'reporting', 'slack-inbox']);

function Sidebar({ section, setSection, user, dark, setDark, allowed }) {
  // `allowed` = null → show every module (admins / unrestricted preview);
  // otherwise a Set of module keys (section ids) the user may see.
  const canSee = (id) => allowed === null || allowed.has(id);
  // Deep-sea teal rail from the original app (--deep-sea #013e46). Sticky, full
  // height, so the user card / notifications stay anchored at the bottom-left.
  const [notifOpen, setNotifOpen] = useState(false);
  const [userOpen, setUserOpen] = useState(false);
  const [notifs, setNotifs] = useState([]);
  const [unread, setUnread] = useState(0);
  const footRef = useRef(null);

  useEffect(() => {
    let cancelled = false;
    api.fetchNotifications().then(r => { if (!cancelled) { setNotifs(r.items || []); setUnread(r.unreadCount ?? 0); } }).catch(() => {});
    return () => { cancelled = true; };
  }, []);
  useEffect(() => {
    if (!notifOpen && !userOpen) return;
    const onDoc = (e) => { if (footRef.current && !footRef.current.contains(e.target)) { setNotifOpen(false); setUserOpen(false); } };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [notifOpen, userOpen]);

  async function markAllRead() {
    const ids = notifs.filter(n => n.unread).map(n => n.id);
    if (ids.length) { try { await api.markNotificationsRead(ids); } catch {} }
    setNotifs(prev => prev.map(n => ({ ...n, unread: false }))); setUnread(0);
  }
  async function signOut() {
    try { await api.signOut(); } catch {}
    // Clear the mirrored session identity so a stale login can't linger after
    // the server session is gone; the reload then lands on the SignInGate.
    try { api.setCurrentUserEmail(''); api.setCurrentUserName(''); api.setCurrentUserLogin(''); } catch {}
    window.__DISPATCH_AUTHED__ = false;
    window.location.reload();
  }

  const Item = ({ it }) => {
    const active = section === it.id;
    return (
      <button onClick={() => setSection(it.id)}
        className={cn('relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors',
          active ? 'bg-[#a9cf38]/15 text-white' : 'text-[#89a7ad] hover:bg-white/5 hover:text-white')}>
        {active && <span className="absolute inset-y-1.5 left-0 w-0.5 rounded-r bg-[#a9cf38]" />}
        <NavIcon name={it.icon} /> {it.label}
      </button>
    );
  };

  return (
    <aside className="sticky top-0 hidden h-screen w-64 shrink-0 flex-col bg-[#013e46] text-[#c9dadf] dark:border-r dark:border-border dark:bg-background dark:text-muted-foreground md:flex">
      <div className="flex h-14 items-center justify-center border-b border-white/10 px-4">
        {/* White wordmark, centered on the teal rail — no chip, both themes. */}
        <img src="assets/dispatch-logo-w.png" alt="Dispatch" className="h-[50px] w-auto" />
      </div>
      <div className="flex-1 space-y-1 overflow-y-auto p-3">
        {LANES.filter(l => canSee(l.id)).map(l => <Item key={l.id} it={l} />)}
        <div className="my-2 border-t border-white/10" />
        {TOOLS.filter(t => canSee(t.id)).map(t => <Item key={t.id} it={t} />)}
      </div>

      {/* Anchored footer: notifications + user menu, each a floating popover. */}
      <div ref={footRef} className="relative border-t border-white/10 p-3">
        <button onClick={() => { setUserOpen(false); setNotifOpen(o => !o); }}
          className="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-[#89a7ad] hover:bg-white/5 hover:text-white">
          <NavIcon name="bell" /> Notifications
          {unread > 0 && <span className="ml-auto rounded-full bg-[#a9cf38] px-1.5 text-xs font-semibold text-[#013e46]">{unread}</span>}
        </button>
        {notifOpen && (
          <div className="absolute bottom-2 left-full z-50 ml-2 w-80 overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-lg">
            <div className="flex items-center justify-between border-b p-3">
              <span className="text-sm font-semibold">Notifications</span>
              <button className="text-xs text-muted-foreground hover:text-foreground" onClick={markAllRead}>Mark all read</button>
            </div>
            <div className="max-h-[60vh] overflow-y-auto">
              {notifs.length === 0 && <div className="p-4 text-sm text-muted-foreground">No notifications.</div>}
              {notifs.map(n => (
                <div key={n.id} className={cn('border-b p-3 last:border-0', n.unread && 'bg-muted/40')}>
                  <div className="flex items-baseline justify-between gap-2"><span className="text-sm font-medium">{n.title}</span><span className="flex-shrink-0 text-xs text-muted-foreground">{n.when || relAge(n.createdAt)}</span></div>
                  {n.body && <div className="mt-0.5 text-sm text-muted-foreground">{n.body}</div>}
                </div>
              ))}
            </div>
          </div>
        )}

        <button onClick={() => { setNotifOpen(false); setUserOpen(o => !o); }}
          className="mt-1 flex w-full items-center gap-2 rounded-md px-3 py-2 text-left hover:bg-white/5">
          <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-white/10 text-xs font-semibold text-white">{user.initials}</div>
          <div className="min-w-0"><div className="truncate text-sm font-medium text-white">{user.name}</div><div className="truncate text-xs text-[#89a7ad]">{user.email}</div></div>
        </button>
        {userOpen && (
          <div className="absolute bottom-2 left-full z-50 ml-2 w-64 overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-lg">
            <div className="border-b p-3">
              <div className="truncate text-sm font-medium">{user.name}</div>
              <div className="truncate text-xs text-muted-foreground">{user.email}</div>
            </div>
            <div className="p-1">
              <button onClick={() => setDark(d => !d)} className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"><NavIcon name={dark ? 'sun' : 'moon'} /> {dark ? 'Light mode' : 'Dark mode'}</button>
              <button onClick={signOut} className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm text-destructive hover:bg-accent"><NavIcon name="logout" /> Sign out</button>
            </div>
          </div>
        )}
      </div>
    </aside>
  );
}

function DemoApp() {
  const [section, setSection] = useState('qa'); // My Tasks is the default landing page
  // Persist the theme choice so it survives reloads (like the original app).
  const [dark, setDark] = useState(() => {
    try { return localStorage.getItem(THEME_KEY) === 'dark'; } catch { return false; }
  });
  useEffect(() => {
    document.documentElement.classList.toggle('dark', dark);
    try { localStorage.setItem(THEME_KEY, dark ? 'dark' : 'light'); } catch {}
  }, [dark]);

  // Per-user module access (see docs/access-control.md). null while loading.
  const [access, setAccess] = useState(null);
  useEffect(() => {
    let cancelled = false;
    const load = () => api.fetchMyAccess().then(a => { if (!cancelled) setAccess(a); }).catch(() => { if (!cancelled) setAccess({ identified: false }); });
    load();
    window.addEventListener('dispatch:current-user-changed', load);
    return () => { cancelled = true; window.removeEventListener('dispatch:current-user-changed', load); };
  }, []);
  // Admins + unidentified/unknown users see everything (null). Known non-admins
  // see only their granted modules + My Tasks.
  const allowed = (!access || !access.identified || access.isAdmin)
    ? null
    : new Set([...(access.moduleKeys || []), 'qa']);
  // If the current section isn't allowed, fall back to My Tasks.
  useEffect(() => {
    if (allowed && !allowed.has(section)) setSection('qa');
  }, [allowed, section]);

  // The overview Hide/Show toggle lives in the top bar; it's synced across
  // pages via useOverviewOpen. Only shown on pages that have an overview card.
  const [overviewOpen, setOverviewOpen] = useOverviewOpen();

  const name = api.getCurrentUserName() || 'Guest';
  const email = api.getCurrentUserEmail() || 'not signed in';
  const initials = (name.match(/\b\w/g) || ['?']).slice(0, 2).join('').toUpperCase();
  const title = (ALL_NAV.find(l => l.id === section) || {}).label || 'Dispatch';
  return (
    <div className="flex min-h-screen bg-muted/30">
      <Sidebar section={section} setSection={setSection} user={{ name, email, initials }} dark={dark} setDark={setDark} allowed={allowed} />
      <div className="flex min-w-0 flex-1 flex-col">
        <header className="sticky top-0 z-10 flex h-14 items-center gap-3 border-b bg-background px-4">
          <h1 className="text-sm font-semibold">{title}</h1>
          {SECTIONS_WITH_OVERVIEW.has(section) && <CollapseToggle open={overviewOpen} onClick={() => setOverviewOpen(o => !o)} />}
          <div className="ml-auto flex items-center gap-2">
            <Badge variant="outline">shadcn/ui + Tailwind</Badge>
            <Button variant="ghost" size="icon" aria-label="Toggle dark mode" title="Toggle dark mode" onClick={() => setDark(d => !d)}>
              <NavIcon name={dark ? 'sun' : 'moon'} />
            </Button>
          </div>
        </header>
        <main className="flex-1 overflow-y-auto p-6">
          {section === 'helpdesk' ? <HelpDesk />
            : section === 'hub' ? <Hub />
            : section === 'qa' ? <MyTasks />
            : section === 'qa-board' ? <QaBoard />
            : section === 'reporting' ? <Reporting />
            : section === 'slack-inbox' ? <SlackInbox />
            : section === 'settings' ? <Settings />
            : section === 'integrations' ? <Integrations />
            : section === 'dev' ? <DevBoard />
            : ['netadmin', 'architect'].includes(section) ? <LanePage lane={section} />
            : (
              <div className="rounded-xl border border-dashed p-10 text-center text-sm text-muted-foreground">
                “{title}” isn't part of the demo yet — it exists in the full app.
              </div>
            )}
        </main>
      </div>
    </div>
  );
}

// Full-screen gate shown when there's no Google session. Ported from the
// retired old design's SignInGate, restyled in shadcn/Tailwind. The bearer
// token is still accepted by the backend (machine callers, scripts) — this
// gate is only for browser users. Sessions last 30 days.
function SignInGate({ denied, error }) {
  return (
    <div className="fixed inset-0 z-50 grid place-items-center bg-muted/40 p-4">
      <div className="w-full max-w-sm rounded-xl border bg-card p-8 text-center shadow-lg">
        {/* Teal logo on the light card; white logo when the card is dark. */}
        <img src="assets/dispatch-logo.png" alt="Dispatch" className="mx-auto mb-4 block h-11 w-auto dark:hidden" />
        <img src="assets/dispatch-logo-w.png" alt="Dispatch" className="mx-auto mb-4 hidden h-11 w-auto dark:block" />
        <p className="mt-1 text-sm text-muted-foreground">UCPM IT Hub. Sign in with your <code>@ucpm.com</code> Google account.</p>
        {denied && (
          <div className="mt-4 rounded-md bg-destructive/10 px-3 py-2 text-left text-xs text-destructive">
            <strong>Sign-in denied.</strong> Your account isn't on the allowed list. Make sure you're signed into your <code>@ucpm.com</code> Google account.
          </div>
        )}
        {error && !denied && (
          <div className="mt-4 rounded-md bg-amber-500/10 px-3 py-2 text-left text-xs text-amber-700 dark:text-amber-400">
            <strong>Sign-in didn't complete.</strong> Something hiccuped on the way back from Google — try again. If it keeps happening, clear your cookies for this site and retry.
          </div>
        )}
        <a href={api.googleSignInUrl()} className="mt-6 inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90">
          <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
            <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
            <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
            <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
            <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
          </svg>
          Sign in with Google
        </a>
        <p className="mt-4 text-[11px] leading-relaxed text-muted-foreground">By signing in you agree to internal-tool usage policies. Sessions last 30 days.</p>
      </div>
    </div>
  );
}

// Auth gate wrapper. Probes /api/auth/me on load: 'loading' → splash,
// 'authed' → the app, 'unauthed' → SignInGate. Mirrors the retired old
// design's behavior so browser users can't enter as an anonymous "Guest".
function Root() {
  const [auth, setAuth] = useState({ status: 'loading', denied: false, error: false });

  // Match the persisted theme on the gate/splash too (DemoApp isn't mounted
  // yet, so it can't apply the dark class here).
  useEffect(() => {
    try { document.documentElement.classList.toggle('dark', localStorage.getItem(THEME_KEY) === 'dark'); } catch {}
  }, []);

  useEffect(() => {
    let cancelled = false;
    // Pick up "?signin=denied|error" left by the OAuth callback, then strip it
    // from the URL. denied = domain check failed; error = a recoverable OAuth
    // hiccup (expired state cookie, network) — just ask the user to retry.
    let denied = false;
    let oauthError = false;
    try {
      const url = new URL(window.location.href);
      const v = url.searchParams.get('signin');
      if (v === 'denied') denied = true;
      if (v === 'error') oauthError = true;
      if (v) {
        url.searchParams.delete('signin');
        window.history.replaceState(null, '', url.toString());
      }
    } catch {}
    (async () => {
      try {
        const me = await api.fetchMe();
        if (cancelled) return;
        if (me && me.authenticated) {
          // Mirror the verified session identity into localStorage so the
          // existing identity-aware code paths keep working unchanged.
          api.setCurrentUserEmail(me.email || '');
          api.setCurrentUserName(me.name || '');
          if (me.githubLogin) api.setCurrentUserLogin(me.githubLogin);
          window.__DISPATCH_AUTHED__ = true;
          window.dispatchEvent(new CustomEvent('dispatch:current-user-changed'));
          setAuth({ status: 'authed', denied: false, error: false });
        } else {
          setAuth({ status: 'unauthed', denied, error: oauthError });
        }
      } catch {
        // /api/auth/me unreachable — treat as unauthed so we never silently
        // drop a browser user into the app without a verified session.
        if (!cancelled) setAuth({ status: 'unauthed', denied, error: oauthError });
      }
    })();
    return () => { cancelled = true; };
  }, []);

  if (auth.status === 'loading') {
    return (
      <div className="fixed inset-0 grid place-items-center bg-muted/40">
        <div className="animate-pulse">
          <img src="assets/dispatch-favicon.png" alt="Dispatch" className="block h-32 w-32 object-contain dark:hidden" />
          <img src="assets/dispatch-favicon-w.png" alt="Dispatch" className="hidden h-32 w-32 object-contain dark:block" />
        </div>
      </div>
    );
  }
  if (auth.status === 'unauthed') return <SignInGate denied={auth.denied} error={auth.error} />;
  return <DemoApp />;
}

ReactDOM.createRoot(document.getElementById('root')).render(<Root />);
