import { Suspense, lazy, useEffect, useState, type ReactNode } from "react";
import { TenantProvider } from "@/components/tenant-provider";
import { QueryCachePersister } from "@/components/query-cache-persister";
import { OutboxSyncRunner } from "@/components/outbox-sync-runner";
import { OfflineMutationRunner } from "@/components/offline-mutation-runner";
import { ConflictNotice } from "@/components/conflict-notice";
import { ConflictBanner } from "@/components/conflict-banner";
import { ConflictResolutionDialogHost } from "@/components/conflict-resolution-dialog";
import { OfflinePassphraseGate } from "@/components/offline-passphrase-gate";
import { useLocationCollector } from "@/lib/geolocation";
import { CimbarRoot } from "@/components/cimbar";
import { AccessibilityRoot } from "@/components/accessibility/accessibility-root";
import { SkipToContent } from "@/components/accessibility/skip-to-content";
import { AccessibilityOnboardingNudge } from "@/components/accessibility/accessibility-onboarding-nudge";

function LocationCollectorRunner() {
  useLocationCollector();
  return null;
}

/**
 * Task #1448 — defers mounting non-essential post-auth runners until
 * `requestIdleCallback` (with a `setTimeout` fallback) fires. The
 * outbox sync runner, snapshot primer, location collector, and
 * accessibility onboarding nudge all start work as soon as they're
 * mounted, contending with the dashboard's own initial queries.
 * Holding them back one idle tick gives the destination route's
 * first paint clear bandwidth.
 */
function IdleMount({ children }: { children: ReactNode }) {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    type RICWindow = Window & {
      requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number;
      cancelIdleCallback?: (id: number) => void;
    };
    const w = window as RICWindow;
    if (typeof w.requestIdleCallback === "function") {
      const id = w.requestIdleCallback(() => setMounted(true), { timeout: 1500 });
      return () => w.cancelIdleCallback?.(id);
    }
    const t = window.setTimeout(() => setMounted(true), 1);
    return () => window.clearTimeout(t);
  }, []);
  if (!mounted) return null;
  return <>{children}</>;
}

// Snapshot priming sentinel (Task #390). Lazy-loaded so it doesn't
// inflate the authed-shell chunk for tenants that have already primed
// their caches in a previous session. The progress indicator has been
// moved into AppLayout's status bar (SnapshotProgressInline) so it
// appears as inline text rather than a floating pill.
const SnapshotPrimer = lazy(() =>
  import("@/components/snapshot-primer").then((m) => ({
    default: m.SnapshotPrimer,
  })),
);

/**
 * Single lazy-loaded "authed shell" boundary.
 *
 * Bundles every provider and sentinel that is only meaningful once
 * the user has signed in:
 *
 *   - {@link TenantProvider} — fetches `/api/tenants/me` and exposes
 *     the active tenant context.
 *   - {@link QueryCachePersister} — wires React Query to IndexedDB,
 *     scoped per (user, tenant).
 *   - {@link OutboxSyncRunner} — drains the offline mutation outbox.
 *   - {@link ConflictNotice} — surfaces the toast when a conflict is
 *     detected.
 *   - {@link ConflictResolutionDialogHost} — owns the side-by-side
 *     conflict resolution dialog.
 *   - {@link SnapshotPrimer} — primes detail-view caches for matters,
 *     contacts, firms, and interactions on first sign-in per (user,
 *     tenant) (Task #390).
 *
 * All of these used to be either eagerly imported (TenantProvider) or
 * lazy-loaded as separate chunks. Folding them into one chunk that's
 * only fetched on the first signed-in render means a not-yet-
 * authenticated visitor's cold load doesn't pay for any of them, and
 * a signed-in user only pays the cost once per device per release.
 *
 * The inner `<Suspense>` is a defence-in-depth fallback in case any
 * of the children themselves trigger a follow-on lazy import (the
 * snapshot sentinels are themselves lazy()).
 */
export function AuthedShell({ children }: { children: React.ReactNode }) {
  return (
    <TenantProvider>
      {/* QueryCachePersister stays outside the gate because it owns
          the unlock side-effect: when the gate calls
          unlockWithPassphrase, the persister must be mounted to react
          to the resulting state change and attach the DEK. */}
      <QueryCachePersister />
      <OfflinePassphraseGate>
        <CimbarRoot>
          <AccessibilityRoot>
            <SkipToContent />
            <Suspense fallback={null}>
              {/* ConflictNotice / ConflictResolutionDialogHost must
                  mount eagerly because they own toast surfaces that
                  could fire from a server-side conflict the moment
                  the dashboard's first round of queries returns. */}
              <ConflictNotice />
              <ConflictBanner />
              <ConflictResolutionDialogHost />
              {/* Task #1448 — everything below runs after the
                  destination route's first paint so its initial
                  queries don't fight the outbox / snapshot / nudges
                  for bandwidth and main-thread time. */}
              <IdleMount>
                <OutboxSyncRunner />
                <OfflineMutationRunner />
                <SnapshotPrimer />
                <LocationCollectorRunner />
                <AccessibilityOnboardingNudge />
              </IdleMount>
            </Suspense>
            {children}
          </AccessibilityRoot>
        </CimbarRoot>
      </OfflinePassphraseGate>
    </TenantProvider>
  );
}

export default AuthedShell;
