"use client";

import { useEffect, useMemo, useState } from "react";
import { Autocomplete } from "@base-ui/react/autocomplete";
import { SearchIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { debounce } from "@/lib/debounce";
import { cn } from "@/lib/utils";

// Champ de recherche à complétion — la **base partagée** de toutes les recherches serveur de
// l'application, au même titre qu'`OptionSelect` l'est des sélecteurs. Un seul rendu, un seul
// débounce, une seule gestion de la course : pas de variante par écran.
//
// Bâti sur la primitive officielle `Autocomplete` de Base UI (celle que les styles shadcn
// `base-*` habillent), en `mode="none"` : **les items ne sont pas filtrés côté client**. Ils
// arrivent déjà classés par l'api, qui seule voit les centaines de milliers de lignes du
// réservoir ; les refiltrer ici sur dix résultats ne ferait que défaire ce classement.
//
// Trois gestes, trois issues distinctes — c'est tout le contrat du composant :
//
// - **choisir une suggestion** (clic, ou Entrée sur une ligne surlignée au clavier) applique
//   ce terme exact : `onSelect` ;
// - **Entrée sans surlignage** valide la saisie telle quelle : `onSubmit`, la recherche
//   pleine d'aujourd'hui, inchangée ;
// - **Échap** ferme le menu sans rien appliquer (Base UI s'en charge), et périme les requêtes
//   en vol pour qu'une réponse en retard ne le rouvre pas.

export type SearchSuggestion = {
  id: string;
  /** Le terme exact : c'est lui qui sera appliqué comme recherche. */
  label: string;
  /** Repère secondaire aligné à droite (un volume, un décompte). */
  hint?: string;
};

/** Silence après la dernière frappe avant d'interroger l'api. */
const DEBOUNCE_MS = 250;

/** Un catalogue déjà en mémoire (les listes d'une cible) : rien à débouncer, on répond. */
const LOCAL_DEBOUNCE_MS = 0;

/** Marque le popup pour que le `blur` sache distinguer « on le quitte » de « on y clique ». */
const POPUP_SLOT = "search-combobox-popup";

export function SearchCombobox({
  id,
  label,
  placeholder,
  value,
  onValueChange,
  onSubmit,
  onSelect,
  loadSuggestions,
  local = false,
  disabled = false,
  className,
}: {
  id: string;
  label: string;
  placeholder?: string;
  value: string;
  onValueChange: (next: string) => void;
  /** Saisie validée telle quelle : recherche pleine. */
  onSubmit: (query: string) => void;
  /** Une suggestion choisie dans la liste. */
  onSelect: (suggestion: SearchSuggestion) => void;
  /**
   * Charge les suggestions d'une saisie. Rend une liste vide quand il n'y a rien à demander
   * (saisie trop courte) ou rien à proposer — jamais une erreur : une complétion est un
   * confort, elle ne doit pas casser la recherche.
   */
  loadSuggestions: (query: string) => Promise<SearchSuggestion[]>;
  /**
   * Le catalogue est **déjà chargé** (les listes d'une cible, par exemple) plutôt que servi par
   * l'api : on répond sans débounce, et le simple focus propose tout ce qu'il y a. Sans cela,
   * un champ prérempli d'un nom qui ne correspond à aucune ligne rendrait les entrées
   * existantes invisibles jusqu'à ce que l'utilisateur pense à tout effacer.
   */
  local?: boolean;
  /** Champ inerte tant que le catalogue n'a pas répondu : on ne saisit pas dans le vide. */
  disabled?: boolean;
  className?: string;
}) {
  const [items, setItems] = useState<SearchSuggestion[]>([]);
  const [open, setOpen] = useState(false);

  // `useMemo` et non `useState` : le débounce ne porte aucun état à préserver entre deux
  // valeurs de `loadSuggestions`, il doit au contraire se reconstruire si elle change.
  //
  // Conséquence pour l'appelant, et elle n'est pas négociable : `loadSuggestions` DOIT être
  // stable d'un rendu à l'autre (`useCallback`, ou une constante de module). Une fermeture
  // recréée à chaque rendu reconstruirait le débounce, dont le nettoyage d'effet annulerait
  // aussitôt l'appel que la frappe venait de programmer — le menu ne s'ouvrirait plus jamais.
  const debounced = useMemo(
    () =>
      debounce<[string]>(
        (isCurrent, query) => {
          void loadSuggestions(query).then((found) => {
            //  Le jeton du débounce : une réponse dépassée par une frappe plus récente est
            //  jetée. Sans lui, « jewel » écraserait « jewelry manufacturing ».
            if (!isCurrent()) return;
            setItems(found);
            setOpen(found.length > 0);
          });
        },
        local ? LOCAL_DEBOUNCE_MS : DEBOUNCE_MS,
      ),
    [loadSuggestions, local],
  );

  // Le démontage doit périmer ce qui est en vol : un `setItems` après coup serait une fuite.
  useEffect(() => debounced.cancel, [debounced]);

  const change = (next: string) => {
    onValueChange(next);
    debounced.run(next);
  };

  const close = () => {
    debounced.cancel();
    setOpen(false);
  };

  return (
    <Autocomplete.Root
      items={items}
      // Les items viennent déjà filtrés et classés par l'api : `none` empêche Base UI de les
      // refiltrer sur la saisie, ce qui viderait la liste dès que la suggestion diffère de la
      // frappe (une faute rattrapée par le repli trigramme, par exemple).
      mode="none"
      value={value}
      onValueChange={change}
      open={open}
      onOpenChange={(next) => {
        if (!next) close();
        else setOpen(items.length > 0);
      }}
      // `itemToStringValue` sert l'accessibilité et le remplissage : nos items sont des objets.
      itemToStringValue={(item: SearchSuggestion) => item.label}
    >
      <form
        className={cn("min-w-0 grow space-y-1 sm:max-w-xs", className)}
        onSubmit={(event) => {
          event.preventDefault();
          //  Entrée sans suggestion surlignée : la recherche pleine. Quand une ligne *est*
          //  surlignée, Base UI déclenche son `onClick` et empêche cette soumission.
          close();
          onSubmit(value);
        }}
      >
        <Label htmlFor={id}>{label}</Label>
        <div className="relative">
          <SearchIcon className="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2" />
          <Autocomplete.Input
            //  Quitter le champ périme ce qui est en vol (review F3). Sans cela, une frappe
            //  suivie d'un `Tab` immédiat laissait la requête revenir 200 ms plus tard et
            //  **ouvrir** le menu sous un champ que l'utilisateur avait déjà quitté.
            //  `debounced.cancel()` couvre les deux temps — l'appel pas encore parti et celui
            //  déjà en vol (testé dans `lib/debounce.test.ts`).
            //
            //  Le garde-fou `POPUP_SLOT` est indispensable : cliquer une suggestion fait
            //  perdre le focus au champ *avant* que le clic n'aboutisse. Fermer là
            //  démonterait la ligne sous le curseur et mangerait la sélection. On ne ferme
            //  donc que si le focus a réellement quitté le composant.
            onBlur={(event) => {
              const next = event.relatedTarget;
              if (next instanceof Element && next.closest(`[data-slot="${POPUP_SLOT}"]`)) return;
              close();
            }}
            //  Catalogue local : le focus suffit à proposer tout ce qui existe, sur la saisie
            //  vide plutôt que sur la valeur en place — c'est le geste « montre-moi les
            //  listes », pas « cherche celle-ci ».
            onFocus={
              local
                ? () => {
                    void loadSuggestions("").then((found) => {
                      setItems(found);
                      setOpen(found.length > 0);
                    });
                  }
                : undefined
            }
            render={
              <Input id={id} className="pl-8" placeholder={placeholder} disabled={disabled} />
            }
          />
        </div>

        <Autocomplete.Portal>
          <Autocomplete.Positioner sideOffset={4} className="isolate z-50">
            <Autocomplete.Popup
              data-slot={POPUP_SLOT}
              className="max-h-(--available-height) w-(--anchor-width) min-w-48 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0"
            >
              <Autocomplete.List>
                {(item: SearchSuggestion) => (
                  <Autocomplete.Item
                    key={item.id}
                    value={item}
                    className="flex w-full cursor-default items-center gap-2 rounded-md px-1.5 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground"
                    onClick={() => {
                      close();
                      onSelect(item);
                    }}
                  >
                    <span className="min-w-0 flex-1 truncate">{item.label}</span>
                    {item.hint === undefined ? null : (
                      <span className="text-muted-foreground shrink-0 text-xs tabular-nums">
                        {item.hint}
                      </span>
                    )}
                  </Autocomplete.Item>
                )}
              </Autocomplete.List>
            </Autocomplete.Popup>
          </Autocomplete.Positioner>
        </Autocomplete.Portal>
      </form>
    </Autocomplete.Root>
  );
}
