/**
 * Quem manda nesta conversa — a pergunta com UMA resposta.
 *
 * ## O defeito que este arquivo existe para consertar
 *
 * "Quem está atendendo?" e "o automático está ligado?" eram respondidas por
 * SETE fatos espalhados: `conversations.status`, `assigned_to_user_id`,
 * `assignee_kind`, `bot_silenced_until`, `last_handoff_at/reason` e
 * `contacts.force_human`. Cada pedaço da tela juntava um subconjunto diferente —
 * o cabeçalho olhava dois (`bot_silenced_until || force_human`), a linha da lista
 * olhava um (`status === 'ai_handling'`, por COR), e o painel direito olhava
 * nenhum. Três leituras parciais do mesmo estado é como se produz uma tela em que
 * ninguém sabe quem manda.
 *
 * Aqui não nasce estado novo: a doutrina DIRC manda **C**alcular antes de
 * duplicar, e uma oitava coluna a sincronizar seria o anti-pattern nº 5. Isto é
 * uma função pura sobre a linha que a rota JÁ devolve.
 *
 * ## Por que estes gates e não outros
 *
 * O espelho é o do MOTOR, não o do desenhista. Quem cala o automático, medido no
 * código de produção:
 *
 *   1. `contacts.force_human`            → `isLeadInHandoff`, `before-send`, worker
 *   2. `conversations.bot_silenced_until`→ `isLeadInHandoff`, worker
 *   3. `conversations.assignee_kind='user'` → worker legado (`assigned_to_human`)
 *      e, desde a migration 0173, consequência de (2): assumir grava silêncio.
 *
 * Um motivo a MAIS na lista seria a tela afirmando sobre o motor uma coisa que o
 * motor não faz. Dois candidatos ficaram FORA de propósito:
 *
 *   * **janela de 24h** — nem todo canal a tem (é uma CAPACIDADE,
 *     `freeformOutsideWindow` em `lib/channels/capabilities.ts`), e quem responde
 *     por isso na tela é o `JanelaSelo`, que consulta a capacidade. Recalcular 24h
 *     aqui diria "o automático está calado porque a janela fechou" em toda conversa
 *     de canal sem janela com mais de um dia, ao lado de um selo dizendo o
 *     contrário. E perguntar de que provider é o canal, aqui, seria o que a
 *     doutrina de restrição de canal proíbe fora de `lib/channels/` — foi o
 *     `lint:channels` que pegou a primeira versão deste comentário.
 *   * **conversa encerrada** — já é o `status`, e o `STATUS_LABEL` do cabeçalho já
 *     a mostra. Ela entra como ESTADO DE COMANDO (`encerrada`), não como motivo.
 *
 * Informação com propósito (invariante 5): cada motivo aqui muda a ação de quem
 * lê. `resposta_humana_recente` existe justamente para dizer **não faça nada** —
 * é a janela deslizante de 5 min do envio manual, que se desfaz sozinha, e hoje a
 * tela oferece um botão de "devolver" para um estado que já vai voltar sozinho.
 */

import { formatDistanceStrict } from "date-fns";
import type { Locale } from "date-fns";

