"use client";

import { useId, useState, useTransition, type ReactNode } from "react";
import {
  addToListAction,
  attachKeywordsAction,
  removeFromListAction,
  tagKeywordsAction,
  untagKeywordsAction,
} from "@/app/(app)/sites/[siteId]/keywords/actions";
import { OptionSelect } from "@/components/app/option-select";
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { KeywordList, Tag } from "@/lib/keywords";

// Les gestes de l'écran Réservoir (spec Imports §1.8) : attacher à une page, ranger dans une
// liste, tagger. Ils vivent dans un seul fichier parce qu'ils partagent exactement la même
// mécanique — une sélection, une Server Action, un décompte, un message d'erreur relayé — et
// que la dupliquer trois fois serait le seul effet de trois fichiers.
//
// Chaque dialogue sert **la ligne comme la sélection** : une action de masse n'est qu'une
// sélection de plusieurs identifiants, jamais un autre écran.

/** Valeur sentinelle du Select « nouvelle liste » / « nouveau tag ». */
const NEW = "__new";

/**
 * Ce qu'une action rend à l'écran : un décompte, ou un échec.
 *
 * Le décompte n'est pas décoratif — l'api ignore les doublons (`ON CONFLICT DO NOTHING`) et
 * les identifiants périmés, donc « 5 sélectionnés » peut donner « 3 ajoutés ». Fermer le
 * dialogue sans le dire ferait passer un succès partiel pour un succès complet.
 */
type Outcome = { message: string } | { error: string };

/**
 * L'état d'un dialogue, **remis à zéro à chaque ouverture**.
 *
 * Sans cela, `outcome` ne serait effacé qu'au submit suivant — or il n'y a plus de submit une
 * fois le résultat affiché (le formulaire cède la place). Le dialogue rouvert resterait donc
 * bloqué sur l'ancien message, sans champ ni bouton : le mot-clé cesserait d'être actionnable
 * jusqu'au rechargement de la page. L'ajustement se fait **pendant le rendu** (pattern React
 * de synchronisation sur une prop) et non dans un effet, pour qu'aucune frame ne montre le
 * résultat périmé.
 */
function useAction(open: boolean, resetFields: () => void) {
  const [pending, startTransition] = useTransition();
  const [outcome, setOutcome] = useState<Outcome | null>(null);
  const [wasOpen, setWasOpen] = useState(open);
  if (wasOpen !== open) {
    setWasOpen(open);
    if (open) {
      setOutcome(null);
      resetFields();
    }
  }
  const run = (action: () => Promise<Outcome>): void => {
    setOutcome(null);
    startTransition(async () => setOutcome(await action()));
  };
  return { pending, outcome, run };
}

/**
 * « 3 ajoutés sur 5 » plutôt que « 3 ajoutés » quand le compte diffère de la sélection — et
 * l'explication du manquant dépend du geste, elle ne peut pas être générique :
 * l'appartenance à une liste ou à un tag est un `ON CONFLICT DO NOTHING` (un manquant est
 * souvent un doublon), l'attache est un **upsert** (un manquant y est toujours un échec).
 */
function countMessage(done: number, total: number, verb: string, whenShort: string): string {
  const plural = done > 1 ? "s" : "";
  if (done === total) return `${done} ${verb}${plural}.`;
  return `${done} ${verb}${plural} sur ${total} — ${whenShort}`;
}

const ALREADY = "les autres l'étaient déjà, ou ont disparu de la sélection.";
const ATTACH_SHORT = "les autres ont échoué.";

