/**
 * O ÚNICO lugar do sistema que pode conhecer a diferença entre os canais.
 *
 * Feature nenhuma pergunta *com quem* falamos — pergunta *o que o canal permite*
 * (invariante 1 de `docs/doctrine/restricao-de-canal.md`). Cada capability abaixo
 * nasce de uma diferença real e medida entre WAHA e Meta Cloud; capability que
 * ninguém consome é código morto, e o teste de matriz reprova.
 */
import type { ChannelCapabilities, ChannelProvider, ProviderDeMensagem } from "./types";

export type { ChannelProvider, ChannelCapabilities, ProviderDeMensagem };

/**
 * A matriz descreve o que um canal de MENSAGEM permite — por isso a chave é
 * `ProviderDeMensagem`, não `ChannelProvider`. Perguntar a uma linha de voz se
 * ela manda texto fora da janela de 24h é erro de categoria, e responder
 * qualquer coisa (inclusive tudo `false`) faria a pergunta parecer legítima.
 * `capabilitiesOf` segue falhando fechado para quem não está aqui.
 */
export const CHANNEL_CAPABILITIES: Record<ProviderDeMensagem, ChannelCapabilities> = {
  // Auto-restrição: falo quando quiser, mas o WhatsApp me bane se eu abusar.
  waha: {
    freeformOutsideWindow: true,
    requiresTemplates: false,
    // Não há WABA por trás: não existe definição aprovada para gerir.
    canManageTemplates: false,
    banRisk: true,
    minIntervalMs: null,
    voiceNote: "server-convert",
    groups: "full",
    costPerMessage: false,
    alteraMensagemEnviada: true,
    podeReagir: true,
    transmissao: false,
    // É a linha que conversa com um APARELHO pareado: a agenda salva no
    // celular está lá dentro, e o `contacts/all` a devolve paginada (#2736).
    phonebook: true,
    // Status é do WhatsApp com aparelho atrás: só a linha por QR tem o
    // endpoint de publicação. Ver `publishStatus` em `./types`.
    statusUpdates: true,
    // Sem ponte de conversão: a venda desta linha sai pela conexão direta com a Meta
    // (conjunto de dados + token). Declarar `true` ofereceria uma chave que não envia nada.
    reportaConversao: false,
  },
  // Hetero-restrição: não me banem, mas a Meta me proíbe e me cobra.
  meta_cloud: {
    freeformOutsideWindow: false,
    requiresTemplates: true,
    // A Graph API cria e edita definições; o repo hoje só ESPELHA, e é essa
    // lacuna que a capability torna visível em vez de deixar implícita.
    canManageTemplates: true,
    banRisk: false,
    minIntervalMs: 6000,
    voiceNote: "opus-only",
    groups: "limited",
    costPerMessage: true,
    alteraMensagemEnviada: false,
    podeReagir: true,
    transmissao: false,
    // Nenhuma destas linhas conversa com um aparelho pareado — não existe
    // agenda de celular a ler, e dizer `true` aqui seria um botão que só falha.
    phonebook: false,
    // A API oficial não tem endpoint de Status: o gesto só existe no app do
    // WhatsApp, atrás de um aparelho pareado. Declarar `true` aqui ofereceria
    // um botão que sempre falha (ver `publishStatus` em `./types`).
    statusUpdates: false,
    // Sem ponte de conversão: a venda desta linha sai pela conexão direta com a Meta
    // (conjunto de dados + token). Declarar `true` ofereceria uma chave que não envia nada.
    reportaConversao: false,
  },
  // Mesma hetero-restrição do canal oficial, por baixo: é um BSP: a WABA é da
  // Meta, os templates são aprovados pela Meta e a janela de 24h é da Meta. O
  // intermediário muda o TRANSPORTE (quem endereça, como se autentica), não o
  // que o WhatsApp permite — e capability descreve o permitido, não o encanamento.
  //
  // As duas diferenças reais, medidas na doc do provider, não na intuição:
  //
  //  - `voiceNote: "opus-only"`. O provider tem um `voiceNote: true` no envio,
  //    mas exige ogg/opus mono explicitamente e NÃO converte — mesma restrição
  //    do canal oficial. Ler o campo booleano como "ele resolve para mim" é o
  //    erro que manda mp3 e entrega anexo de música.
  //  - `groups: "limited"`. Existe API de grupos, mas só em plano de uso e só
  //    para números fora de coexistência. Capability é o que a instalação MÉDIA
  //    pode fazer; prometer "full" aqui quebraria em quem não paga o plano.
  // `freeformOutsideWindow: false` está MEDIDO, não deduzido. A API aceita o
  // envio livre (200 + wamid) e a Meta recusa a ENTREGA depois, pelo webhook:
  //
  //   131047 Re-engagement message — "The 24-hour customer service window for
  //   this contact is closed. Send an approved template to re-open the
  //   conversation, or wait for the contact to message you first."
  //
  // O detalhe que engana: mandar um template NÃO abre a janela. Só o cliente
  // abre, respondendo. Quem ler o 200 como "enviado" acha que funciona.
  zernio_social: {
    freeformOutsideWindow: false,
    requiresTemplates: false,
    canManageTemplates: false,
    banRisk: false,
    minIntervalMs: 1000,
    voiceNote: "server-convert",
    groups: "none",
    costPerMessage: true,
    alteraMensagemEnviada: false,
    podeReagir: false,
    transmissao: false,
    // Nenhuma destas linhas conversa com um aparelho pareado — não existe
    // agenda de celular a ler, e dizer `true` aqui seria um botão que só falha.
    phonebook: false,
    // A API oficial não tem endpoint de Status: o gesto só existe no app do
    // WhatsApp, atrás de um aparelho pareado. Declarar `true` aqui ofereceria
    // um botão que sempre falha (ver `publishStatus` em `./types`).
    statusUpdates: false,
    // Sem ponte de conversão: a venda desta linha sai pela conexão direta com a Meta
    // (conjunto de dados + token). Declarar `true` ofereceria uma chave que não envia nada.
    reportaConversao: false,
  },
  zernio: {
    freeformOutsideWindow: false,
    requiresTemplates: true,
    canManageTemplates: true,
    banRisk: false,
    minIntervalMs: 6000,
    voiceNote: "opus-only",
    groups: "limited",
    costPerMessage: true,
    alteraMensagemEnviada: false,
    podeReagir: false,
    // MEDIDO em 16/10/2026 (2 destinatários, entregues em 1 a 3 s): o provedor
    // cria a transmissão, recebe os telefones e manda a todos. O valor de cada
    // variável sai LITERAL do `components` — o `variableMapping` documentado
    // chegou ao destinatário como "{{1}}", e o nome que ele guarda é o número.
    transmissao: true,
    // Nenhuma destas linhas conversa com um aparelho pareado — não existe
    // agenda de celular a ler, e dizer `true` aqui seria um botão que só falha.
    phonebook: false,
    // A API oficial não tem endpoint de Status: o gesto só existe no app do
    // WhatsApp, atrás de um aparelho pareado. Declarar `true` aqui ofereceria
    // um botão que sempre falha (ver `publishStatus` em `./types`).
    statusUpdates: false,
    // O provedor tem a ponte: reporta a venda ao conjunto de dados da Meta pela
    // conversa (`reportConversion` no adapter, `../zernio/conversoes.ts`).
    reportaConversao: true,
  },
  // Parceiro homologado pela Meta que espelha a Cloud API (recorte do #1130):
  // a WABA, a janela de 24h e o custo são da Meta. O parceiro muda o TRANSPORTE
  // (host, token), não o que o WhatsApp permite — então o perfil é o do canal
  // oficial.
  //
  // `canManageTemplates: true`: os modelos são os da Cloud API e o parceiro
  // expõe os mesmos endpoints de catálogo; a tela cria e sincroniza por lá.
  // `requiresTemplates: true` porque a regra da Meta é real: fora da janela de
  // 24h, só modelo aprovado passa.
  datafy: {
    freeformOutsideWindow: false,
    requiresTemplates: true,
    canManageTemplates: true,
    banRisk: false,
    minIntervalMs: 6000,
    voiceNote: "opus-only",
    groups: "limited",
    costPerMessage: true,
    alteraMensagemEnviada: false,
    podeReagir: false,
    transmissao: false,
    // Nenhuma destas linhas conversa com um aparelho pareado — não existe
    // agenda de celular a ler, e dizer `true` aqui seria um botão que só falha.
    phonebook: false,
    // A API oficial não tem endpoint de Status: o gesto só existe no app do
    // WhatsApp, atrás de um aparelho pareado. Declarar `true` aqui ofereceria
    // um botão que sempre falha (ver `publishStatus` em `./types`).
    statusUpdates: false,
    // Sem ponte de conversão: a venda desta linha sai pela conexão direta com a Meta
    // (conjunto de dados + token). Declarar `true` ofereceria uma chave que não envia nada.
    reportaConversao: false,
  },
};