/** As colunas de que esta função precisa — nada além. */
export interface FatosDoComando {
  status: string;
  /**
   * A ORG tem atendimento automático de pé?
   *
   * `undefined` significa "não sei" — leitura em andamento ou que falhou — e é
   * tratado como "não afirme nada", nunca como `false`: dizer "não há automático"
   * por causa de uma requisição que não voltou é a mesma mentira ao contrário.
   * Com `undefined` a função mantém o comportamento de assumir que há, que é o
   * certo para a instalação configurada.
   */
  automaticoDaOrg?: boolean;
  assigned_to_user_id: string | null;
  /** Nome do atendente, quando o servidor conseguiu resolvê-lo (pode ser null). */
  assigned_to_user_name?: string | null;
  assignee_kind?: string | null;
  /** ISO, ou o literal `"infinity"` que o Postgres devolve para o silêncio durável. */
  bot_silenced_until?: string | null;
  /**
   * `conversations.last_handoff_reason` — distingue um handoff formal de uma
   * PAUSA por resposta no celular (`MOTIVO_ATENDIMENTO_MANUAL`), que tem um
   * motivo próprio na tela. Opcional: quem não passa, cai em `"pausado"`.
   */
  last_handoff_reason?: string | null;
  /** A trava do CONTATO — irrevogável pelo agente. */
  force_human?: boolean | null;
  /**
   * Conversa de GRUPO de WhatsApp (`conversations.is_group`). O automático nunca
   * atende grupo (o banco nem emite `message.received` para ele), então grupo sem
   * dono é conversa HUMANA esperando alguém — `aguardando`, na fila humana — e
   * nunca "Automático atendendo". Espelho SQL: `p_is_group` de
   * `fn_comando_da_conversa` (migration 0482).
   */
  is_group?: boolean | null;
  /**
   * O contato pediu para não receber mensagens (`contacts.is_blocked`).
   *
   * Entra aqui porque o motor o trata como parada dura — `before-send.ts` recusa
   * com `select (is_blocked or force_human) as stopped` — e uma tela que dissesse
   * "Automático atendendo" sobre um contato descadastrado afirmaria o oposto do
   * que vai acontecer.
   *
   * **Mas ele NÃO liga `travaVigente`**, e a diferença é deliberada: `travaVigente`
   * é o que acende "Devolver ao automático", e devolver não desfaz um opt-out — o
   * `stopGate` recusaria na mesma. Um botão que aparece e não pode funcionar é
   * controle decorativo, que é pior que ausência de botão.
   */
  is_blocked?: boolean | null;
  /**
   * HÁ AGENTE QUE VAI RESPONDER ESTA CONVERSA? — o fato por NÚMERO que o banco
   * passou a responder na migration 9331 (`automatico_da_conversa(c)`).
   *
   * `undefined` = "não sei" (cache antigo, resposta de antes do campo entrar no
   * `SELECT_COLS`): cai no comportamento de antes, que é o da org. `false` =
   * nenhum agente publicado no número da conversa, ou o contato fora da lista
   * de teste do canal em pré-go-live — nos dois casos NINGUÉM vai responder, e
   * a resposta certa é `aguardando` (a Fila), não `automatico`.
   *
   * A pergunta é a MESMA que o dreno faz antes de gastar
   * (`haQuemAtendaASessao`) mais o portão de pré-go-live; o espelho SQL mora em
   * `fn_automatico_da_conversa` e é casado por
   * `tests/invariants/comando-da-conversa-por-numero.test.ts`.
   */
  automaticoDaConversa?: boolean;
}

export type Comando =
  /** Uma pessoa está no comando. */
  | { quem: "humano"; userId: string; nome: string | null }
  /** O automático está atendendo. */
  | { quem: "automatico" }
  /**
   * Sem dono e sem trava, mas a org NÃO tem atendimento automático de pé — então
   * não há quem responda. É o estado de toda instalação que ainda não configurou
   * agente, e a versão anterior desta função o chamava de "automatico": a tela
   * afirmava que o robô estava cuidando de conversas que ninguém estava
   * respondendo — na primeira impressão, que é P0.
   */
  | { quem: "ninguem" }
  /** Ninguém: o automático saiu e nenhuma pessoa assumiu. É a fila. */
  | { quem: "aguardando" }
  /** Acabou. Nem pessoa nem automático têm o que fazer aqui. */
  | { quem: "encerrada" };

/** Motivo gravado por `pausarIaPorAtendimentoManual` (espelha `atendimento-manual.ts`). */
const MOTIVO_ATENDIMENTO_MANUAL = "Atendimento manual pelo canal (resposta fora do CRM)";

export type MotivoDoSilencio =
  /** Alguém assumiu. Ação: só devolver ao automático libera. */
  | "atendente_no_comando"
  /** `contacts.force_human` — vale para TODAS as conversas deste cliente. */
  | "contato_travado"
  /** Alguém pausou de propósito, ou o automático passou o caso para uma pessoa. */
  | "pausado"
  /**
   * Uma pessoa respondeu o cliente pelo CELULAR (fora do CRM). A pausa é
   * durável: só o comando `#on` pelo celular ou "devolver ao automático" na tela
   * religam a IA. Ação: devolver quando o atendimento humano terminar.
   */
  | "atendimento_pelo_celular"
  /** Janela deslizante do envio manual. Ação: NENHUMA — volta sozinho. */
  | "resposta_humana_recente"
  /**
   * `contacts.is_blocked` — o cliente pediu para sair. Ação: NENHUMA no
   * automático; quem decide reabrir é o cliente, não a equipe.
   */
  | "contato_descadastrado";

