/**
 * App — BrowserRouter shell with react-router declarative routing.
 *
 * Routes:
 *   /             → redirect to /calendar
 *   /calendar     → CalendarShell
 *   /lists        → ListsIndex
 *   /lists/:listId → ListDetail
 *   /setup        → SetupPage (standalone wizard — no AppNav/BottomTabBar)
 *
 * Setup gate (Phase 12):
 *   On app load, GET /api/setup/status is fetched with staleTime 0 (always fresh).
 *   While loading: render nothing (prevent flash — mirrors the isAdmin loading gate).
 *   When setupComplete === false: redirect all non-/setup routes to /setup via Navigate.
 *   When setupComplete === true: normal app boot proceeds.
 *   The /setup route renders standalone — AppNav/BottomTabBar are NOT rendered on wizard.
 *
 * Layout:
 *   AppNav is rendered as a PERSISTENT sibling of <Routes> (outside any Route),
 *   so it survives route transitions (FIX 3). AppNav provides:
 *     - Phone (≤767px): 48px top bar (the header above the main content area)
 *     - Desktop (≥768px): 240px left sidebar with nav links + colour legend
 *
 *   BottomTabBar is also a sibling of <Routes> so the tab state remains consistent.
 *   On desktop (≥768px) BottomTabBar is hidden via CSS (FIX 4).
 *
 * Auth flow:
 *   /api/me is fetched once at the App level. While loading or on error the
 *   full-screen AuthSplash overlay is shown (CalendarShell's AuthSplash is
 *   positioned fixed with z-index so it covers AppNav too). AppNav renders
 *   with empty / partial data until auth resolves — this is fine because the
 *   AuthSplash overlay hides the AppNav during that window.
 *
 * navigateFallback ('/index.html') in vite.config.ts covers SPA deep-links to
 * /lists/* — the SW denylist only excludes /callback, /api/, and /health, so
 * /lists/* is served from cache correctly.
 *
 * Phase 5 additions:
 *   - PermissionDeniedBanner: shown below AppNav when OS permission revoked (D-10)
 *   - SettingsSheet: avatar-triggered bottom sheet with master notifications toggle (D-09)
 *
 * Phase 6 fix (FIX 3):
 *   - AppNav lifted from CalendarShell to this level so /lists keeps the nav chrome.
 *   - /api/me query shared so AppNav has user data on all routes.
 */

import { useState, useMemo, useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { CalendarShell } from './components/CalendarShell.js';
import { ListsIndex } from './routes/ListsIndex.js';
import { ListDetail } from './routes/ListDetail.js';
import { AdminPage } from './routes/AdminPage.js';
import { SetupPage } from './routes/SetupPage.js';
import { LoginPage } from './routes/LoginPage.js';
import { BottomTabBar } from './components/BottomTabBar.js';
import { AppNav } from './components/AppNav.js';
import { PushPermissionPrompt } from './components/PushPermissionPrompt.js';
import { PermissionDeniedBanner } from './components/PermissionDeniedBanner.js';
import { SetupBanner } from './components/SetupBanner.js';
import { SettingsSheet } from './components/SettingsSheet.js';
import { useIsPhone } from './hooks/useIsPhone.js';
import { fetchMe, fetchSetupStatus, fetchAuthMode } from './api/client.js';

/**
 * OidcRedirect — tiny helper that triggers a top-level navigation to /api/login.
 *
 * Used in the auth gate when localEnabled === false and oidcEnabled === true —
 * the OIDC-only mode that was the app's only auth path before Phase 19.
 * A top-level navigation (not a React Router navigate) is required because
 * /api/login responds with a 302 redirect to the external OIDC provider,
 * which browsers cannot follow as a fetch/XHR (T-07-04).
 */
function OidcRedirect() {
  // IN-01: navigate from an effect, not during render. React may invoke a
  // component body more than once (StrictMode double-invoke, concurrent
  // re-renders); side effects belong in useEffect.
  useEffect(() => {
    window.location.replace('/api/login');
  }, []);
  // IN-03: render a perceivable status (not an empty aria-hidden div) so the
  // transition is announced to screen-reader/keyboard users if the redirect is slow.
  return (
    <div
      role="status"
      style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        minHeight: '100vh',
        color: 'var(--color-text-muted, #6b7280)',
        fontSize: '0.95rem',
      }}
    >
      Redirecting to sign in…
    </div>
  );
}

