/**
 * Débounce : n'exécute qu'après un silence de `delay` ms, et **numérote les exécutions**.
 *
 * Deux problèmes distincts, et c'est pour le second que ce module existe séparément du
 * composant. Le premier — ne pas partir à chaque frappe — est le débounce ordinaire. Le
 * second est la **course** : deux requêtes lancées à 250 ms d'intervalle peuvent revenir dans
 * l'ordre inverse, et la plus ancienne écraserait alors la plus récente. L'écran afficherait
 * les suggestions de « jewel » sur une saisie devenue « jewelry manufacturing ».
 *
 * D'où le jeton passé au travail : `isCurrent()` répond `false` dès qu'une exécution plus
 * récente a été programmée, et l'appelant jette son résultat au lieu de le rendre. C'est la
 * seule annulation qui vaille côté client — `AbortController` ne s'applique pas à une Server
 * Action, dont l'appel n'est pas un `fetch` que l'on tient.
 *
 * Logique volontairement pure et sans React : c'est ce qui la rend testable au temps
 * simulé, sans monter de composant.
 */
export type Debounced<Args extends unknown[]> = {
  /** Programme un appel ; tout appel précédent encore en attente est abandonné. */
  run: (...args: Args) => void;
  /** Abandonne l'appel en attente **et** périme les exécutions déjà parties. */
  cancel: () => void;
};

export function debounce<Args extends unknown[]>(
  work: (isCurrent: () => boolean, ...args: Args) => void,
  delay: number,
): Debounced<Args> {
  let timer: ReturnType<typeof setTimeout> | null = null;
  //  Numéro de la dernière exécution *programmée*. Une exécution partie reste « courante »
  //  tant que ce compteur n'a pas bougé — c'est-à-dire tant que l'utilisateur n'a pas retapé.
  let generation = 0;

  const clear = () => {
    if (timer !== null) clearTimeout(timer);
    timer = null;
  };

  return {
    run: (...args: Args) => {
      clear();
      generation += 1;
      const mine = generation;
      timer = setTimeout(() => {
        timer = null;
        work(() => generation === mine, ...args);
      }, delay);
    },
    cancel: () => {
      clear();
      //  Périmer aussi ce qui est déjà en vol : sans cette ligne, une réponse arrivée après
      //  la fermeture du menu le rouvrirait.
      generation += 1;
    },
  };
}