export interface ComandoDaConversa {
  comando: Comando;
  /** O automático responderia a próxima mensagem do cliente? */
  automaticoAtivo: boolean;
  /**
   * Existe uma TRAVA vigente a devolver — silêncio na conversa ou `force_human`
   * no contato.
   *
   * Não é o mesmo que `!automaticoAtivo`, e a diferença decide um botão. Uma
   * conversa ENCERRADA tem `automaticoAtivo: false` sem ter trava nenhuma: se o
   * botão de devolver saísse de `!automaticoAtivo`, ele apareceria em TODA
   * conversa fechada, e clicá-lo reabriria uma conversa que ninguém pediu para
   * reabrir. E o contrário também importa — a conversa fechada que ficou com uma
   * trava pendurada é justamente onde a volta mais falta, porque "Liberar" só
   * existe para o dono e a rota recusa quem não é.
   */
  travaVigente: boolean;
  /** Por que ele está calado. `null` quando está ativo. */
  motivo: MotivoDoSilencio | null;
  /**
   * Quando o silêncio se desfaz sozinho — só existe para
   * `resposta_humana_recente`. Nos outros motivos alguém tem de agir, e é a
   * diferença entre "espere" e "faça algo".
   */
  silencioAte: Date | null;
}

/** O literal que o PostgREST devolve para `timestamptz 'infinity'`. */
const INFINITO = "infinity";
/**
 * O gêmeo do INFINITO, e ele NÃO é silêncio.
 *
 * `new Date("-infinity")` também é `Invalid Date`, então sem este ramo ele cairia
 * no fallback de "data ilegível = calado" — e o Postgres, que é quem grava,
 * discorda: `'-infinity' > now()` é **false**. Enquanto a regra vivia só no
 * TypeScript a divergência não tinha como aparecer; a partir do momento em que o
 * banco calcula o mesmo comando, ela vira uma linha vermelha no espelho.
 * Quem está certo é o banco: `-infinity` é um instante no passado infinito, ou
 * seja, um silêncio que já venceu.
 */
const MENOS_INFINITO = "-infinity";

/**
 * `resolved` entra, e a razão de ele não estar aqui antes é uma pergunta
 * diferente: `CONVERSATION_TERMINAL_STATUSES` responde "o que o `exclude_finished`
 * esconde", enquanto este conjunto responde "quem manda". São vizinhos e não são
 * o mesmo — mas para a pergunta do comando, conversa resolvida é conversa que
 * acabou, e deixá-la de fora punha uma conversa resolvida e sem dono na Fila.
 * Medido em 2026-08-30: nenhum código de produção escreve
 * `conversations.status='resolved'` (só há leitores), então isto não muda nada
 * hoje — e passa a importar no instante em que o banco calcular o mesmo comando.
 */
export const STATUS_ENCERRADOS_LISTA = ["closed", "archived", "resolved"] as const;
export const STATUS_ENCERRADOS: ReadonlySet<string> = new Set(STATUS_ENCERRADOS_LISTA);