/**
 * O que assumir quando o banco NÃO diz qual é o canal — só quando a linha de
 * `channel_sessions` não pôde ser lida (a coluna é `not null default 'waha'`,
 * então uma sessão que existe sempre responde).
 *
 * Espelha o default da coluna de propósito: é o que mantém o comportamento
 * idêntico ao dos literais que as Tasks 4b/5 deixaram no código. E é o canal
 * CONSERVADOR dos dois — banRisk armado, throttle e warm-up ligados; errar para
 * o lado do meta_cloud desarmaria o anti-ban num número que pode ser banido.
 */
export const DEFAULT_CHANNEL_PROVIDER: ChannelProvider = "waha";

/**
 * Constantes nomeadas dos providers. Existem para que nenhum arquivo fora deste
 * módulo precise escrever a string — é o que o `scripts/lint-channels.ts` cobra.
 */
export const CHANNEL_PROVIDER_WAHA: ChannelProvider = "waha";
export const CHANNEL_PROVIDER_META: ChannelProvider = "meta_cloud";
export const CHANNEL_PROVIDER_SOCIAL: ChannelProvider = "zernio_social";
export const CHANNEL_PROVIDER_ZERNIO: ChannelProvider = "zernio";
/**
 * Os canais cuja entrada responde ANTES de processar a mensagem nova — o porquê
 * está em `./inbound.ts`, que o reexporta. Mora aqui, num módulo sem dependência,
 * porque o piso ocioso do cron (`lib/cron/piso-ocioso.ts`) também o lê: ele
 * pergunta se há entrada aceita a reprocessar SEM escrever o nome do canal.
 */
