/**
 * O freio de quem erra a chave em série.
 *
 * Acertar uma chave no chute não acontece: são 256 bits de sorteio. O que este
 * freio evita é outra coisa: alguém usar o endereço do conector para despejar
 * chaves inventadas e fazer o banco trabalhar à toa, uma consulta por chute.
 * Passou do limite de erros num minuto, o endereço de onde eles vêm é recusado
 * antes de qualquer consulta, até a janela virar.
 *
 * Só erros contam. Quem usa uma chave boa nunca é freado por aqui: o limite de
 * uso de uma chave válida é outro, contado por chave, no banco.
 *
 * Mora na memória do processo, que é onde uma rajada acontece. Só regra, sem
 * servidor, para o teste ao lado rodar sozinho.
 */

export const ERROS_POR_MINUTO = 30;
const JANELA_MS = 60_000;
const MAXIMO_DE_ENDERECOS = 20_000;

type Marca = { desde: number; quantos: number };

export function criarFreio(limite: number = ERROS_POR_MINUTO, janelaMs: number = JANELA_MS) {
  const marcas = new Map<string, Marca>();

  const viva = (quem: string, agora: number) => {
    const marca = marcas.get(quem);
    if (!marca) return null;
    if (agora - marca.desde >= janelaMs) {
      marcas.delete(quem);
      return null;
    }
    return marca;
  };

  return {
    /** Este endereço já errou demais nesta janela? Sem endereço conhecido, não há o que frear. */
    bloqueado(quem: string | null, agora: number = Date.now()) {
      if (!quem) return false;
      return (viva(quem, agora)?.quantos ?? 0) >= limite;
    },

    anotar(quem: string | null, agora: number = Date.now()) {
      if (!quem) return;
      const marca = viva(quem, agora);
      if (marca) {
        marca.quantos += 1;
        return;
      }
      /* Um mapa que só cresce seria, ele mesmo, um jeito de derrubar o servidor. */
      if (marcas.size >= MAXIMO_DE_ENDERECOS) marcas.clear();
      marcas.set(quem, { desde: agora, quantos: 1 });
    },
  };
}

/**
 * As chaves que funcionaram há pouco, pelo hash.
 *
 * Vários computadores podem sair para a internet pelo mesmo endereço (um
 * escritório, um provedor). Se um deles dispara chaves erradas e o endereço é
 * freado, quem já vinha usando uma chave boa por ali não pode pagar pelo
 * vizinho: a chave que funcionou há pouco passa pelo freio e segue para a
 * conferência normal no banco.
 */
export function criarLembranca(valePorMs: number = 10 * 60_000, maximo: number = 20_000) {
  const boas = new Map<string, number>();
  return {
    /* `porMs` troca o prazo só desta lembrança. */
    lembrar(hash: string, agora: number = Date.now(), porMs: number = valePorMs) {
      if (boas.size >= maximo) boas.clear();
      boas.set(hash, agora + porMs);
    },
    conhecida(hash: string, agora: number = Date.now()) {
      const ate = boas.get(hash);
      if (ate === undefined) return false;
      if (ate <= agora) {
        boas.delete(hash);
        return false;
      }
      return true;
    },
    esquecer(hash: string) {
      boas.delete(hash);
    },
  };
}

/**
 * O endereço de quem chama. Atrás do Cloudflare, o cabeçalho dele é o que não
 * dá para forjar; sem ele, vale o primeiro do encaminhamento.
 */
export function enderecoDeQuemChama(cabecalhos: { get(nome: string): string | null }): string | null {
  const direto = (cabecalhos.get("cf-connecting-ip") || "").trim();
  if (direto) return direto.slice(0, 64);
  const encaminhado = (cabecalhos.get("x-forwarded-for") || "").split(",")[0]?.trim();
  return encaminhado ? encaminhado.slice(0, 64) : null;
}

/**
 * A faixa do endereço, para contar limites por quem está atrás dele.
 *
 * No IPv4 cada endereço é uma casa. No IPv6 uma casa recebe uma faixa inteira
 * (as primeiras quatro partes, o /64) e pode trocar o resto à vontade: contar
 * por endereço deixaria uma casa só se passar por milhões.
 */
export function faixaDoEndereco(endereco: string | null): string | null {
  if (!endereco) return null;
  const limpo = endereco.trim().toLowerCase().replace(/^\[|\]$/g, "").split("%")[0];
  if (!limpo.includes(":")) return limpo;
  /* IPv4 escrito dentro do IPv6 (::ffff:1.2.3.4) é IPv4. */
  const mapeado = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(limpo);
  if (mapeado) return mapeado[1];
  const [cabeca, cauda] = limpo.split("::");
  const partesDaCabeca = cabeca ? cabeca.split(":") : [];
  const partesDaCauda = cauda !== undefined && cauda ? cauda.split(":") : [];
  const faltam = cauda !== undefined ? Math.max(0, 8 - partesDaCabeca.length - partesDaCauda.length) : 0;
  const partes = [...partesDaCabeca, ...Array.from({ length: faltam }, () => "0"), ...partesDaCauda];
  return `${partes
    .slice(0, 4)
    .map((parte) => parte.replace(/^0+(?=.)/, "") || "0")
    .join(":")}::/64`;
}