/**
 * O PRÉ-FILTRO DO COMANDO — o que o banco pode recusar ANTES de avaliar a função.
 *
 * ─── O desperdício que ele conserta (issue #4325) ───────────────────────────
 * `comando_da_conversa` é `stable security definer` (`baseline.sql:51746-51765`):
 * o Postgres NÃO faz inline, então a lista da Fila e as duas contagens a avaliam
 * LINHA A LINHA, antes do `LIMIT`, em todo o histórico fechado da organização.
 * Mas `fn_comando_da_conversa` (baseline:42674-42699) só chega a `aguardando` ou
 * `automatico` quando `assigned_to_user_id is null` E o status está fora de
 * `('closed','archived','resolved')`. Pedir essas duas condições ao banco tira do
 * caminho as linhas que a função já recusaria.
 *
 * ─── O que ele NÃO é ───────────────────────────────────────────────────────
 * Ele não substitui a função: o `comando_da_conversa.in.(…)` continua no `WHERE`
 * e é quem DECIDE. O pré-filtro só reduz o conjunto que ela avalia, e é por isso
 * que os dois têm de ser logicamente implicados um pelo outro — condição
 * NECESSÁRIA de `aguardando`/`automatico`, nada além.
 *
 * ─── Quando ele vale ───────────────────────────────────────────────────────
 * Só quando o comando pedido é subconjunto de `{aguardando, automatico, ninguem}`
 * — `ninguem` (#4012) é o `automatico` rebaixado pelo wrapper quando a
 * organização não tem agente no ar, então herda a mesma condição necessária; sem
 * ele no conjunto a Fila, que pede `aguardando` e `ninguem`, perderia o
 * pré-filtro. Comando
 * vazio (ou qualquer valor fora do conjunto, ou mistura) devolve `null`: a
 * consulta tem de ser, byte a byte, a de antes. A lista de status é a de
 * `STATUS_ENCERRADOS_LISTA` — a MESMA da função SQL, com `resolved`; copiá-la de
 * `CONVERSATION_TERMINAL_STATUSES` daria um filtro frouxo (aquela constante
 * responde outra pergunta e não tem `resolved`).
 */
export const COMANDOS_SEM_DONO_E_ABERTOS = ["aguardando", "automatico", "ninguem"] as const;

export interface PreFiltroDoComando {
  /** `.is("assigned_to_user_id", null)` — sem dono é condição da função. */
  semDono: true;
  /** `.not("status", "in", "(closed,archived,resolved)")` — idem. */
  statusEncerrados: readonly string[];
}

/** Os predicados do pré-filtro, ou `null` quando a consulta é a de sempre. */
export function preFiltroDoComando(
  comando: string | readonly string[] | null | undefined,
): PreFiltroDoComando | null {
  const pedidos = typeof comando === "string" ? [comando] : (comando ?? []);
  if (pedidos.length === 0) return null;
  const dentroDoConjunto = pedidos.every((c) =>
    (COMANDOS_SEM_DONO_E_ABERTOS as readonly string[]).includes(c),
  );
  if (!dentroDoConjunto) return null;
  return { semDono: true, statusEncerrados: STATUS_ENCERRADOS_LISTA };
}

/**
 * Aplica o pré-filtro NA MESMA consulta — a lista e as duas contagens chamam esta
 * função, e é ela que garante que o pedido seja um só em vez de três cópias.
 *
 * O tipo é o mínimo que o builder do PostgREST oferece (`.is`/`.not` encadeando
 * sobre si mesmos), então serve tanto para o `supabase.from(...).select(...)` da
 * lista quanto para a fábrica `countExact()` das contagens.
 */
export interface ConsultaComPreFiltro<Q> {
  is(coluna: string, valor: null): Q;
  not(coluna: string, operador: "in", valor: string): Q;
}

export function comPreFiltroDoComando<Q extends ConsultaComPreFiltro<Q>>(
  consulta: Q,
  comando: string | readonly string[] | null | undefined,
): Q {
  const pre = preFiltroDoComando(comando);
  // Sem pré-filtro a consulta volta INTACTA — nenhum predicado a mais, nenhuma
  // mudança de plano, nenhuma mudança de resposta.
  if (!pre) return consulta;
  return consulta
    .is("assigned_to_user_id", null)
    .not("status", "in", `(${pre.statusEncerrados.join(",")})`);
}

