/**
 * Coalescência de rajada inbound — a janela que junta as mensagens de UM contato
 * em UM turno.
 *
 * Morava inline no `drain.ts`; saiu para cá com teste próprio (issue #1390)
 * porque o comportamento carrega duas leis que se cruzam — a janela de debounce
 * e a exclusão do job em HOLD — e as duas precisam de régua própria.
 *
 * Contrato:
 *   - mensagem que chega enquanto há job PENDING **do mesmo contato** com a
 *     janela ainda aberta (`run_after > now()`) viaja de carona nele: o turno lê
 *     o histórico completo e responde a todas as mensagens do lote;
 *   - mensagem que chega fora da janela abre janela nova;
 *   - `debounceMs = 0` desliga a coalescência: job imediato, zero consulta.
 *
 * A janela é ANCORADA no primeiro job, não deslizante: mensagens que chegam
 * dentro dela não empurram o `run_after` para frente (nada aqui faz update de
 * `run_after`). Quem mede o pior caso com contato falante é
 * `debounce.medicao.test.ts`.
 */
import type pg from 'pg';

import { adiadoPelaJanelaDeResposta } from '../../pacing/adiados-da-janela';

/**
 * Job pendente do contato cuja janela ainda não venceu — a carona desta
 * mensagem.
 *
 * ⚠️ `run_after > now()` sozinho casa com um job em HOLD (`enforceHolds`,
 * session-watchdog.ts), que usa `run_after = 'infinity'` como marcador, e
 * 'infinity' É maior que `now()`. Um job em hold por sessão MORTA (WhatsApp
 * reconectado, sessão antiga arquivada) nunca libera — a condição de liberação
 * exige a MESMA sessão antiga voltar a 'WORKING', o que não acontece nunca.
 * Sem esta exclusão, TODA mensagem nova do mesmo contato — inclusive na sessão
 * NOVA — coalescia nesse job morto para sempre: o cliente escrevia, o evento
 * saía "done" sem erro nenhum, e nenhum turno rodava. Medido em produção
 * (2026-09-14): 6 mensagens ao longo de 7h, zero resposta, zero job novo — só o
 * coalescing silencioso repetido no mesmo job com `held_run_after` no payload.
 */
const SQL_JOB_PARA_COALESCER = `select id, payload from job_queue
 where organization_id = $1 and contact_id = $2
   and kind = 'inbound_turn' and status = 'pending' and run_after > now()
   and not (payload ? 'held_run_after')
 limit 1`;

/** O que fazer com a mensagem que acabou de chegar. */
export type DecisaoDeRajada =
  | { tipo: 'coalescido'; jobId: string }
  | {
      tipo: 'enfileirar';
      runAfter: Date | undefined;
      /**
       * Job que ERA o candidato natural à carona mas está adiado pela JANELA DE
       * RESPOSTA que já ABRIU (#3383): não recebe a mensagem, e quem acorda o
       * adiado é quem chama (`drain.ts` — ver `soltarAdiadoPelaJanela`).
       */
      caronaRecusadaEm?: string;
    };

/** Job pendente do contato que pode receber a mensagem de carona. */
export interface CandidatoDeCarona {
  id: string;
  /** `true` quando o job espera a ABERTURA da janela de resposta (rastro do #3383). */
  adiadoPelaJanelaDeResposta: boolean;
}

export async function buscarJobParaCoalescer(
  pool: pg.Pool,
  alvo: { organizationId: string; contactId: string },
): Promise<CandidatoDeCarona | undefined> {
  const { rows } = await pool.query<{ id: string; payload: unknown }>(SQL_JOB_PARA_COALESCER, [
    alvo.organizationId,
    alvo.contactId,
  ]);
  const row = rows[0];
  if (row === undefined) return undefined;
  return { id: row.id, adiadoPelaJanelaDeResposta: adiadoPelaJanelaDeResposta(row.payload) };
}

/**
 * Janela da rajada aberta por esta mensagem: `undefined` quando não há debounce
 * (o job nasce claimável agora).
 *
 * `agora` é injetável porque a janela é aritmética — o teste prende o número
 * sem depender do relógio.
 */
export function janelaDeRajada(debounceMs: number, agora: number = Date.now()): Date | undefined {
  return debounceMs > 0 ? new Date(agora + debounceMs) : undefined;
}

/**
 * Teto da janela de rajada configurável por agente (#1856): não deixar ninguém
 * travar o atendimento sem querer. Vale também como default de tela/UX.
 */
export const TETO_DEBOUNCE_MS = 60_000;

/**
 * Janela de rajada EFETIVA de um agente.
 *
 * `null`/`undefined` (campo vazio na versão) = o `INBOUND_DEBOUNCE_MS` da
 * instalação — regressão zero para quem nunca mexeu no campo. O valor
 * configurado é CLAMPADO em [0, TETO_DEBOUNCE_MS]: 0 desliga a coalescência e
 * valores acima do teto (dado sujo que entrou por fora da validação) caem para
 * 60s em vez de travar o atendimento.
 */
export function debounceEfetivo(
  configurado: number | null | undefined,
  padraoInstalacao: number,
): number {
  if (configurado === null || configurado === undefined) return padraoInstalacao;
  return Math.min(Math.max(0, configurado), TETO_DEBOUNCE_MS);
}

/**
 * Decide entre carona em job existente e janela nova.
 *
 * `janelaDeRespostaAberta` (#3383) é opcional e SÓ é consultada quando o
 * candidato carrega o rastro `adiado_por_janela` — job de debounce (8 s), cap,
 * saldo ou sessão não pagam a leitura e continuam exatamente como antes.
 *
 * Com a janela ABERTA, o job adiado pela janela não recebe carona: a mensagem
 * nova enfileira o próprio turno (e o chamador acorda o adiado, que senão
 * dormiria até o horário antigo). Com a janela FECHADA o comportamento é o de
 * sempre — a mensagem viaja de carona e os dois respondem juntos às 7h.
 * Sem o quinto argumento (todos os chamadores antigos), nada muda.
 */
export async function decidirRajada(
  pool: pg.Pool,
  alvo: { organizationId: string; contactId: string },
  debounceMs: number,
  agora: number = Date.now(),
  janelaDeRespostaAberta?: () => Promise<boolean>,
): Promise<DecisaoDeRajada> {
  if (debounceMs > 0) {
    const candidato = await buscarJobParaCoalescer(pool, alvo);
    if (candidato !== undefined) {
      if (
        candidato.adiadoPelaJanelaDeResposta &&
        janelaDeRespostaAberta !== undefined &&
        (await janelaDeRespostaAberta())
      ) {
        return {
          tipo: 'enfileirar',
          runAfter: janelaDeRajada(debounceMs, agora),
          caronaRecusadaEm: candidato.id,
        };
      }
      return { tipo: 'coalescido', jobId: candidato.id };
    }
  }
  return { tipo: 'enfileirar', runAfter: janelaDeRajada(debounceMs, agora) };
}
