import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { UsernamePromptDialog } from "@/components/account/username-prompt-dialog";
import { AppSidebar } from "@/components/app-sidebar";
import { TourRunner } from "@/components/tours/tour-runner";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { getActiveOrgId } from "@/lib/active-org";
import { auth } from "@/lib/auth";
import { isCurrentUserSuperAdmin, isSuperAdmin } from "@/lib/auth/super-admin";
import { getOrgBillingInfo } from "@/lib/billing/status";
import { SiteProvider } from "@/lib/keywords/site-context";
import { ThemeLabelsProvider } from "@/lib/keywords/theme-context";
import { getActivationSnapshot } from "@/lib/onboarding/activation/snapshot";
import { requiredComplete } from "@/lib/onboarding/activation/steps";
import {
  DEFER_COOKIE,
  DEFER_COOKIE_VALUE,
  shouldEnterWizard,
} from "@/lib/onboarding/wizard-gate";
import { getSitesForActiveOrg, hasFullSiteAccess } from "@/lib/sites/access";
import { getActiveSiteId } from "@/lib/sites/active-site";
import { shouldMountTourRunner } from "@/lib/tours/mount-gate";
import { getTourProgress } from "@/lib/tours/progress";

export default async function AppLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const reqHeaders = await headers();
  // Auth gate FIRST — before any org-scoped resolution. The whole (app) route
  // group is authenticated (login/signup/onboarding/billing-locked live outside
  // it), so gating here covers every app page at once. Crucially this must run
  // before getActiveOrgId()/getSitesForActiveOrg(), which HARD-THROW
  // ("No active organization in session") when there's no session — that throw
  // previously 500'd logged-out visitors instead of bouncing them to /login.
  // Session lue SANS disableCookieCache (coût par-page) : ce redirect est un
  // confort de routage UX, pas un gate d'autorisation — les vrais gates
  // (layout (superadmin), assertSuperPermission/assertSysPermission) relisent
  // frais. Un cache de 5 min ici ne fait au pire que retarder un rebond vers
  // /admin, jamais fuiter un accès.
  const session = await auth.api.getSession({ headers: reqHeaders });
  if (!session?.user?.id) {
    redirect("/login");
  }
  // While a super-admin impersonates a target, the live session is the target's
  // (with session.impersonatedBy set). Calculé ICI, juste après la session, car
  // le gate du wizard plus bas en dépend — les popups username/first-site le
  // réutilisent tel quel.
  const isImpersonating = !!(
    session.session as { impersonatedBy?: string | null }
  ).impersonatedBy;
  // Opérateurs SYS (spec RBAC §2.bis) : leur SEULE surface est /admin — ni org,
  // ni site, ni app cliente. Redirect INCONDITIONNEL, AVANT le gate onboarding
  // (un compte SYS n'a pas d'onboarding) et AVANT toute résolution d'org (un
  // compte SYS est orgless par construction — le self-heal de getActiveOrgId ne
  // doit jamais tourner pour lui). Admin SYS = email ∈ SUPER_ADMIN_EMAILS ;
  // Staff SYS = user.role "staff". Aucun accès dual : un Admin SYS ne voit
  // jamais l'app.
  // role "admin" (Admin SYS) ET "staff" (Staff SYS) sont tous deux orgless :
  // on redirige sur le rôle SYS directement (pas seulement isSuperAdmin(email)),
  // pour qu'un compte SYS ne puisse jamais atteindre getActiveOrgId (qui throw).
  const sysRole = (session.user as { role?: string | null }).role;
  if (
    sysRole === "staff" ||
    sysRole === "admin" ||
    isSuperAdmin(session.user.email)
  ) {
    redirect("/admin");
  }
  // Le gate onboarding NE VIT PLUS ICI : il lisait `onboardingCompletedAt`
  // avant toute résolution d'org, alors que le nouveau critère est dérivé de
  // l'état réel du produit (donc org-scopé). Il descend plus bas, après la
  // résolution org/sites et le gate de facturation — voir « Gate du wizard ».
  //
  // Ce qu'il faut RETENIR de son ancienne position : il tournait avant toute
  // résolution d'org, et c'est ce qui protégeait l'utilisateur fraîchement créé
  // par lien magique — le hook `session.create` peut courir avant la fin du
  // bootstrap d'org, il existe donc quelques millisecondes pendant lesquelles
  // la session n'a pas d'org active. `getActiveOrgId()` se self-heal dans ce
  // cas, MAIS lève quand la re-résolution ne trouve rien (active-org.ts:20,22),
  // et ce throw remonterait en garde d'erreur sur TOUTES les pages de l'app.
  // On sonde donc d'abord, et on renvoie vers /onboarding — la seule page qui
  // sait vivre sans org (resolveOnboardingOrgId).
  //
  // La sonde n'est payée QUE si la session est sans org active : sinon un
  // incident base serait maquillé en « pas d'org » et enverrait tout le monde
  // au wizard. `getActiveOrgId` étant `cache()`-ée, le Promise.all qui suit ne
  // re-résout rien.
  if (!session.session.activeOrganizationId) {
    let orgless = false;
    try {
      await getActiveOrgId();
    } catch (err) {
      console.error("[app-layout] no resolvable org for session:", err);
      orgless = true;
    }
    // redirect() lève en interne : IMPÉRATIVEMENT hors du try, qui l'avalerait.
    if (orgless) redirect("/onboarding");
  }
  // Authenticated from here on. Resolve org-scoped data in parallel.
  const [activeOrgId, sites, orgs] = await Promise.all([
    getActiveOrgId(),
    getSitesForActiveOrg(),
    auth.api.listOrganizations({ headers: reqHeaders }),
  ]);
  // Super-admin gate: drives both the billing bypass below and the god-mode-only
  // nav entry (global Users management) surfaced in the sidebar.
  const superAdmin = await isCurrentUserSuperAdmin();
  // Billing gate: an org whose trial lapsed without a live subscription is
  // locked out of the whole app shell — super-admins bypass for support.
  // The plan also drives the sidebar upsell (hide "Upgrade to Pro" once Pro+).
  let currentPlan: string | null = null;
  if (session?.user?.id && activeOrgId) {
    const billing = await getOrgBillingInfo(activeOrgId);
    currentPlan = billing.plan;
    if (billing.status === "expired" && !superAdmin) {
      redirect("/billing-locked");
    }
  }
  // Username prompt: email/password signup makes a username required, but
  // magic-link sign-ups and legacy accounts land here with `username = null`. The
  // product intent is that EVERYONE has one, so prompt before using the app.
  // `username` is a plugin field on the session user (returned by getSession but
  // not on the base type) — read it defensively, same as the account page.
  const sessionUsername = (session.user as { username?: string | null })
    .username;
  // Suppress the username popup while impersonating (isImpersonating est
  // calculé plus haut, juste après la session) — an admin doing support
  // shouldn't be forced through the target's identity flow — and surface the
  // impersonation banner instead.
  const needsUsername = !isImpersonating && !sessionUsername;
  // La popup « premier site » A DISPARU, et ce n'est pas un oubli : le gate du
  // wizard (plus bas) renvoie déjà vers /onboarding tant que `create-site` est
  // faux, et l'étape 1 du parcours porte la même création — en mieux, puisque
  // le pays cible y est obligatoire. Garder les deux, c'était deux surfaces de
  // création à maintenir et une modale qui s'ouvrait par-dessus une redirection
  // qui allait de toute façon avoir lieu. `hasFullSiteAccess` reste lu ici : il
  // commande le gate du wizard.
  const fullSiteAccess = await hasFullSiteAccess();
  // Le pendant "membre" (org sans site partagé → explication au lieu de pages
  // vides) N'EST PAS gaté ici : la racine du groupe (app) couvrirait aussi
  // settings/support, soit exactement les pages dont un membre bloqué a besoin
  // pour se débloquer. Il vit dans les layouts de section site-scoped, voir
  // src/lib/sites/site-scoped-gate.
  const initialActiveSiteId = await getActiveSiteId(sites.map((s) => s.id));
  // Checklist d'activation : dérivée ICI, après le gate de facturation (elle
  // dépend de l'org active) et jamais avant — un visiteur redirigé ne doit pas
  // payer la dérivation. Fail-silent : `null` fait simplement disparaître la
  // carte de la sidebar.
  const activation = await getActivationSnapshot();
  // Gate du wizard — dérivé, pas tamponné. Tant que les étapes REQUISES
  // manquent, le parcours s'impose ; l'état « onboarding terminé mais compte
  // vide » n'est plus représentable. Conséquence VOULUE : un site fraîchement
  // créé remet toutes ses étapes de données à faux, donc le parcours se rouvre
  // à chaque création de site, pas seulement à la première.
  //
  // Le cookie de report est lu AVANT la redirection : c'est la seule sortie
  // d'un utilisateur qui a des étapes en attente et veut naviguer quand même.
  // Il est posé par /onboarding/defer, jamais par le client.
  //
  // `activation` à `null` = la dérivation a échoué (et elle a déjà journalisé).
  // On laisse alors passer : un incident de lecture ne doit pas se traduire par
  // une redirection en boucle. `activation.dismissed`, en revanche, ne dit RIEN
  // ici — masquer la carte de la barre latérale ne désactive pas le parcours.
  const deferred =
    (await cookies()).get(DEFER_COOKIE)?.value === DEFER_COOKIE_VALUE;
  if (
    activation &&
    shouldEnterWizard({
      requiredDone: requiredComplete(activation.flags),
      deferred,
      canCreateSite: fullSiteAccess,
      isImpersonating,
    })
  ) {
    redirect("/onboarding");
  }
  // « Aucun tour tant que la popup username, NON FERMABLE, est montée » est une
  // interdiction de la spec. Elle vit dans `@/lib/tours/mount-gate`, pur et
  // testé, et non dans un ternaire d'ici : une règle écrite dans un `.tsx` est
  // une règle que personne ne vérifie — la supprimer ne rougissait nulle part.
  const mountTour = shouldMountTourRunner({
    usernamePromptOpen: needsUsername,
  });
  // Progression du tutoriel, lue APRÈS tous les gates (un visiteur redirigé ne
  // la paye pas), et sous la MÊME décision — pour ÉVITER UNE REQUÊTE inutile,
  // et pour rien d'autre : ce `{}` ne protège de rien, il rend même un tour
  // PLUS probable, puisqu'une progression vide est celle d'un utilisateur qui
  // n'a encore rien vu. Ce qui garde, et seul, c'est le montage conditionnel de
  // `<TourRunner>` plus bas : sans runner, pas d'overlay par-dessus une popup
  // qu'on ne peut pas fermer.
  const tourProgressMap = mountTour
    ? await getTourProgress(session.user.id)
    : {};
  const user = session?.user
    ? {
        name: session.user.name,
        email: session.user.email,
        avatar: session.user.image ?? "",
      }
    : undefined;
  return (
    <SiteProvider
      initialSites={sites}
      initialActiveSiteId={initialActiveSiteId}
    >
      <ThemeLabelsProvider>
        <SidebarProvider>
          <AppSidebar
            user={user}
            orgs={(orgs ?? []).map((o) => ({ id: o.id, name: o.name }))}
            activeOrgId={activeOrgId}
            currentPlan={currentPlan}
            isSuperAdmin={superAdmin}
            activation={activation}
          />
          {/* La bannière d'impersonation n'est plus montée ici : elle vit dans
              le layout RACINE, pour couvrir aussi les écrans hors du groupe
              (app) (/billing-locked, /onboarding, /accept-invitation,
              /two-factor, le 404 de /admin). La remonter ici la doublerait.
              `isImpersonating` reste utilisé ci-dessus par les gates. */}
          <SidebarInset>{children}</SidebarInset>
          {needsUsername ? <UsernamePromptDialog /> : null}
          {/* Monté APRÈS la chaîne de modales et exclusif avec elle : la popup
              username n'est pas fermable, l'overlay de driver.js s'ouvrirait
              par-dessus sans que l'utilisateur puisse en sortir. */}
          {mountTour ? <TourRunner progress={tourProgressMap} /> : null}
        </SidebarProvider>
      </ThemeLabelsProvider>
    </SiteProvider>
  );
}