/**
 * O silêncio, lido do jeito que o Postgres o entrega.
 *
 * ## Um ramo só, e a razão é uma sabotagem que não pegou
 *
 * A primeira versão tinha DOIS caminhos: um `if (valor === INFINITO)` explícito e,
 * depois, um fallback para data ilegível. Apagar o primeiro deixou os 20 casos
 * VERDES — porque `new Date("infinity")` já é `Invalid Date` (medido), então o
 * fallback devolvia exatamente o mesmo resultado. Dois caminhos para uma saída é
 * um ramo que nenhum teste consegue distinguir: a guarda parecia existir e não
 * existia.
 *
 * Com um ramo só, a asserção de que `'infinity'` cala o automático volta a ter
 * dentes — trocá-la por "data ilegível = sem silêncio" reprova na hora.
 *
 * E a direção da falha é deliberada: valor que não sabemos ler é tratado como
 * CALADO. Dizer "o automático está ativo" em cima de um dado ilegível é a frase
 * tranquilizadora que a doutrina proíbe — falha fechada na ação, aberta na
 * informação. `INFINITO` fica nomeado porque é quem o leitor vem procurar.
 */
export function silencioVigente(
  valor: string | null | undefined,
  agora: Date,
): { vigente: boolean; duravel: boolean; ate: Date | null } {
  if (valor === null || valor === undefined) return { vigente: false, duravel: false, ate: null };
  if (valor === MENOS_INFINITO) return { vigente: false, duravel: false, ate: null };
  const ate = new Date(valor);
  if (valor === INFINITO || Number.isNaN(ate.getTime())) {
    return { vigente: true, duravel: true, ate: null };
  }
  return { vigente: ate.getTime() > agora.getTime(), duravel: false, ate };
}

export function comandoDaConversa(fatos: FatosDoComando, agora: Date = new Date()): ComandoDaConversa {
  const silencio = silencioVigente(fatos.bot_silenced_until, agora);
  const travado = fatos.force_human === true;
  const bloqueado = fatos.is_blocked === true;
  const encerrada = STATUS_ENCERRADOS.has(fatos.status);
  const grupo = fatos.is_group === true;

  const comando: Comando = fatos.assigned_to_user_id
    ? {
        quem: "humano",
        userId: fatos.assigned_to_user_id,
        nome: fatos.assigned_to_user_name ?? null,
      }
    : encerrada
      ? { quem: "encerrada" }
      : // Sem dono: quem manda depende do automático estar de pé. Calado e sem
        // dono é a conversa que o automático escalou e ninguém pegou — a fila.
        // Grupo entra junto: o automático nunca o atende, é sempre de humano.
        grupo || silencio.vigente || travado || bloqueado
        ? { quem: "aguardando" }
        : // As duas perguntas da #4012, NA MESMA ORDEM do wrapper do banco
          // (`comando_da_conversa`, migration 9331): primeiro a organização,
          // depois o número. Organização sem agente no ar é `ninguem` — não há
          // robô nenhum nesta instalação.
          fatos.automaticoDaOrg === false
          ? { quem: "ninguem" }
          : // Há agente na organização, mas não publicado NESTE número (ou o
            // contato está fora da lista de teste do canal): a conversa espera
            // uma PESSOA, que é a fila. Os dois valores estão em
            // `comandosDaFila`, então a Fila mostra a conversa nos dois casos.
            fatos.automaticoDaConversa === false
            ? { quem: "aguardando" }
            : { quem: "automatico" };

  /**
   * Encerrada COM dono continua nomeando quem atendeu.
   *
   * A versão anterior colapsava para `encerrada` e apagava o nome — justamente na
   * aba "Fechadas", que é onde a pergunta "quem atendeu isto?" é a única que
   * importa. O produto não solta o dono ao fechar de propósito ("quem atendeu é
   * histórico"), e a tela estava jogando esse histórico fora. Que a conversa
   * acabou já é dito pelo selo de status, ao lado.
   */
  const comandoFinal: Comando = comando;

  const automaticoAtivo =
    !grupo && !encerrada && !travado && !bloqueado && !silencio.vigente && fatos.automaticoDaConversa !== false;

  const motivo: MotivoDoSilencio | null = automaticoAtivo
    ? null
    : grupo
      ? null // Grupo não é automático pausado: o automático nunca o atende.
      : encerrada
      ? null // Encerrada não é silêncio: é ausência de assunto. O estado já diz.
      : fatos.automaticoDaConversa === false && !silencio.vigente
        ? // Sem agente NESTE número (ou fora da lista de teste) não há
          // automático PAUSADO a explicar: não há automático aqui. Dizer
          // "volta em instantes" seria a mesma mentira, ao contrário.
          null
      : bloqueado
        ? // ANTES de `travado`, de propósito: quando as duas valem, é o opt-out que
          // decide a AÇÃO — não há nenhuma. Nomear a trava menor faria a tela
          // sugerir um "devolver" que o `stopGate` recusaria na sequência.
          "contato_descadastrado"
        : travado
          ? "contato_travado"
          : // Ordem importa: a trava do CONTATO é mais forte e mais ampla que a da
            // conversa, então ela nomeia o motivo mesmo havendo silêncio local —
            // senão a tela ofereceria "devolver ao automático" explicando o motivo
            // menor, e a pessoa clicaria esperando o efeito errado.
            silencio.duravel
            ? fatos.assigned_to_user_id
              ? "atendente_no_comando"
              : fatos.last_handoff_reason === MOTIVO_ATENDIMENTO_MANUAL
                ? "atendimento_pelo_celular"
                : "pausado"
            : "resposta_humana_recente";

  return {
    comando: comandoFinal,
    automaticoAtivo,
    // `bloqueado` ANULA a trava devolvível: veja o comentário de `is_blocked` em
    // `FatosDoComando`. Devolver não desfaz opt-out, e o botão seria decorativo.
    // Grupo também: "Devolver ao automático" num grupo prometeria o que não existe.
    travaVigente: (travado || silencio.vigente) && !bloqueado && !grupo,
    motivo,
    silencioAte: motivo === "resposta_humana_recente" ? silencio.ate : null,
  };
}

