/* ============================================================
 * Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com
 * Rate limit por IP — hoje só a página pública de verificação usa isto.
 *
 * Guardado no próprio PostgreSQL, não em Redis: o stack documentado
 * (docs/escopo.md §15) prevê Redis para filas, mas isso ainda não existe
 * neste servidor, e um contador por IP não precisa de mais que o banco que
 * já está de pé. Se o volume um dia doer, trocar o storage é uma decisão
 * isolada — esta classe é a única coisa no sistema que sabe onde o
 * contador mora.
 *
 * Janela fixa, não deslizante: mais barata (um UPSERT, sem varrer histórico
 * por IP) e suficiente para o que isto protege — não é um limite de
 * cobrança, é um freio contra varredura de códigos.
 * ============================================================ */

import type { Pool } from '../db/client.ts';
import { MuitasRequisicoes } from '../core/errors.ts';

export class RateLimitService {
  constructor(
    private readonly pool: Pool,
    private readonly limite = 30,
    private readonly janelaSegundos = 60,
  ) {}

  async exigirDentroDoLimite(ip: string): Promise<void> {
    const conn = await this.pool.connect();
    try {
      const { rows } = await conn.query(
        `INSERT INTO recibos.rate_limit_verificacao_publica (ip, janela_inicio, contagem)
         VALUES ($1::inet, to_timestamp(floor(extract(epoch FROM now()) / $2) * $2), 1)
         ON CONFLICT (ip, janela_inicio) DO UPDATE
           SET contagem = recibos.rate_limit_verificacao_publica.contagem + 1
         RETURNING contagem`,
        [ip, this.janelaSegundos],
      );

      // Limpeza oportunista — não vale um job só para isto. 1 em 20
      // chamadas apaga o que já saiu de qualquer janela plausível.
      if (Math.random() < 0.05) {
        await conn.query(
          `DELETE FROM recibos.rate_limit_verificacao_publica
            WHERE janela_inicio < now() - interval '1 hour'`,
        );
      }

      if (rows[0].contagem > this.limite) throw new MuitasRequisicoes();
    } finally {
      conn.release();
    }
  }
}