export default function App() {
  const [settingsOpen, setSettingsOpen] = useState(false);
  const phone = useIsPhone();

  // Setup status query — staleTime 0 so the wizard gate is always fresh (D-10 spirit).
  // Must be fetched before rendering any authenticated route to gate the app on /setup.
  // Uses the pre-auth /api/setup/status endpoint — no OIDC session required.
  const setupQuery = useQuery({
    queryKey: ['setupStatus'],
    queryFn: fetchSetupStatus,
    retry: false,
    staleTime: 0,
  });

  // Fetch current user once at the app shell level so AppNav has member data on
  // ALL routes. This is the same query key (['me']) used by CalendarShell, so
  // TanStack Query deduplicates the request — no double fetch.
  //
  // gap 6 (mechanism (ii) — ['me'] staleness, NOT a linking gap; see SUMMARY):
  // the first-login claim in upsertUser (auth/user.ts) preserves the same users.id,
  // so the wizard-stored CalDAV credential stays linked → needsProviderSetup is
  // correctly FALSE in the DB after the operator authenticates post-wizard. The bug
  // was purely client-cache: a ['me'] entry populated BEFORE the claim (e.g. an
  // earlier pre-auth visit) served a stale needsProviderSetup=true for up to 5
  // minutes, so the "Set up your calendar" banner kept showing. Set staleTime 0 on
  // the boot ['me'] query so the authenticated app shell always refetches member
  // status on entry — needsProviderSetup then reflects the just-claimed credential.
  const meQuery = useQuery({
    queryKey: ['me'],
    queryFn: fetchMe,
    retry: false,
    staleTime: 0,
  });

  // Auth mode query — fetched pre-auth (no session required).
  // Determines whether to show /login (localEnabled) or OIDC redirect (!localEnabled && oidcEnabled).
  // staleTime 60s: auth mode changes rarely; re-fetches on new tab/focus.
  const authModeQuery = useQuery({
    queryKey: ['authMode'],
    queryFn: fetchAuthMode,
    retry: false,
    staleTime: 60_000,
  });

  // isAdmin from /api/me — used for UX gating only (D-03). Server enforces 403.
  // While meQuery is loading, isAdmin is false/undefined → admin route redirects
  // (loading gate: no flash of admin content for non-admins).
  const isAdmin = meQuery.data?.user.isAdmin ?? false;

  // Derive members for AppNav from the shared /api/me response
  const members = useMemo(() => {
    if (!meQuery.data?.user) return [];
    return [
      {
        id: String(meQuery.data.user.id),
        name: meQuery.data.user.displayName ?? 'Member',
        color: meQuery.data.user.color,
      },
    ];
  }, [meQuery.data]);

  // Outer layout: phone = column, desktop = row (AppNav sidebar + content)
  const outerStyle: React.CSSProperties = {
    height: '100dvh',
    display: 'flex',
    flexDirection: phone ? 'column' : 'row',
    background: 'var(--color-surface)',
    color: 'var(--color-text-primary)',
    fontFamily: 'var(--font-family-base)',
    overflow: 'hidden',
  };

  // Content area: fills remaining space next to / below AppNav
  const contentStyle: React.CSSProperties = {
    flex: 1,
    minWidth: 0,
    minHeight: 0,
    display: 'flex',
    flexDirection: 'column',
    overflow: 'hidden',
    position: 'relative',
    // Phone-only: reserve space for the fixed BottomTabBar so content is not occluded
    ...(phone ? { paddingBottom: 'var(--bottom-chrome-h)' } : {}),
  };

  // Setup gate: while setup status is loading, render nothing (prevent flash).
  // Mirrors the isAdmin loading-gate pattern for the admin route.
  const setupComplete = setupQuery.data?.setupComplete;
  const setupLoading = setupQuery.isLoading;

  return (
    <BrowserRouter>
      <Routes>
        {/* /setup route — standalone wizard, no AppNav/BottomTabBar shell (UI-SPEC §Routing).
            Reverse gate (gap 5, T-12-04): once setup is complete the wizard must NOT re-mount.
            - While setupQuery is loading → render the no-flash placeholder (no wizard before
              status resolves), mirroring the `*`-route loading gate below.
            - setupComplete === true → render SetupPage with alreadyLocked → Surface 8
              ("Setup already complete"), keeping the operator on /setup with a terminal surface.
            - setupComplete === false (or undefined post-load) → active wizard, as before.
            The backend already 423s setup mutations; this is the matching frontend reverse-gate. */}
        <Route
          path="/setup"
          element={
            setupLoading ? (
              <div aria-hidden="true" />
            ) : setupComplete === true ? (
              <SetupPage alreadyLocked={true} />
            ) : (
              <SetupPage />
            )
          }
        />

        {/* /login route — standalone login page, no AppNav/BottomTabBar shell (UI-SPEC §Surface 1).
            Phase 19: shown when the user is unauthenticated AND localEnabled === true.
            The route itself always renders LoginPage (authMode gating is in the `*` route gate below).
            LoginPage receives authMode so it can show the optional OIDC button when oidcEnabled. */}
        <Route path="/login" element={<LoginPage authMode={authModeQuery.data} />} />

        {/* All other routes are gated on setup completion */}
        <Route
          path="*"
          element={
            // While setupQuery is loading: render nothing (no flash before redirect)
            setupLoading ? (
              <div aria-hidden="true" />
            ) : setupComplete === false ? (
              // Not configured: full-app redirect to /setup (no nav shell rendered)
              <Navigate to="/setup" replace />
            ) : meQuery.isError && !meQuery.isLoading && authModeQuery.data?.localEnabled ? (
              // Unauthenticated + localEnabled: redirect to /login
              <Navigate to="/login" replace />
            ) : meQuery.isError &&
              !meQuery.isLoading &&
              !authModeQuery.data?.localEnabled &&
              authModeQuery.data?.oidcEnabled ? (
              // Unauthenticated + OIDC-only mode: top-level redirect to /api/login (today's behavior)
              // Use a render side-effect via useEffect isn't available here; use a helper element
              <OidcRedirect />
            ) : (
              // Setup complete: render the normal authenticated app shell
              <>
                {/* Permission-denied banner: shown when OS revoked and user had notifications on (D-10) */}
                <PermissionDeniedBanner />

                <div style={outerStyle}>
                  {/* Persistent AppNav — phone: top bar; desktop: left sidebar.
                      Renders on ALL routes so nav chrome survives route transitions (FIX 3). */}
                  <AppNav
                    members={members}
                    currentUserColor={meQuery.data?.user.color}
                    currentUserName={meQuery.data?.user.displayName ?? undefined}
                    onOpenSettings={() => setSettingsOpen(true)}
                    isAdmin={isAdmin}
                  />

                  {/* Main content area — all routes render here */}
                  <div style={contentStyle}>
                    {/* SetupBanner: shown above content when needsProviderSetup=true (D-07).
                        Reads from the shared ['me'] query — no additional fetch. */}
                    <SetupBanner />

                    <Routes>
                      <Route path="/" element={<Navigate to="/calendar" replace />} />
                      <Route path="/calendar" element={<CalendarShell />} />
                      <Route path="/lists" element={<ListsIndex />} />
                      <Route path="/lists/:listId" element={<ListDetail />} />
                      {/* /admin route: gated by isAdmin (UX, D-03). Server enforces 403 on all /api/admin/* */}
                      {/* Loading gate: show nothing while meQuery is fetching (prevents flash).
                          Once resolved: isAdmin → AdminPage; else → redirect to /calendar. */}
                      <Route
                        path="/admin"
                        element={
                          meQuery.isLoading ? (
                            <div aria-hidden="true" />
                          ) : isAdmin ? (
                            <AdminPage />
                          ) : (
                            <Navigate to="/calendar" replace />
                          )
                        }
                      />
                    </Routes>
                  </div>
                </div>

                {/* BottomTabBar — phone-only (hidden ≥768px via CSS, FIX 4) */}
                <BottomTabBar isAdmin={isAdmin} />

                {/* Post-install permission prompt (D-08): renders only when isInstalled() is true
                    and Notification.permission === 'default' and not dismissed */}
                <PushPermissionPrompt />

                {/* Settings sheet — master notifications toggle (D-09), opened from avatar */}
                <SettingsSheet isOpen={settingsOpen} onClose={() => setSettingsOpen(false)} />
              </>
            )
          }
        />
      </Routes>
    </BrowserRouter>
  );
}