/**
 * O que a tela ESCREVE para cada estado. Fica aqui, ao lado da regra, porque foi
 * ter duas listas em arquivos diferentes que fez a timeline e o banco divergirem
 * (ver o cabeçalho de `lib/leads/activity-vocabulary.ts`).
 *
 * A palavra do estado é **"automático"**, nunca "IA": ela já é contrato em quatro
 * arquivos (`ConversationHeader`, `BudgetCard`, `orcamento.ts`, `dicionario.ts`) e
 * está travada por `tests/unit/handoff-por-orcamento.test.ts`, cujo controle
 * NEGATIVO usa literalmente "Voltar para a IA" como a sabotagem que deve reprovar.
 */
export const ROTULO_DO_COMANDO: Record<Comando["quem"], string> = {
  humano: "Em atendimento",
  automatico: "Automático atendendo",
  ninguem: "Sem atendente",
  aguardando: "Aguardando atendente",
  encerrada: "Encerrada",
};

export const ROTULO_DO_MOTIVO: Record<MotivoDoSilencio, string> = {
  atendimento_pelo_celular: "Automático pausado — atendimento pelo celular (#on religa)",
  atendente_no_comando: "Automático pausado — alguém assumiu",
  contato_travado: "Automático pausado para este cliente",
  pausado: "Automático pausado",
  resposta_humana_recente: "Automático volta em instantes",
  contato_descadastrado: "Cliente pediu para não receber mensagens",
};

/**
 * O VOCABULÁRIO QUE O BANCO FALA — cinco, desde a issue #4012.
 *
 * `fn_comando_da_conversa` (migration 0202) devolve os quatro primeiros; o quinto,
 * `ninguem`, o banco passou a devolver quando a ORGANIZAÇÃO não tem agente no ar
 * (`comando_da_conversa` rebaixa `automatico` → `ninguem` via
 * `fn_org_tem_automatico`, migration 9330).
 *
 * Até a #4012 `ninguem` era renomeação SÓ de TS, e a razão escrita aqui era que
 * "a org tem automático?" só o servidor sabia responder. Era verdade pela metade:
 * o servidor *compensava* na consulta (`comandosDaFila`), mas as funções do BANCO
 * que decidem pelo comando (`fn_definir_carteira_do_cliente`, a adoção de
 * `fn_conversation_assign`) não tinham como compensar — e aplicavam a regra "a
 * conversa da IA fica com a IA" numa instalação sem IA. O fato virou SQL porque o
 * banco precisa dele para decidir, e a régua é uma: o espelho de `agenteAtende`
 * em `fn_org_tem_automatico`.
 */
