// src/components/gsc/gsc-properties-card.tsx — une ligne par site : mapping
// propriété↔site, état GSC du site, autorité qui le porte, et sync manuelle.
// Le sélecteur est alimenté par MES propriétés (tous mes comptes Google scopés
// confondus) : choisir une propriété épingle le compte qui la voit, il n'y a
// donc aucun sélecteur de compte. Le Select reprend le pattern de l'ex
// gsc-integrations.tsx (reload sur succès) ; la sync affiche son résultat
// inline sans reload (l'utilisateur enchaîne souvent plusieurs sites).
"use client";

import { useState, useTransition } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  type GscSiteRow,
  type GscStatus,
  setSiteGscPropertyAction,
  syncGscKeywordsAction,
} from "@/lib/gsc/actions";
import { gscFailureMessage } from "@/lib/gsc/failure-message";
import type { GscSiteSync } from "@/lib/gsc/health";

const UNMAPPED = "__none__";

const NO_GOOGLE_TITLE =
  "Connecte d'abord ton compte Google Search Console : on ne peut pas te désigner comme autorité d'un site sans compte utilisable.";

/** Libellé d'état, court, calibré pour un badge. Le `state` est dérivé côté
 *  serveur (`deriveSiteGscState`) : on ne décide rien ici, on traduit. La
 *  valeur `token-refused` n'existe pas dans `GscSiteRow["state"]` — elle ne se
 *  constate qu'après un appel réel à Google — donc pas de branche pour elle. */
function stateBadge(
  site: GscSiteRow,
): { label: string; variant: "outline" | "destructive" } | null {
  switch (site.state) {
    case "ok":
      return null;
    case "no-property":
      return { label: "Aucune propriété mappée", variant: "outline" };
    case "no-authority":
    case "authority-lost":
      return { label: "À reconnecter", variant: "destructive" };
    case "no-google-account":
    case "missing-scope":
      // Le compte en cause est celui de l'AUTORITÉ, qui n'est pas forcément
      // moi. Dire « reconnecte ton compte Google » à quelqu'un dont le compte
      // va très bien, juste sous « Connecté par Alice », c'est se contredire
      // sur une même ligne. Le détail nomme le vrai coupable (stateDetail).
      return site.isMine
        ? { label: "Reconnecte ton compte Google", variant: "destructive" }
        : { label: "À reconnecter", variant: "destructive" };
  }
}

/** Phrase d'explication, seulement quand le badge ne peut pas suffire : la
 *  panne porte sur le compte de quelqu'un d'autre. Volontairement à la
 *  troisième personne — elle suit « Connecté par Alice », donc « son compte »
 *  se rattache sans répéter le nom. */
function stateDetail(site: GscSiteRow): string | null {
  if (site.isMine) return null;
  const owner = site.authorityName ? "Son" : "Le";
  const suffix = site.authorityName ? "" : " rattaché à ce site";
  switch (site.state) {
    case "missing-scope":
      return `${owner} compte Google${suffix} n'a plus l'accès Search Console.`;
    case "no-google-account":
      return `${owner} compte Google${suffix} n'est plus lié.`;
    default:
      return null;
  }
}

/** Qui porte l'autorité GSC de ce site. Rien à dire quand personne ne la porte
 *  — l'état s'en charge déjà. */
function authorityLabel(site: GscSiteRow): string | null {
  if (site.isMine) return "Connecté par toi";
  if (site.authorityName) return `Connecté par ${site.authorityName}`;
  return null;
}

/** Tout état où la propriété est là mais l'autorité inutilisable se répare en
 *  reprenant la main — y compris quand c'est le compte Google de quelqu'un
 *  d'autre qui est cassé : sans ce chemin, un membre qui voit pourtant la
 *  propriété n'a que « Non mappé » puis re-sélection, dont la première étape
 *  efface les colonnes d'autorité d'un site déjà en panne. Le serveur revalide
 *  de toute façon contre MES propriétés, donc le bouton ne peut rien casser.
 *
 *  L'état « ok » en fait partie dès que l'autorité est un tiers, et ce n'est pas
 *  un détail : `token-refused` (refresh token révoqué — mot de passe Google
 *  changé, consentement retiré) n'est pas dérivable sans appeler Google, donc un
 *  site dont le jeton est mort se dérive `ok`. La sync répond alors « Google a
 *  refusé l'accès — reconnecte le compte », phrase inactionnable quand le compte
 *  en cause est celui d'Alice. Sans reprise, il ne reste que « Non mappé » puis
 *  re-sélection, la manœuvre destructive ci-dessus.
 *
 *  Choix de forme : condition sans état (dérivée de la seule ligne) plutôt que
 *  « afficher après un échec de sync » — pas de mémoire à tenir, et le bouton
 *  est là AVANT que l'utilisateur ait buté sur l'erreur. Seule exclusion :
 *  « ok » + autorité = moi, où « Reprendre » se reprendrait à soi-même. Ce cas
 *  a déjà son chemin propre — un jeton à moi révoqué allume
 *  `status.propertiesError` en haut de carte et « Mon compte Google » offre la
 *  reconnexion. */