function Shell({
  open,
  onOpenChange,
  title,
  description,
  outcome,
  pending,
  submitLabel,
  submitDisabled = false,
  onSubmit,
  children,
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description: string;
  outcome: Outcome | null;
  pending: boolean;
  submitLabel: string;
  submitDisabled?: boolean;
  onSubmit: () => void;
  children: ReactNode;
}) {
  const done = outcome !== null && "message" in outcome;
  return (
    <Dialog
      open={open}
      onOpenChange={(next) => {
        // Fermer en vol (croix, Échap, clic hors dialogue) laisserait la promesse
        // aboutir dans le vide et son résultat s'attribuer à la sélection suivante.
        if (!next && pending) return;
        onOpenChange(next);
      }}
    >
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription>{description}</DialogDescription>
        </DialogHeader>

        {/* Une fois le geste passé, le formulaire cède la place à son résultat : le
            dialogue ne se referme pas tout seul sur un succès partiel silencieux. */}
        {done ? null : <div className="space-y-3">{children}</div>}

        {outcome === null ? null : "error" in outcome ? (
          // Le Problem Details de l'api est rendu tel quel : il dit précisément ce qui a été
          // refusé (URL invalide, nom de liste déjà pris), mieux qu'un message générique.
          <p role="alert" className="text-destructive text-sm">
            {outcome.error}
          </p>
        ) : (
          <output className="text-sm">{outcome.message}</output>
        )}

        <DialogFooter>
          <Button variant="outline" onClick={() => onOpenChange(false)} disabled={pending}>
            {done ? "Fermer" : "Annuler"}
          </Button>
          {done ? null : (
            <Button onClick={onSubmit} disabled={pending || submitDisabled}>
              {pending ? "En cours…" : submitLabel}
            </Button>
          )}
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

export function AttachDialog({
  open,
  onOpenChange,
  siteId,
  targetId,
  keywordIds,
  /** Nom du mot-clé quand il n'y en a qu'un — le dialogue de masse n'en a pas. */
  keyword,
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  siteId: string;
  targetId: string;
  keywordIds: string[];
  keyword?: string;
}) {
  const id = useId();
  const [url, setUrl] = useState("");
  const { pending, outcome, run } = useAction(open, () => setUrl(""));
  const bulk = keywordIds.length > 1;

  return (
    <Shell
      open={open}
      onOpenChange={onOpenChange}
      title="Attacher à une page"
      description={
        keyword === undefined
          ? `${keywordIds.length} mot(s)-clé(s) à rattacher à la même page — le geste de planification d'un cluster.`
          : `« ${keyword} » — déclarez l'URL de la page qui doit se positionner dessus.`
      }
      outcome={outcome}
      pending={pending}
      submitLabel="Attacher"
      submitDisabled={url.trim() === ""}
      onSubmit={() =>
        run(async () => {
          const result = await attachKeywordsAction({ siteId, targetId, keywordIds, url });
          // `ok` et non `error` : le succès porte lui aussi un champ `error` — celui de la
          // dernière ligne en échec dans un lot partiellement attaché.
          if (!("ok" in result)) return result;
          // Une URL refusée fait échouer tout le lot de la même façon : le message de l'api
          // est alors la seule information utile, le décompte ne dirait que « 0 attaché ».
          if (result.attached === 0 && result.error !== null) return { error: result.error };
          setUrl("");
          const message = countMessage(result.attached, keywordIds.length, "attaché", ATTACH_SHORT);
          const last = result.error;
          return { message: last === null ? message : `${message} Dernière erreur : ${last}` };
        })
      }
    >
      <div className="space-y-1">
        <Label htmlFor={`${id}-url`}>URL de la page</Label>
        <Input
          id={`${id}-url`}
          value={url}
          placeholder="https://exemple.fr/ma-page"
          onChange={(event) => setUrl(event.target.value)}
        />
      </div>
      <p className="text-muted-foreground text-sm">
        La page n'a pas besoin d'exister : l'attache reste « en attente de crawl » jusqu'à ce qu'un
        crawl la trouve. C'est ainsi qu'on planifie une page à écrire. Le choix parmi les pages déjà
        crawlées arrivera avec le module Crawl.
      </p>
      {/* L'attache manuelle est **unique** par (cible, mot-clé) — décision n°6 : réattacher
          remplace la décision précédente. Sur une ligne, l'utilisateur a le mot-clé sous les
          yeux et c'est son intention ; sur une sélection de cinquante, il ne peut pas savoir
          lesquelles portaient déjà une décision — l'écran ne la montre nulle part, `GET
          /keywords` ne rendant ni l'URL ni la source des maps. Le dire est le minimum tant
          que l'api ne compte pas les remplacements (dette suivie en checklist). */}
      {bulk ? (
        <p className="text-sm font-medium">
          Attention : toute attache manuelle déjà posée sur ces {keywordIds.length} mots-clés sera{" "}
          <strong>remplacée</strong> par cette URL, et l'ancienne sera perdue. Les observations
          issues des imports, elles, ne sont pas touchées.
        </p>
      ) : null}
    </Shell>
  );
}

export function ListDialog({
  open,
  onOpenChange,
  siteId,
  targetId,
  keywordIds,
  lists,
  mode,
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  siteId: string;
  targetId: string;
  keywordIds: string[];
  lists: KeywordList[];
  mode: "add" | "remove";
}) {
  const id = useId();
  const [choice, setChoice] = useState<string>(lists[0]?.id ?? NEW);
  const [newName, setNewName] = useState("");
  const adding = mode === "add";
  // Un **seul** `ListDialog` sert les deux modes : sans remise à zéro à l'ouverture, le
  // « Nouvelle liste… » choisi en mode « ajouter » survivait au passage en mode « retirer »
  // et partait en `listId: "__new"` — un 422 par la porte de derrière.
  const { pending, outcome, run } = useAction(open, () => {
    setChoice(lists[0]?.id ?? NEW);
    setNewName("");
  });
  const options = [
    ...lists.map((list) => ({ value: list.id, label: `${list.name} (${list.item_count})` })),
    ...(adding ? [{ value: NEW, label: "Nouvelle liste…" }] : []),
  ];

  return (
    <Shell
      open={open}
      onOpenChange={onOpenChange}
      title={adding ? "Ajouter à une liste" : "Retirer d'une liste"}
      description={
        adding
          ? `${keywordIds.length} mot(s)-clé(s) à ranger dans une liste de cette cible.`
          : `${keywordIds.length} mot(s)-clé(s) à retirer d'une liste. Ils restent dans le réservoir de l'organisation.`
      }
      outcome={outcome}
      pending={pending}
      submitLabel={adding ? "Ajouter" : "Retirer"}
      // Retirer sans liste à choisir n'a pas de sens : le bouton part désactivé plutôt que
      // d'envoyer la valeur sentinelle et de récolter un 422 « Requête invalide ».
      submitDisabled={options.length === 0 || (adding && choice === NEW && newName.trim() === "")}
      onSubmit={() =>
        run(async () => {
          if (adding) {
            const result = await addToListAction({
              siteId,
              targetId,
              keywordIds,
              ...(choice === NEW ? { newName } : { listId: choice }),
            });
            if ("error" in result) return result;
            setNewName("");
            return { message: countMessage(result.added, keywordIds.length, "ajouté", ALREADY) };
          }
          const result = await removeFromListAction({ siteId, listId: choice, keywordIds });
          if ("error" in result) return result;
          return { message: countMessage(result.removed, keywordIds.length, "retiré", ALREADY) };
        })
      }
    >
      {options.length === 0 ? (
        <p className="text-muted-foreground text-sm">
          Aucune liste sur cette cible — il n'y a rien à retirer.
        </p>
      ) : (
        <div className="space-y-1">
          <Label htmlFor={`${id}-list`}>Liste</Label>
          <OptionSelect
            id={`${id}-list`}
            label="Liste"
            options={options}
            value={choice}
            onValueChange={setChoice}
          />
        </div>
      )}
      {adding && choice === NEW ? (
        <div className="space-y-1">
          <Label htmlFor={`${id}-name`}>Nom de la liste</Label>
          <Input
            id={`${id}-name`}
            value={newName}
            placeholder="Cluster chaussures"
            onChange={(event) => setNewName(event.target.value)}
          />
        </div>
      ) : null}
    </Shell>
  );
}

export function TagDialog({
  open,
  onOpenChange,
  siteId,
  keywordIds,
  tags,
  mode,
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  siteId: string;
  keywordIds: string[];
  tags: Tag[];
  mode: "add" | "remove";
}) {
  const id = useId();
  const [choice, setChoice] = useState<string>(tags[0]?.id ?? NEW);
  const [newName, setNewName] = useState("");
  const adding = mode === "add";
  const { pending, outcome, run } = useAction(open, () => {
    setChoice(tags[0]?.id ?? NEW);
    setNewName("");
  });
  const options = [
    ...tags.map((tag) => ({ value: tag.id, label: tag.name })),
    ...(adding ? [{ value: NEW, label: "Nouveau tag…" }] : []),
  ];

  return (
    <Shell
      open={open}
      onOpenChange={onOpenChange}
      title={adding ? "Tagger" : "Retirer un tag"}
      description={
        adding
          ? `${keywordIds.length} mot(s)-clé(s) à tagger. Les tags sont partagés par toute l'organisation.`
          : `${keywordIds.length} mot(s)-clé(s) à détagger. Le tag lui-même est conservé.`
      }
      outcome={outcome}
      pending={pending}
      submitLabel={adding ? "Tagger" : "Retirer"}
      submitDisabled={options.length === 0 || (adding && choice === NEW && newName.trim() === "")}
      onSubmit={() =>
        run(async () => {
          if (adding) {
            const result = await tagKeywordsAction({
              siteId,
              keywordIds,
              ...(choice === NEW ? { newName } : { tagId: choice }),
            });
            if ("error" in result) return result;
            setNewName("");
            return { message: countMessage(result.added, keywordIds.length, "tagué", ALREADY) };
          }
          const result = await untagKeywordsAction({ siteId, tagId: choice, keywordIds });
          if ("error" in result) return result;
          return { message: countMessage(result.removed, keywordIds.length, "détagué", ALREADY) };
        })
      }
    >
      {options.length === 0 ? (
        <p className="text-muted-foreground text-sm">
          Aucun tag dans l'organisation — il n'y a rien à retirer.
        </p>
      ) : (
        <div className="space-y-1">
          <Label htmlFor={`${id}-tag`}>Tag</Label>
          <OptionSelect
            id={`${id}-tag`}
            label="Tag"
            options={options}
            value={choice}
            onValueChange={setChoice}
          />
        </div>
      )}
      {adding && choice === NEW ? (
        <div className="space-y-1">
          <Label htmlFor={`${id}-name`}>Nom du tag</Label>
          <Input
            id={`${id}-name`}
            value={newName}
            placeholder="priorité"
            onChange={(event) => setNewName(event.target.value)}
          />
        </div>
      ) : null}
    </Shell>
  );
}