export const CANAIS_QUE_RESPONDEM_ANTES: readonly string[] = [CHANNEL_PROVIDER_ZERNIO];
/** Parceiro que espelha a Cloud API — canal opcional da instalação, desligado por padrão. */
export const CHANNEL_PROVIDER_DATAFY: ChannelProvider = "datafy";
/** Chamada de voz WhatsApp (spec 18). Não transporta mensagem — ver abaixo. */
export const CHANNEL_PROVIDER_WACALLS: ChannelProvider = "wacalls";

/**
 * O provider em que o NÚMERO é a conexão: pareado por QR, a linha só vira
 * conexão quando um aparelho lê o código. A linha morta (`FAILED`/`STOPPED`)
 * sem número, ali, é QR que ninguém leu e não ocupa a vaga do plano (#3416);
 * nos outros provedores o número não identifica a conexão. É a mesma régua dos
 * gatilhos `fn_trava_canais_do_plano` e `fn_vaga_ao_reviver_canal`.
 */
export const PROVIDER_EM_QUE_O_NUMERO_E_A_CONEXAO: ChannelProvider = CHANNEL_PROVIDER_WAHA;

/**
 * Os providers por onde MENSAGEM entra e sai — a única lista que responde
 * "este canal serve para conversar?".
 *
 * Existe porque `channel_sessions` deixou de ser só a tabela dos transportes de
 * texto quando a voz entrou nela, e ~39 leituras daquela tabela não filtram
 * provider nenhum: elas dizem "canal" e querem dizer "canal de mensagem". Sem
 * esta lista, uma organização que pareia voz vê a linha de voz virar opção no
 * seletor "Número conectado", nascer amarrada ao primeiro agente publicado,
 * contar como canal conectado no retrato da instalação e ser escolhida por uma
 * automação para mandar texto — por um canal que não manda texto.
 *
 * `satisfies` e não anotação solta: um provider novo que não seja de mensagem
 * precisa ser DECIDIDO aqui, não esquecido.
 */