function canRetake(site: GscSiteRow): boolean {
  if (site.gscProperty === null) return false;
  if (site.state === "ok") return !site.isMine;
  return true;
}

function lastSyncLabel(iso: string | null): string {
  if (!iso) return "Jamais synchronisé";
  return `Dernière sync : ${new Date(iso).toLocaleDateString("fr-FR", {
    day: "numeric",
    month: "short",
    hour: "2-digit",
    minute: "2-digit",
  })}`;
}

export function GscPropertiesCard({
  status,
  syncBySite,
}: {
  status: GscStatus;
  syncBySite: GscSiteSync[];
}) {
  const [error, setError] = useState<string | null>(null);
  const [syncResult, setSyncResult] = useState<
    Record<string, { kind: "ok" | "warn" | "error"; message: string }>
  >({});
  const [syncingId, setSyncingId] = useState<string | null>(null);
  const [pending, startTransition] = useTransition();
  const lastSync = new Map(syncBySite.map((s) => [s.siteId, s.lastGscSyncAt]));
  // Sans compte Google scopé, il n'y a aucune propriété à proposer et aucune
  // autorité à prendre : le mapping est en lecture seule.
  const canMap = status.myGoogle === "linked";

  function applyProperty(siteId: string, property: string | null) {
    setError(null);
    startTransition(async () => {
      // Le server action peut throw (échec réseau) : sans catch on crashe vers
      // l'error boundary racine. On surface l'erreur inline à la place.
      try {
        const res = await setSiteGscPropertyAction(siteId, property);
        // Le message du serveur distingue « propriété pas à toi » de « Google
        // n'a pas répondu » : on l'affiche tel quel, le remplacer par une
        // phrase générique renverrait l'utilisateur démapper sur une panne.
        if (!res.ok) setError(res.error);
        else window.location.reload();
      } catch {
        setError("La requête a échoué — réessaie.");
      }
    });
  }

  function sync(siteId: string) {
    setError(null);
    setSyncingId(siteId);
    startTransition(async () => {
      try {
        const res = await syncGscKeywordsAction(siteId);
        if (!res.ok) {
          setSyncResult((m) => ({
            ...m,
            [siteId]: { kind: "error", message: `Échec : ${res.error}` },
          }));
          return;
        }
        // Succès global mais dimensions best-effort possiblement dégradées :
        // on avertit explicitement (pays en échec / pages non récupérées).
        const base = `${res.queries} requêtes, ${res.matched} matchés, ${res.created} créés`;
        const warnings: string[] = [];
        if (res.countries.failed > 0) {
          warnings.push(`${res.countries.failed} pays en échec`);
        }
        if (!res.pages) warnings.push("pages non récupérées");
        setSyncResult((m) => ({
          ...m,
          [siteId]: warnings.length
            ? {
                kind: "warn",
                message: `${base} — attention : ${warnings.join(", ")}`,
              }
            : { kind: "ok", message: base },
        }));
      } catch {
        setSyncResult((m) => ({
          ...m,
          [siteId]: {
            kind: "error",
            message: "Échec : la requête a échoué — réessaie.",
          },
        }));
      } finally {
        // Toujours relâcher le « Sync… » — sinon le bouton reste bloqué en cas
        // de throw.
        setSyncingId(null);
      }
    });
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle>Propriétés par site</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        {error ? (
          <p className="text-destructive text-sm" role="alert">
            {error}
          </p>
        ) : null}
        {status.propertiesError ? (
          <p className="text-destructive text-sm" role="alert">
            Impossible de lister tes propriétés Search Console — réessaie ou
            reconnecte le compte.
          </p>
        ) : null}
        {status.sites.length === 0 ? (
          <p className="text-muted-foreground text-sm">Aucun site.</p>
        ) : (
          status.sites.map((site) => {
            const badge = stateBadge(site);
            const authority = authorityLabel(site);
            const detail = stateDetail(site);
            const retake = canRetake(site);
            // La propriété mappée peut ne pas figurer dans MES propriétés :
            // c'est le cas normal quand l'autorité est quelqu'un d'autre. Sans
            // cette option, le Select afficherait un déclencheur vide, ce qui
            // se lit comme « non mappé » — un contresens.
            const foreignProperty =
              site.gscProperty !== null &&
              !status.properties.some((p) => p.siteUrl === site.gscProperty)
                ? site.gscProperty
                : null;
            return (
              <div key={site.id} className="flex flex-col gap-1">
                <div className="flex items-center justify-between gap-4">
                  <span className="truncate text-sm">{site.name}</span>
                  <div className="flex items-center gap-2">
                    {canMap ? (
                      <Select
                        defaultValue={site.gscProperty ?? UNMAPPED}
                        onValueChange={(v) =>
                          applyProperty(site.id, v === UNMAPPED ? null : v)
                        }
                        disabled={pending}
                      >
                        <SelectTrigger className="w-[260px]">
                          <SelectValue />
                        </SelectTrigger>
                        {/* position="popper" obligatoire : en item-aligned
                            (défaut Radix), la première option sort du viewport
                            et devient incliquable. */}
                        <SelectContent position="popper">
                          <SelectItem value={UNMAPPED}>Non mappé</SelectItem>
                          {/* `disabled` par intention, pas par accident : sur
                              le chemin de récupération d'erreur (soumission
                              refusée sans reload, puis retour en arrière), la
                              valeur change vraiment et le serveur répondrait
                              « Propriété GSC non vérifiée ou inconnue » — un
                              message qui contredit l'étiquette cliquée. */}
                          {foreignProperty ? (
                            <SelectItem value={foreignProperty} disabled>
                              {foreignProperty} (hors de mes propriétés)
                            </SelectItem>
                          ) : null}
                          {status.properties.map((p) => (
                            <SelectItem key={p.siteUrl} value={p.siteUrl}>
                              {p.siteUrl}
                            </SelectItem>
                          ))}
                        </SelectContent>
                      </Select>
                    ) : (
                      <span className="text-muted-foreground text-sm">
                        {site.gscProperty ?? "Non mappé"}
                      </span>
                    )}
                    {/* Le bouton désactivé doit dire POURQUOI : avant, le clic
                        produisait le message précis de `gscFailureMessage` en
                        ligne. Même traitement que « Reprendre » — `title` sur
                        le wrapper, les navigateurs n'affichant pas
                        systématiquement l'infobulle d'un bouton `disabled`. */}
                    <span
                      title={
                        site.state === "ok"
                          ? undefined
                          : gscFailureMessage(site.state)
                      }
                    >
                      <Button
                        variant="outline"
                        size="sm"
                        disabled={pending || site.state !== "ok"}
                        onClick={() => sync(site.id)}
                      >
                        {syncingId === site.id ? "Sync…" : "Synchroniser"}
                      </Button>
                    </span>
                  </div>
                </div>
                {/* `retake` fait partie de la condition : le bouton vit dans
                    cette bande, et un site « ok » porté par un tiers dont le nom
                    n'a pas pu être résolu n'affiche ni badge, ni autorité, ni
                    détail — la bande disparaîtrait avec la seule réparation
                    disponible. */}
                {badge || authority || detail || retake ? (
                  <div className="flex flex-wrap items-center gap-2">
                    {badge ? (
                      <Badge variant={badge.variant}>{badge.label}</Badge>
                    ) : null}
                    {authority ? (
                      <span className="text-muted-foreground text-xs">
                        {authority}
                      </span>
                    ) : null}
                    {detail ? (
                      <span className="text-muted-foreground text-xs">
                        {detail}
                      </span>
                    ) : null}
                    {retake ? (
                      // `title` porté par le wrapper : sur un bouton désactivé,
                      // les navigateurs n'affichent pas toujours l'infobulle.
                      <span title={canMap ? undefined : NO_GOOGLE_TITLE}>
                        <Button
                          variant="outline"
                          size="sm"
                          disabled={pending || !canMap}
                          onClick={() =>
                            applyProperty(site.id, site.gscProperty)
                          }
                        >
                          Reprendre
                        </Button>
                      </span>
                    ) : null}
                  </div>
                ) : null}
                <p className="text-muted-foreground text-xs">
                  {/* toLocaleDateString dépend de la tz : le rendu serveur et le
                      rendu client peuvent diverger → on suppress le warning
                      d'hydratation sur ce seul span. */}
                  <span suppressHydrationWarning>
                    {lastSyncLabel(lastSync.get(site.id) ?? null)}
                  </span>
                  {syncResult[site.id] ? (
                    syncResult[site.id].kind === "error" ? (
                      <span className="text-destructive" role="alert">
                        {" — "}
                        {syncResult[site.id].message}
                      </span>
                    ) : syncResult[site.id].kind === "warn" ? (
                      <span
                        className="text-amber-600 dark:text-amber-400"
                        role="alert"
                      >
                        {" — "}
                        {syncResult[site.id].message}
                      </span>
                    ) : (
                      ` — ${syncResult[site.id].message}`
                    )
                  ) : null}
                </p>
              </div>
            );
          })
        )}
      </CardContent>
    </Card>
  );
}