export const COMANDOS_DO_BANCO = [
  "humano",
  "automatico",
  "aguardando",
  "encerrada",
  "ninguem",
] as const;
export type ComandoDoBanco = (typeof COMANDOS_DO_BANCO)[number];

/**
 * O QUE A ABA "FILA" PEDE, e é aqui que o fato org-wide entra.
 *
 * Numa org COM automático de pé, "precisa de uma pessoa agora" é `aguardando`: o
 * resto o robô atende. Numa org SEM automático nenhum, `automatico` não descreve
 * ninguém — não há robô — e essas conversas também estão esperando gente; deixá-las
 * fora faria a Fila de uma instalação recém-instalada nascer VAZIA com dezenas de
 * clientes sem resposta, que é o pior estado possível na primeira impressão.
 *
 * `undefined` ("não sei", leitura em andamento ou que falhou) segue a mesma
 * convenção do resto do arquivo: assume que HÁ automático. Errar para o lado de
 * mostrar menos na fila é recuperável em ~200ms; errar para o outro pinta 83
 * linhas de trabalho humano que não existe.
 */
export function comandosDaFila(automaticoDaOrg?: boolean): ComandoDoBanco[] {
  // `ninguem` está SEMPRE: desde a #4012 o banco é quem diz que a conversa sem dono
  // não tem robô por trás (a organização não tem agente no ar), e essa conversa
  // espera uma pessoa. Pedir o valor independe do fato org-wide que a TELA conhece,
  // que pode estar carregando ou ter falhado. A conversa de um número sem agente,
  // numa organização que tem agente em outro, sai `aguardando` (a segunda pergunta
  // do mesmo wrapper, `fn_automatico_da_conversa`, migration 9331).
  //
  // `automatico` continua aqui quando o fato diz que NÃO há automático, e agora por
  // um motivo com prazo: entre o deploy deste código e a migration 9330 aplicada, o
  // banco ainda responde `automatico` para essas conversas. Sem o valor extra, a
  // Fila de uma instalação sem IA nasceria vazia nessa janela — exatamente o P0 que
  // o `ninguem` veio consertar.
  return automaticoDaOrg === false
    ? ["aguardando", "ninguem", "automatico"]
    : ["aguardando", "ninguem"];
}

/**
 * A ORDEM DA FILA, num lugar só — mais tempo esperando primeiro.
 *
 * ─── A régua é a mensagem mais ANTIGA sem resposta, não a última (issue #990) ─
 *
 * A ordem era por `last_inbound_at`, a ÚLTIMA mensagem do cliente — e essa coluna
 * é reescrita a cada mensagem nova (`fn_mark_conversation_message`, migration
 * 0267). O efeito era o oposto do pretendido: o cliente que insiste (pergunta,
 * cobra, escreve de novo) REINICIAVA a própria espera e descia para o fim da
 * fila, atrás de quem escreveu uma vez e ficou quieto. Não é uma lista
 * desordenada na tela — é a pessoa que mais está tentando ser atendida sendo a
 * última a ser atendida.
 *
 * A régua passa a ser `awaiting_since`: o instante da mensagem do cliente mais
 * antiga que ninguém respondeu ainda (min(sent_at) dos inbound posteriores a
 * `last_outbound_at`, no atendimento em curso). Ela é COLUNA, e não expressão,
 * porque a ordem da Fila é um `order by` pedido ao PostgREST pela rota da lista e
 * o PostgREST ordena por coluna — a expressão mora em `messages` e depende de
 * `last_outbound_at`.
 *
 * `last_message_at` continua fora: ele anda quando o atendente responde (a
 * conversa que o cliente abandonou há dois dias volta ao topo assim que ele
 * recebe uma resposta hoje), e `created_at` pode ser de uma conversa antiga
 * reaberta. `nullsFirst: false` põe quem nunca recebeu mensagem no fim; o `id` é
 * o desempate, e sem ele duas conversas com o mesmo instante trocam de lugar
 * entre duas leituras — a linha "pula" sozinha na tela.
 *
 * Quem consome: a rota da lista (`app/api/v1/conversations/_handler.ts`, a aba
 * Fila), o mapa de posições e o resumo da fila (`lib/routing/queue.ts` — o "3º"
 * que o atendente lê na linha e o número que o cliente ouve no WhatsApp) e a
 * pílula "Aguardando há…" com a hora do canto da linha
 * (`components/inbox/ConversationListItem.tsx`, via `esperaDaConversa`). Enquanto
 * cada um escrevia a sua, a mesma conversa podia aparecer em 2º numa e 5ª na
 * outra — ordenada por uma pergunta e numerada por outra, com a espera da linha
 * dizendo um tempo que a ordem não respeitava —, sem nada ficar vermelho.
 */