export const PROVIDERS_DE_MENSAGEM = [
  "waha",
  "meta_cloud",
  "zernio",
  "zernio_social",
  "datafy",
] as const satisfies readonly ProviderDeMensagem[];

/**
 * Os providers que alcançam um APARELHO: o endereço é o TELEFONE.
 *
 * É um recorte de `PROVIDERS_DE_MENSAGEM`, e a diferença é a das REDES SOCIAIS
 * (`zernio_social`): o Instagram e o Facebook só falam com quem já escreveu, o
 * contato de lá é outro endereço (handle) e não existe telefone a discar. Quem
 * tem um número na mão e escolhe a sessão social escolheu um canal que NÃO
 * alcança o destino — e o envio fora da janela de 24h é recusado (#3358,
 * medido: "O canal recusou o envio. Confira a conexão em Conexões.").
 *
 * `satisfies` sobre o recorte declara a relação entre as duas listas: um
 * provider que não seja de mensagem não pode entrar aqui, e um provider social
 * que apareça nesta lista reprova em compilação, não em produção.
 */
export const PROVIDERS_DE_TELEFONE = [
  "waha",
  "meta_cloud",
  "zernio",
  "datafy",
] as const satisfies readonly Exclude<(typeof PROVIDERS_DE_MENSAGEM)[number], "zernio_social">[];

/**
 * `true` quando a linha de `channel_sessions` é um canal de mensagem.
 *
 * Aceita `string | null | undefined` de propósito: quem chama está lendo uma
 * coluna do banco, que pode trazer um provider mais novo que este código (um
 * clone que atualizou o schema antes da imagem). Provider desconhecido responde
 * `false` — falhar fechado aqui significa "não use este canal para mandar
 * recado", que é o erro barato; o caro é mandar por um canal que não entrega.
 * A coluna é `not null default 'waha'`, então `null` só aparece quando a linha
 * não pôde ser lida, e aí também não há canal a usar.
 */
export function transportaMensagem(provider: string | null | undefined): boolean {
  return (PROVIDERS_DE_MENSAGEM as readonly string[]).includes(provider ?? "");
}

/**
 * Os providers que ESTE código conhece e que, sabidamente, não conversam.
 *
 * A diferença para `!transportaMensagem(p)` é a que separa "categoria" de
 * "falha", e ela decide o que o vigia de conexão faz com a linha:
 *
 *   - `wacalls` está aqui: ignorar em silêncio é o certo, e um aviso por sessão
 *     de voz a cada minuto seria ruído perpétuo.
 *   - um provider que o CHECK do banco já aceita e esta imagem ainda não conhece
 *     (o clone que aplicou o baseline antes de puxar a imagem nova) NÃO está
 *     aqui — ele tem de fazer barulho, porque uma conexão sem vigia e sem
 *     rastro é exatamente o buraco mudo que ninguém descobre.
 *
 * `transportaMensagem` responde `false` para os dois, e é o que se quer lá: na
 * hora de escolher por onde mandar recado, o desconhecido é tão inútil quanto a
 * voz. Aqui a pergunta é outra.
 */
export const PROVIDERS_SEM_MENSAGEM = ["wacalls"] as const;

/**
 * Erro de COMPILAÇÃO enquanto sobrar provider fora das duas listas. Provider
 * novo obriga a decidir se ele conversa — esquecer não é uma opção disponível.
 */
type ProviderNaoClassificado = Exclude<
  ChannelProvider,
  (typeof PROVIDERS_DE_MENSAGEM)[number] | (typeof PROVIDERS_SEM_MENSAGEM)[number]
>;
const _todoProviderFoiClassificado: ProviderNaoClassificado extends never ? true : never = true;
void _todoProviderFoiClassificado;

