/**
 * A regra de guardar, renovar e recriar a sessão de cada pessoa.
 *
 * Fica separada de quem fala com o banco (`sessao.ts`) para o teste ao lado
 * rodar sozinho, com um relógio de mentira: esperar uma hora de verdade para
 * ver uma sessão vencer não é teste que alguém rode.
 *
 * A ordem é sempre a mais barata primeiro: a sessão guardada, se ainda tem
 * folga; senão a mesma sessão renovada; e só por último uma sessão nova.
 */

export type SessaoGuardada = { accessToken: string; refreshToken: string | null; venceEm: number };

export function criarGuardaDeSessoes(opcoes: {
  criar: (usuario: { id: string }) => Promise<SessaoGuardada>;
  /* `null` = a sessão antiga não vale mais; o jeito é criar outra. */
  renovar: (refreshToken: string) => Promise<SessaoGuardada | null>;
  folgaMs?: number;
  agora?: () => number;
  guardadas?: Map<string, SessaoGuardada>;
  emCurso?: Map<string, Promise<SessaoGuardada>>;
  maximo?: number;
}) {
  const folga = opcoes.folgaMs ?? 5 * 60 * 1000;
  const agora = opcoes.agora ?? Date.now;
  const guardadas = opcoes.guardadas ?? new Map<string, SessaoGuardada>();
  const emCurso = opcoes.emCurso ?? new Map<string, Promise<SessaoGuardada>>();
  const maximo = opcoes.maximo ?? 5000;

  return {
    /** O token de acesso da pessoa: reaproveitado, renovado ou novo. */
    async obter(usuario: { id: string }): Promise<string> {
      const guardada = guardadas.get(usuario.id);
      if (guardada && guardada.venceEm - agora() > folga) return guardada.accessToken;

      /* Dois pedidos ao mesmo tempo esperam a mesma preparação: um segundo
         link de entrada invalidaria o primeiro antes da troca, e renovar duas
         vezes com o mesmo token derruba a sessão. */
      const esperando = emCurso.get(usuario.id);
      if (esperando) return (await esperando).accessToken;

      const preparando = (async () => {
        const renovada = guardada?.refreshToken ? await opcoes.renovar(guardada.refreshToken) : null;
        return renovada ?? (await opcoes.criar(usuario));
      })()
        .then((sessao) => {
          if (guardadas.size >= maximo) guardadas.clear();
          guardadas.set(usuario.id, sessao);
          return sessao;
        })
        .finally(() => {
          emCurso.delete(usuario.id);
        });
      emCurso.set(usuario.id, preparando);
      return (await preparando).accessToken;
    },
  };
}