/**
 * "Este pedido é o da Fila?" — a mesma pergunta na rota e na lista.
 *
 * A ordem já mora em `ORDEM_DA_ESPERA`, mas o PREDICADO que decide se ela vale
 * continuava escrito duas vezes: em `app/api/v1/conversations/_handler.ts` (que
 * ordena) e em `components/inbox/ConversationList.tsx` (que numera "1º, 2º…" e
 * mostra o tempo de espera). Ganhar uma condição num só dos dois — uma aba nova,
 * um filtro — produz a tela ordenada por uma pergunta e numerada por outra, sem
 * nada ficar vermelho. É a mesma classe que o #994 veio fechar para a ordem.
 */
export function ehAFila(f: {
  comando?: readonly string[] | null;
  assigned_to?: string | null;
}): boolean {
  return f.comando?.includes("aguardando") ?? f.assigned_to === "unassigned";
}

export const ORDEM_DA_ESPERA = {
  coluna: "awaiting_since",
  opcoes: { ascending: true, nullsFirst: false },
} as const;

/**
 * O INSTANTE que a Fila trata como espera, para quem já tem a linha na mão — a
 * contraparte de `ORDEM_DA_ESPERA`, que é a COLUNA pedida ao banco.
 *
 * Existe porque o mesmo instante é mostrado em dois lugares da linha (a pílula
 * "Aguardando há…" e a hora do canto) e o valor não pode sair de duas cadeias de
 * fallback escritas à mão: `?? created_at` num lugar e `?? last_message_at` no
 * outro é exatamente a divergência de 40px que a #464 fechou.
 *
 * `awaiting_since` é a régua (migration 0267). `last_inbound_at` fica no meio da
 * cadeia por um motivo com prazo: entre o deploy deste código e a migration
 * aplicada, a coluna ainda não vem na linha que o realtime entrega, e sem esse
 * degrau a Fila inteira mostraria a data de criação — uma tela plausível e
 * errada. `created_at` é o último degrau, para a conversa que nunca recebeu
 * mensagem do cliente.
 */
export function esperaDaConversa(c: {
  awaiting_since?: string | null;
  last_inbound_at?: string | null;
  created_at?: string | null;
}): string | null {
  return c.awaiting_since ?? c.last_inbound_at ?? c.created_at ?? null;
}

/**
 * O TEXTO da pílula ("Aguardando há 5 min"), a partir do MESMO instante.
 *
 * `esperaDaConversa` responde "desde quando?"; este formatador diz COMO a
 * resposta aparece. Eles nasceram separados de propósito: enquanto o texto era
 * uma função privada dentro de `ConversationListItem.tsx`, a única forma de
 * provar que a pílula lia a régua certa era renderizar o componente inteiro — e
 * foi assim que a #990 passou tanto tempo sem nenhum teste vermelho. Aqui a
 * régua e o texto dela moram no mesmo módulo, e `agora` é PARÂMETRO: com
 * `new Date()` escondido lá dentro, um caso que anda o relógio não teria como
 * prender o arredondamento (dez minutos viram "há 9 minutos" no meio do teste).
 *
 * Sem instante (conversa que nunca recebeu mensagem do cliente), sai o rótulo
 * seco "Aguardando" — inventar uma data seria pior que não dizer nenhuma.
 */
export function rotuloDaEspera(
  desde: string | null,
  agora: Date,
  t: (texto: string) => string = (texto) => texto,
  locale?: Locale,
): string {
  if (!desde) return t("Aguardando");
  return `${t("Aguardando")} ${formatDistanceStrict(new Date(desde), agora, {
    addSuffix: true,
    ...(locale ? { locale } : {}),
  })}`;
}