/** `true` só para provider conhecido cuja natureza não é mensagem. */
export function canalConhecidoSemMensagem(provider: string | null | undefined): boolean {
  return (PROVIDERS_SEM_MENSAGEM as readonly string[]).includes(provider ?? "");
}

/**
 * O canal de campanha por E-MAIL (#3441, fatia E2a).
 *
 * Não é `ProviderDeMensagem`: e-mail não é um `channel_sessions` — ninguém pareia
 * um aparelho, não há janela de 24 horas, não há modelo aprovado pela plataforma
 * e não há número. O endereço vem de `contacts.email` e o remetente da conta
 * Resend da organização. Mora AQUI, e não numa condição espalhada, porque a
 * pergunta "a campanha fala por quê?" continua sendo do canal.
 *
 * A régua da conta de envio NÃO mora aqui: esta função responde só o FORMATO do
 * conteúdo. Quem sabe se a organização tem conta Resend conectada é
 * `decidirConteudo` (`lib/campanhas/conteudo-do-canal.ts`), que consulta
 * `organization_resend_accounts` e recusa com motivo próprio.
 */
export const CANAL_DE_CAMPANHA_EMAIL = "email";

/** O que uma campanha pode mandar por um canal. `null` = por este canal não há campanha. */
export type ModoDeCampanha = "text" | "template" | "email";

/**
 * Como uma CAMPANHA fala por este canal: texto livre (`text`), modelo aprovado
 * (`template`), assunto + corpo (`email`) ou não fala (`null`).
 *
 * Campanha é contato que a empresa inicia — quase sempre fora das 24 horas da
 * última mensagem do cliente. Por isso a pergunta é `freeformOutsideWindow`, e
 * não "aceita texto": o número oficial aceita texto, mas só dentro da janela.
 * Canal que nem manda texto fora da janela nem tem modelo (as redes sociais)
 * não serve para campanha nenhuma.
 *
 * Tolerante como `transportaMensagem`: provider que esta imagem não conhece
 * responde `null` em vez de lançar — a tela de campanha lista conexões, e um
 * throw apagaria a lista inteira.
 */
export function modoDeCampanhaDoCanal(
  provider: string | null | undefined,
): ModoDeCampanha | null {
  // Antes de `transportaMensagem`: e-mail não transporta mensagem de conversa,
  // e é justamente por isso que a pergunta anterior responderia `false` para ele.
  if (provider === CANAL_DE_CAMPANHA_EMAIL) return "email";
  if (!transportaMensagem(provider)) return null;
  try {
    const caps = capabilitiesOf(provider as ProviderDeMensagem);
    if (caps.freeformOutsideWindow) return "text";
    if (caps.requiresTemplates) return "template";
    return null;
  } catch {
    return null;
  }
}

/**
 * O canal corre risco de banimento por volume (número por QR)? É o que liga o
 * aquecimento e o teto anti-ban do ritmo. Desconhecido responde `true`: no
 * ritmo, errar para o lado de mandar devagar é o erro barato.
 */
export function canalTemRiscoDeBanimento(provider: string | null | undefined): boolean {
  if (!transportaMensagem(provider)) return true;
  try {
    return capabilitiesOf(provider as ProviderDeMensagem).banRisk;
  } catch {
    return true;
  }
}

/**
 * O provedor deste número faz a transmissão (a lista inteira de uma vez, com o
 * relatório por destinatário)? Liga a terceira velocidade da campanha.
 * Desconhecido responde `false`: sem a certeza, a campanha fica no envio que o
 * próprio sistema controla.
 */
export function canalTemTransmissao(provider: string | null | undefined): boolean {
  if (!transportaMensagem(provider)) return false;
  try {
    return capabilitiesOf(provider as ProviderDeMensagem).transmissao;
  } catch {
    return false;
  }
}

export function capabilitiesOf(provider: ChannelProvider): ChannelCapabilities {
  const caps = CHANNEL_CAPABILITIES[provider as ProviderDeMensagem];
  // Fail-closed: provider fora da matriz não herda o default do WAHA. O tipo
  // barra em compilação; isto barra o que vem do banco em runtime.
  if (!caps) throw new Error(`unknown_channel_provider: ${provider}`);
  return caps;
}
