import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { OnboardingWizard } from "@/components/onboarding/onboarding-wizard";
import {
  Card,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { env } from "@/env";
import { auth } from "@/lib/auth";
import { sessionIsImpersonated } from "@/lib/auth/impersonation-guard";
import { getMangoolsStatus } from "@/lib/connectors/core";
import { type GscStatus, getGscStatusAction } from "@/lib/gsc/actions";
import { getTargetCandidates } from "@/lib/keywords/queries";
import type { Site } from "@/lib/keywords/types";
import { getActivationSnapshot } from "@/lib/onboarding/activation/snapshot";
import {
  canEnterGscStep,
  canEnterKeywordsStep,
  canEnterSiteStep,
  requiredComplete,
  WIZARD_STEP_IDS,
  type WizardStepId,
} from "@/lib/onboarding/activation/steps";
import type { ActivationSnapshot } from "@/lib/onboarding/activation/types";
import type { TargetCandidate } from "@/lib/onboarding/first-target";
import { resolveOnboardingOrgId } from "@/lib/org/onboarding-org";
import { getSitesForActiveOrg } from "@/lib/sites/access";
import { getActiveSiteId } from "@/lib/sites/active-site";

/** Où l'on dépose quiconque n'a plus rien à faire ici. */
const LANDING = "/keywords/planning";

/**
 * Site du parcours — celui dont l'état d'activation est dérivé.
 *
 * MÊME résolution que `getActivationSnapshot` (cookie de site actif validé
 * contre les sites accessibles, dans le même ordre) : le site sur lequel on
 * écrit est donc exactement celui dont les drapeaux sont dérivés, sans quoi une
 * écriture réussie ne cocherait jamais son étape et le parcours reboucherait.
 *
 * Une seule résolution pour TROIS étapes (Search Console, mots-clés, premier
 * plan). `getSitesForActiveOrg` est `cache()`-ée, donc la répéter serait
 * gratuite — mais ce serait surtout trois occasions de diverger.
 *
 * Tout échec renvoie `null` plutôt que de casser la page : on retire alors les
 * étapes qui dépendent du site, on n'affiche pas un parcours en panne.
 */
async function loadJourneySite(): Promise<Site | null> {
  try {
    const sites = await getSitesForActiveOrg();
    if (sites.length === 0) return null;
    const siteId = await getActiveSiteId(sites.map((s) => s.id));
    return sites.find((s) => s.id === siteId) ?? null;
  } catch (err) {
    console.error("[onboarding] site resolution failed:", err);
    return null;
  }
}

/**
 * Étape Search Console : lue ICI, dans le RSC, jamais au montage du composant
 * (une lecture serveur au montage a déjà figé une navigation dans ce dépôt).
 *
 * Tout échec RETIRE l'étape au lieu de casser la page : elle est facultative,
 * et un parcours d'entrée qui tombe en panne parce que Google n'a pas répondu
 * serait le contraire exact de ce qu'on veut ici.
 */
async function loadGscStep(
  siteId: string,
): Promise<{ status: GscStatus; siteId: string } | null> {
  try {
    return { status: await getGscStatusAction(), siteId };
  } catch (err) {
    console.error("[onboarding] gsc status failed:", err);
    return null;
  }
}

/**
 * Connecteur Mangools de l'organisation active — LU ICI, pas au montage du
 * composant client, qui devrait sinon faire un aller-retour serveur juste pour
 * décider entre « colle ton jeton » et « voici tes listes ».
 *
 * Aucun appel à Mangools : `getMangoolsStatus` lit le status stocké
 * (`active` / `invalid` / absent). Les LISTES, elles, ne sont chargées qu'au
 * clic — chacune coûte un appel API throttlé, et l'étape s'affiche aussi pour
 * qui n'empruntera jamais ce chemin.
 *
 * Tout échec rend `false` plutôt que de casser la page : `getActiveOrgId` LÈVE
 * sur une session sans organisation active (course du signup par lien magique,
 * documentée dans `active-org.ts`). Le pire que produise ce repli, c'est un
 * champ de jeton proposé à quelqu'un qui en a déjà un — le listage rendra alors
 * ses listes dès la connexion re-tentée.
 */
async function loadMangoolsConnected(): Promise<boolean> {
  try {
    return (await getMangoolsStatus()).connected;
  } catch (err) {
    console.error("[onboarding] mangools status failed:", err);
    return false;
  }
}

/**
 * Candidats du premier ciblage. Échec = liste vide : l'étape rend alors son
 * état « réservoir vide », qui porte sa propre sortie — jamais une page en
 * erreur sur la DERNIÈRE étape du parcours.
 */
async function loadTargetCandidates(
  site: Site | null,
): Promise<TargetCandidate[]> {
  if (!site) return [];
  try {
    return await getTargetCandidates(
      site.id,
      site.targetCountries?.[0] ?? null,
    );
  } catch (err) {
    console.error("[onboarding] target candidates failed:", err);
    return [];
  }
}

/**
 * `?step=` : d'où le parcours reprend après un rechargement dur. Toute valeur
 * inconnue est ignorée (reprise depuis le début) — le paramètre est un confort
 * de reprise, jamais une autorisation : il ne peut que RETIRER des étapes déjà
 * traversées, jamais en ouvrir une que l'état dérivé refuse.
 */
function parseResumeStep(
  raw: string | string[] | undefined,
): WizardStepId | null {
  return typeof raw === "string" &&
    (WIZARD_STEP_IDS as readonly string[]).includes(raw)
    ? (raw as WizardStepId)
    : null;
}

/**
 * Étapes que le parcours peut réellement monter, dans l'ordre canonique.
 *
 * Une étape n'entre QUE si (a) son drapeau est encore faux — sinon on la
 * reproposerait après chaque rechargement — et (b) ses données existent : une
 * carte sans données ne rend rien, et un écran vide au milieu d'un parcours
 * obligé est un cul-de-sac.
 *
 * `hasSite` porte la MÊME garde que celle déjà encodée dans `data.gsc` et
 * `data.keywords` (tous deux `null` sans site accessible), et les QUATRE étapes
 * qui se jouent SUR un site la partagent désormais. Sans elle,
 * `/onboarding?step=enrich-keywords` tapé à la main sautait l'étape de création
 * — le filtre de reprise ne gardait que des étapes montées à tort — et le
 * parcours finissait par annoncer « ce site n'a aucun pays cible » alors qu'il
 * n'y avait pas de site. Il ne reste alors que `create-site`, et le repli de
 * `steps` (plus bas) remonte le parcours au début : c'est le bon écran.
 *
 * Snapshot `null` (dérivation en échec) AVEC un site accessible : les étapes de
 * données restent montables, `pending()` les considérant toutes en attente.
 * Sans site du tout, il ne reste rien à monter et l'appelant dépose
 * l'utilisateur dans l'app plutôt que sur une page vide.
 */
function mountableSteps(
  snapshot: ActivationSnapshot | null,
  data: { gsc: unknown; keywords: unknown; hasSite: boolean },
): WizardStepId[] {
  const pending = (id: WizardStepId) => !snapshot?.flags[id];
  const out: WizardStepId[] = [];
  if (canEnterSiteStep(snapshot)) out.push("create-site");
  if (data.gsc) out.push("connect-gsc");
  if (data.keywords) out.push("add-keywords");
  if (data.hasSite && pending("enrich-keywords")) out.push("enrich-keywords");
  if (data.hasSite && pending("target-country")) out.push("target-country");
  return out;
}

export default async function OnboardingPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const session = await auth.api.getSession({ headers: await headers() });
  if (!session?.user?.id) redirect("/login");
  // Sortie fondée sur le MÊME état dérivé que le gate du layout (app), et sur
  // les MÊMES trois conditions : ce qui n'entre pas doit pouvoir sortir, sinon
  // l'URL tapée à la main enferme là où la redirection ne mène plus.
  //   1. étapes requises faites → il n'y a plus rien à guider ;
  //   2. pas le droit de créer un site → la première étape lui est interdite,
  //      le wizard n'a rien à lui proposer (createSiteAction lèverait) ;
  //   3. impersonation → l'opérateur inspecte un compte client ; les écritures
  //      du wizard sont neutralisées, il ne pourrait donc jamais en sortir par
  //      le bas.
  // Un snapshot `null` (dérivation en échec) laisse passer : mieux vaut un
  // wizard de trop qu'une redirection fondée sur une inconnue. Zéro requête
  // supplémentaire — tout est déjà chargé ici.
  const snapshot = await getActivationSnapshot();
  const impersonated = sessionIsImpersonated(session);
  if (
    snapshot &&
    (requiredComplete(snapshot.flags) ||
      !snapshot.canCreateSite ||
      impersonated)
  ) {
    redirect(LANDING);
  }
  // Cet appel ne répare RIEN : il lit, il ne persiste pas. Le self-heal d'une
  // session née sans organisation active (course du signup magic link) a déjà
  // eu lieu plus haut, dans getActivationSnapshot() → getActiveOrgId(), seul
  // endroit qui appelle setActiveOrganization. Ce qu'on fait ici, c'est
  // répondre à une question que `getActiveOrgId` ne sait poser qu'en levant :
  // cet utilisateur appartient-il à une organisation, oui ou non ? `null` ne
  // veut pas dire « session jeune » mais « aucune adhésion en base » — le
  // wizard n'a alors strictement rien à proposer, sa première étape étant la
  // création d'un site, impossible hors d'une org.
  const activeOrgId = await resolveOnboardingOrgId(
    session.user.id,
    session.session.activeOrganizationId,
  );
  // ⚠️ SURTOUT PAS `redirect("/login")` : c'était une boucle fermée. /login ne
  // renvoie pas une session déjà ouverte, son formulaire dépose sur
  // /keywords/reservoir, le layout (app) n'y trouve pas d'org résoluble et
  // renvoie ici, qui renvoyait à /login. Un utilisateur retiré de sa dernière
  // organisation n'a AUCUNE ligne d'adhésion : le self-heal ne le sauve pas, et
  // il tournait en rond. On rend donc un écran terminal — la déconnexion vit
  // déjà dans l'en-tête du layout du parcours, c'est la seule action qui a du
  // sens ici.
  if (!activeOrgId) {
    const tOrg = await getTranslations("Onboarding");
    return (
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle>{tOrg("noOrgTitle")}</CardTitle>
          <CardDescription>{tOrg("noOrgDescription")}</CardDescription>
        </CardHeader>
      </Card>
    );
  }

  const site = await loadJourneySite();
  // L'appel Google n'a lieu que si l'étape a lieu d'être : site accessible et
  // propriété pas encore mappée.
  const gsc =
    canEnterGscStep(snapshot) && site ? await loadGscStep(site.id) : null;
  // L'étape mots-clés suit la même règle : pas de lecture quand elle n'a pas
  // lieu d'être (réservoir déjà peuplé, ou aucun site accessible).
  const keywordsStep =
    canEnterKeywordsStep(snapshot) && site
      ? {
          site,
          // Décidé côté serveur : `env` n'est pas lisible dans un composant
          // client, et sans R2 + service analytics l'import lourd répond
          // 503 — le chemin ne doit alors pas être proposé.
          analyticsEnabled: env.analyticsEnabled,
          mangoolsConnected: await loadMangoolsConnected(),
        }
      : null;

  const params = await searchParams;
  const mountable = mountableSteps(snapshot, {
    gsc,
    keywords: keywordsStep,
    hasSite: site !== null,
  });
  // Reprise après rechargement dur : on ne remonte pas AVANT l'étape d'où l'on
  // vient. Sans ça, une étape facultative déjà passée (Search Console,
  // enrichissement) reviendrait à chaque écriture — son drapeau reste faux,
  // c'est le principe même d'une étape qu'on a le droit de sauter.
  const resume = parseResumeStep(params.step);
  const from = resume ? WIZARD_STEP_IDS.indexOf(resume) : 0;
  const resumed = mountable.filter((id) => WIZARD_STEP_IDS.indexOf(id) >= from);
  // Le filtre ne doit jamais VIDER le parcours : une reprise trop avancée
  // (drapeau coché entre-temps depuis un autre onglet) rendrait un écran nu.
  const steps = resumed.length > 0 ? resumed : mountable;
  // Plus rien à monter alors que des étapes requises manquent : le seul cas est
  // un état dérivé illisible. Mieux vaut déposer l'utilisateur dans l'app que
  // lui montrer une page vide dont il ne saurait pas sortir.
  if (steps.length === 0) redirect(LANDING);

  // Chargés indépendamment de l'étape courante : `enrich-keywords` avance
  // LOCALEMENT vers le ciblage (son drapeau vient des volumes en base, pas du
  // connecteur), donc l'étape finale doit déjà avoir ses données à ce rendu.
  const candidates = steps.includes("target-country")
    ? await loadTargetCandidates(site)
    : [];

  // `linkSocial` est un flux redirect : un refus côté Google revient ici en
  // `?error=<code>`. Sans cette lecture, l'utilisateur retomberait sur un écran
  // inchangé, sans un mot. Lu côté serveur — `useSearchParams` dans le wizard
  // imposerait une frontière Suspense pour rien.
  const linkError = params.error;

  return (
    <OnboardingWizard
      userName={session.user.name}
      steps={steps}
      gsc={gsc}
      gscLinkFailed={typeof linkError === "string" && linkError.length > 0}
      keywords={keywordsStep}
      target={{ candidates, countries: site?.targetCountries ?? [] }}
    />
  );
}
