/**
 * Códigos de erro canônicos da API DeskcommCRM.
 *
 * Adicionar novo código:
 *  1. Adicionar à enum/constante abaixo
 *  2. Documentar em docs/specs/<spec>.md
 *  3. Sem renomear código existente — versionar em /api/v2/ se precisar quebrar
 */

export const ApiErrorCodes = {
  // 400 — body / params
  invalid_request: "invalid_request",
  validation_failed: "validation_failed", // Zod retornou erros de schema (422 também aceita)
  invalid_cursor: "invalid_cursor",
  // POST /api/v1/avisos (#1418): `publicar_em` no futuro é recusado
  // porque nada publica o aviso depois — gravar seria criar um recado que
  // nunca chega. Código próprio para a tela distinguir de "corpo inválido" e
  // oferecer o caminho: publicar agora ou enviar sem data.
  aviso_agendado_nao_suportado: "aviso_agendado_nao_suportado",

  // Configuração de agentes: validação/estado ou indisponibilidade do provedor.
  prospecting_agent_session_failed: "prospecting_agent_session_failed",
  prospecting_agent_prepare_failed: "prospecting_agent_prepare_failed",
  prospecting_agent_chat_failed: "prospecting_agent_chat_failed",
  prospecting_agent_setup_failed: "prospecting_agent_setup_failed",
  voice_assistant_unavailable: "voice_assistant_unavailable",

  // 401 — auth
  unauthorized: "unauthorized", // segredo interno inválido/ausente (rotas host↔app, ex. system/agent)
  unauthenticated: "unauthenticated",
  token_expired: "token_expired",
  token_revoked: "token_revoked",
  invalid_credentials: "invalid_credentials",
  mfa_required: "mfa_required",
  auth_in_query_forbidden: "auth_in_query_forbidden",

  // 403 — authz
  forbidden: "forbidden",
  forbidden_role: "forbidden_role",
  forbidden_tenant: "forbidden_tenant",
  // Conta da empresa suspensa (spec da cobrança §4): sessão, token `dsk_` e MCP.
  // Só LGPD e cobrança passam, por `requireRole({ permiteOrgSuspensa: true })`.
  org_suspended: "org_suspended",
  // Platform admin `support_readonly` tentando ESCREVER (`requirePlatformAdminEscrita`).
  forbidden_scope: "forbidden_scope",
  lgpd_anonymization_irreversible: "lgpd_anonymization_irreversible",

  // 404
  not_found: "not_found",

  // ⚠️ AGENDA — declarados AQUI, e não no `fail()`, porque `fail()` NÃO protege.
  //
  // A assinatura é `code: ApiErrorCode | (string & {})`, e o segundo ramo aceita
  // qualquer string: um `"slot_taken"` inventado no call site vira contrato de
  // wire sem passar por lista nenhuma, e o consumidor do outro lado nunca sabe
  // que ele existe. Quem confia que a união protege está lendo o arquivo errado.
  agenda_horario_indisponivel: "agenda_horario_indisponivel",
  agenda_fora_da_jornada: "agenda_fora_da_jornada",
  agenda_tipo_desativado: "agenda_tipo_desativado",
  agenda_sem_responsavel: "agenda_sem_responsavel",
  agenda_disponibilidade_invalida: "agenda_disponibilidade_invalida",
  agenda_ja_cancelado: "agenda_ja_cancelado",
  agenda_listagem_sem_recorte: "agenda_listagem_sem_recorte",
  // Código PRÓPRIO, e não o `unprocessable_entity` genérico: quem recebe isto
  // precisa saber que o `lead_id` mandado não é um negócio do funil (é quase
  // sempre um id de CONTATO — ver #509/#540) e que a correção é trocar o
  // parâmetro, não tratar como indisponibilidade.
  //
  // Quem recebe é a TELA da agenda: a rota só aceita sessão (`requireRole`), e
  // nenhum Bearer a alcança — o proxy devolve 401 antes. Se um dia ela passar a
  // aceitar token, este comentário ganha o integrador de volta.
  agenda_listagem_alvo_nao_e_lead: "agenda_listagem_alvo_nao_e_lead",
  /**
   * O dia pedido não EXISTE no calendário (`2026-02-31`, `2026-13-01`), e não
   * apenas está mal escrito (#3221).
   *
   * Código próprio, e não o `validation_error` de sempre, porque o consumidor
   * desta rota — a camada "Datas dos negócios" da Agenda — precisa distinguir
   * "o cliente chamou errado" de "o dia pedido não existe": a forma está certa
   * (`YYYY-MM-DD`), o filtro iria para o banco e voltaria vazio, e a grade
   * diria "ninguém embarca neste período" para uma data que ninguém digitou.
   * Com o código próprio, a tela mostra o motivo e esquece a escolha, como já
   * faz com `campo_nao_e_data`.
   */
  data_de_calendario_invalida: "data_de_calendario_invalida",

  // 409 — conflito
  idempotency_conflict: "idempotency_conflict",
  // Mesma chave, MESMO corpo, e a primeira execução ainda está em curso: o
  // recibo/encurso está gravado (reserva, migration 0321) mas o efeito não
  // terminou. Código próprio e não o `idempotency_conflict` acima porque a ação
  // de quem recebe é outra: aqui a chave está CERTA e o pedido é o mesmo —
  // retentar depois resolve, enquanto conflito manda trocar a chave.
  idempotency_in_progress: "idempotency_in_progress",
  state_conflict: "state_conflict",
  // POST /api/v1/prospecting (`start` e `resume`): a CONEXÃO escolhida já tem
  // uma campanha de prospecção em andamento (#3896). Uma campanha ativa por
  // NÚMERO, não por organização — a recusa é do número, e `details` traz
  // `campaign_id`/`campaign_name` de quem o ocupa para a tela oferecer
  // "abrir essa campanha". Irmão do `channel_archived`: mesma classe (o canal é
  // o recurso escasso), outro motivo.
  prospecting_channel_busy: "prospecting_channel_busy",
  // Escrita vinda de uma aba que ficou numa organização diferente da do cookie
  // `active_org` (#2335). O header `X-Org-Da-Aba` só serve para RECUSAR: a org
  // efetiva continua sendo a do cookie (`lib/auth/require-role.ts`).
  org_divergente: "org_divergente",
  // POST /admin/tenants/[id]/reactivate sobre suspensão por falta de pagamento:
  // a saída é "Dar prazo" ou "Tornar isenta", nunca o "Reativar" genérico.
  suspensao_de_cobranca: "suspensao_de_cobranca",
  // Rotas do dono da cobrança (spec da cobrança do revendedor §7e, §7g).
  // PATCH de plano que muda preço ou intervalo com assinante (em plano_id ou
  // plano_agendado_id): a saída é arquivar e criar outro.
  plano_com_assinantes: "plano_com_assinantes",
  // Troca de plano com a assinatura em dívida (em_atraso/cancelada) ou com o
  // teste grátis vencido sem pagamento.
  pagamento_pendente: "pagamento_pendente",
  // Limite do plano: o gatilho de assentos ou de canais recusou (PT402,
  // `details` = { recurso, limite }), ou a troca de plano não cabe no uso
  // (`details.excedente`). Spec cobrança §5.
  plan_limit_reached: "plan_limit_reached",
  // POST /admin/tenants/[id]/delete sobre suspensão por falta de pagamento:
  // excluir deixaria a assinatura cobrando no provedor. Irmão do de cima.
  exclusao_com_cobranca_pendente: "exclusao_com_cobranca_pendente",
  // POST /admin/tenants/[id]/delete sobre empresa suspensa pelo ADMINISTRADOR
  // que ainda tem assinatura viva no provedor: o gatilho da migration 0601
  // recusa a exclusão (PT409 organizacao_com_assinatura_viva). Não é falta de
  // pagamento — é cobrança que seguiria correndo sem ninguém para cancelar.
  exclusao_com_assinatura_viva: "exclusao_com_assinatura_viva",
  // POST /admin/tenants/[id]/suspend|reactivate quando o descarte da fila bate
  // na trava do aviso do Meet (`appointment_notice_busy`, 40001): outra escrita
  // do mesmo contato está em curso. Nada foi gravado; tentar de novo resolve.
  retry_later: "retry_later",
  invalid_state: "invalid_state", // resposta a um agent_case que saiu de awaiting_human (spec 15 §7)
  tenant_already_exists: "tenant_already_exists",
  // POST /api/v1/settings/api-tokens quando a organização já está no teto de
  // tokens ATIVOS (migration 0415, issue #1448). O corpo devolve a MESMA
  // mensagem que o gatilho levantou — com o limite e a instrução de revogar um
  // token para liberar espaço —, então quem lê sabe o que fazer sem perguntar.
  api_token_teto_atingido: "api_token_teto_atingido",
  // POST /api/v1/contacts com telefone já cadastrado na mesma organização
  // (índice uniq_contacts_org_phone). O corpo traz `details.contact_id` para a
  // tela oferecer o contato existente em vez de só mostrar que deu erro.
  contact_exists: "contact_exists",
  // POST/PATCH /api/v1/attendants/sectors com nome que já existe na organização
  // (índice attendant_sectors_org_name_unique, migration 0801).
  sector_name_taken: "sector_name_taken",
  // POST /api/v1/ai/agents e PATCH .../agents/[id] com nome que a organização já
  // usa (índice ai_agents_name_unique, composto `(organization_id, name)` —
  // baseline.sql:1963). Era 500 "Erro ao criar agent."; #3952. A régua que separa
  // ESTE `23505` dos outros índices mora em `lib/ai/agents/nome-repetido.ts`.
  // Irmão do `sector_name_taken`: mesma classe, outro cadastro.
  agent_name_taken: "agent_name_taken",
  duplicate_external_id: "duplicate_external_id",
  event_gone: "event_gone", // resend de run cujo event_log original foi apagado (on delete set null)
  no_actions_to_resend: "no_actions_to_resend", // resend de regra que não tem mais nenhuma ação de webhook — reenviar nada não é sucesso
  next_action_absent: "next_action_absent", // decisão sobre proposta que não existe (mais) [wave 4]
  next_action_changed: "next_action_changed", // o agente reescreveu a proposta entre o render e o clique
  channel_archived: "channel_archived", // ação sobre canal que o usuário excluiu (a linha só sobrevive como âncora das FKs)
  // POST /api/v1/ai/followups/enrollments numa instalação SEM número conectado
  // (#3923): `fn_service_begin` levanta `service_channel_not_found` e o banco
  // está certo — sem canal não há conversa para abrir. Era 500 sem corpo; agora
  // é 409 com esta código estável, e a tela troca a frase genérica por "conecte
  // um número". 409 e não 422: o corpo do pedido está correto, quem não está
  // pronto é o estado da instalação.
  service_channel_not_found: "service_channel_not_found",
  knowledge_source_type_in_use: "knowledge_source_type_in_use", // fonte ATIVA do mesmo tipo no agente — era o índice ai_knowledge_sources_unique_per_agent, que a 0181 derrubou; nenhuma rota emite mais este código
  voice_already_paired: "voice_already_paired", // POST /voice/sessions/pair com aparelho já vinculado — a saída é DELETE /voice/sessions, nunca re-parear por cima (ver a rota)

  // 422 — semântica
  unprocessable_entity: "unprocessable_entity",
  // 422, e é PRODUTO, não erro: o aviso do art. 12.º vale só para organizações
  // FORA do Brasil (decisão de 2026-10-16). A organização brasileira não recebe
  // "recusa parcial" — recebe a porta fechada, e a recebe ANTES de o e-mail
  // sair, de o `due_at` ser tocado e de a auditoria registrar o aviso.
  lgpd_art12_so_fora_do_brasil: "lgpd_art12_so_fora_do_brasil",
  plano_invalido: "plano_invalido", // plano inexistente, arquivado, de outro intervalo, ou cobrança desligada
  // #2846: cobrança ligada, planos oferecidos e nenhum plano padrão — a empresa
  // não nasce sem plano (doc 146). Quem administra define um padrão.
  plano_obrigatorio: "plano_obrigatorio",
  // Cobrança do revendedor, PR 3a (spec §7a–§7g, §13). O provedor fora do ar é
  // 503 e o provedor que recusou é 502: a tela diz "tente de novo" num caso e
  // "confira os dados" no outro.
  provedor_indisponivel: "provedor_indisponivel",
  provedor_recusou: "provedor_recusou",
  // A instalação ainda não conectou provedor (a empresa lê "o administrador ainda não conectou a cobrança").
  provedor_nao_conectado: "provedor_nao_conectado",
  // Trocar de provedor, ou pôr chave de teste no lugar da de produção, com empresas pagando de verdade.
  provedor_com_assinaturas: "provedor_com_assinaturas",
  // Trocar a chave de teste pela de produção devolve ao teste grátis quem assinou em teste (D-7): pede confirmação.
  publicacao_requer_confirmacao: "publicacao_requer_confirmacao",
  // A chave não passou no teste do provedor (`details.motivo`).
  chave_recusada: "chave_recusada",
  // Sem https público o provedor não entrega os avisos de pagamento.
  url_publica_invalida: "url_publica_invalida",
  // Outro clique está gerando o link de pagamento desta empresa (reserva de 2 min).
  checkout_em_preparo: "checkout_em_preparo",
  // Troca de plano recusada enquanto houver link de pagamento em aberto.
  checkout_em_aberto: "checkout_em_aberto",
  // Já existe assinatura esperando o 1º pagamento (`details.link_de_pagamento`).
  pagamento_em_andamento: "pagamento_em_andamento",
  // Tornar isenta exige cancelar antes no provedor.
  assinatura_viva_no_provedor: "assinatura_viva_no_provedor",
  // O provedor não oferece portal para esta assinatura.
  sem_portal: "sem_portal",
  // A chave nova é de OUTRA conta do provedor: os clientes que já pagam não existem nela.
  chave_de_outra_conta: "chave_de_outra_conta",
  // Em atraso sem fatura pagável agora (ex.: assinatura pausada): o caminho é o portal.
  sem_link_de_pagamento: "sem_link_de_pagamento",
  // Cobrança do revendedor, PR 3b (Asaas). O CPF/CNPJ de quem paga vai direto ao
  // provedor e não é guardado: dígitos que não conferem, ausente quando o Asaas
  // exige, ou com os dígitos certos e recusado pelo provedor.
  documento_invalido: "documento_invalido",
  documento_obrigatorio: "documento_obrigatorio",
  documento_recusado: "documento_recusado",
  // O cliente no provedor ainda não existe e nenhum admin da empresa tem e-mail (link gerado pelo dono, D-15).
  empresa_sem_email: "empresa_sem_email",
  // O Asaas recusa trocar o plano com a cobrança do período em uso ainda pendente (spec §6.2, §7e).
  pagamento_do_periodo_pendente: "pagamento_do_periodo_pendente",
  channel_without_session: "channel_without_session", // operação de sessão (reiniciar, parear) pedida a canal que não tem sessão no transporte — o oficial
  janela_fechada: "janela_fechada", // POST /messages por token/agente com texto livre fora das 24h em canal com restrição (131047) — a saída é modelo aprovado (#1614)
  // POST /api/v1/messages/{id}/retry (lib/messaging/reenvio.ts): a regra não permite reenviar (403),
  // a mensagem pode ter chegado e falta a confirmação (409), ou outro toque já travou a linha (409).
  reenvio_indisponivel: "reenvio_indisponivel",
  confirmacao_necessaria: "confirmacao_necessaria",
  reenvio_em_curso: "reenvio_em_curso",
  invalid_state_transition: "invalid_state_transition",
  invalid_owner: "invalid_owner", // novo dono não é membro ativo agent+ da org (bulk assign, G3-04)
  trigger_kind_not_implemented: "trigger_kind_not_implemented", // publish de followup-flow com kind sem motor de enrollment (stage_change/conversation_end)
  // PATCH /api/v1/ai/jev ao ligar. Dois códigos porque são duas ações de quem lê:
  // colar e testar a chave, ou marcar o aceite de mandar a mensagem para fora.
  jev_exige_chave_validada: "jev_exige_chave_validada",
  jev_exige_aceite: "jev_exige_aceite",
  // PATCH /api/v1/ai/jev pedindo `decidindo` numa tarefa que, nesta versão, só
  // observa (`soObserva` em lib/ai/decisao/tarefas.ts) — a do follow-up.
  jev_tarefa_so_observa: "jev_tarefa_so_observa",

  // 415 — tipo de mídia
  unsupported_media_type: "unsupported_media_type",
  // SVG recusado como logo. Código PRÓPRIO e não o genérico acima porque a pessoa
  // que sobe um SVG fez a coisa mais natural do mundo (é o formato em que um
  // designer entrega logo) e precisa ler "mande PNG ou JPG", não "tipo de mídia
  // não suportado". A razão da recusa está em lib/branding/logo-arquivo.ts.
  logo_svg_recusado: "logo_svg_recusado",

  // 413
  payload_too_large: "payload_too_large",

  // 429
  rate_limited: "rate_limited",

  // ─── ANÚNCIOS, eixo de LEITURA (0214) ───
  //
  // Declarados aqui pelo mesmo motivo que os da Agenda: `fail()` aceita
  // `(string & {})`, então um código inventado no call site vira contrato de
  // wire sem passar por lista nenhuma. E estes precisam ser distinguíveis pelo
  // cliente — a tela mostra uma frase DIFERENTE para cada um, porque cada um
  // pede uma ação diferente de quem lê (colar token novo, refazer o token com
  // `ads_read`, esperar a cota, ou avisar quem mantém o sistema).
  ads_sem_conexao: "ads_sem_conexao",
  ads_token_invalido: "ads_token_invalido",
  ads_permissao_insuficiente: "ads_permissao_insuficiente",
  ads_limite_de_chamadas: "ads_limite_de_chamadas",
  ads_campo_invalido: "ads_campo_invalido",
  ads_cifra_indisponivel: "ads_cifra_indisponivel",

  // ─── BANCO DE DADOS EXTERNO DO AGENTE (migration 0372) ───
  //
  // Declarados aqui pelo mesmo motivo dos da Agenda/Anúncios: `fail()` aceita
  // qualquer string, então o código só é contrato se estiver nesta lista. A tela
  // distingue "destino bloqueado pela política de rede" de "senha cifrada não
  // pôde ser lida" — cada um pede uma ação diferente de quem lê.
  external_db_destino_bloqueado: "external_db_destino_bloqueado",
  external_db_label_em_uso: "external_db_label_em_uso",
  external_db_desativada: "external_db_desativada",
  external_db_sem_chave: "external_db_sem_chave",

  // ─── CHAMADA DE VOZ (spec 18, migration 0234) ───
  //
  // Três recusas que pedem TRÊS ações diferentes de quem lê, e por isso não
  // podem colapsar num genérico. A tela e a IA precisam distinguir:
  //
  //   • a organização não ligou a feature      → um admin liga em Segurança;
  //   • o admin tentou ligar sem aceitar o risco → ler o aviso e confirmar;
  //   • a instalação não oferece o serviço      → falar com quem administra a
  //     VPS; nenhum clique na tela resolve.
  //
  // O terceiro é 503 (dependência de instalação, como `waha_not_configured`);
  // os dois primeiros são 422 (recurso desligado por configuração da própria
  // organização, como `agenda_tipo_desativado`).
  voice_desligada_na_organizacao: "voice_desligada_na_organizacao",
  voice_risco_nao_aceito: "voice_risco_nao_aceito",
  voice_indisponivel_na_instalacao: "voice_indisponivel_na_instalacao",
  // 503: a leitura do estado não voltou. Separado dos dois acima de propósito —
  // "não sei" não pode se disfarçar de "está desligada", que mandaria a pessoa
  // procurar um interruptor quando o problema é o banco.
  voice_estado_indeterminado: "voice_estado_indeterminado",

  // ─── NEGÓCIOS E FUNIL (issues #917 e #922) ───
  //
  // Onze códigos de wire que a família de `/api/v1/leads` já emitia — alguns há
  // meses — sem passar por esta lista. Pelo mesmo motivo dos blocos acima:
  // `fail()` aceita `(string & {})`, então o código nasce no call site e vira
  // contrato sem ninguém decidir que virou. `grep` de cada um contra este
  // arquivo devolvia zero, inclusive para `lead_stage_changed_concurrent` e
  // `pipeline_immutable_use_clone`, que são contrato de wire em produção.
  //
  // Registrados JUNTOS, e não só os dois que o lote acrescentou, porque corrigir
  // por instância deixa as irmãs de fora — e elas não se parecem por fora.
  //
  // 409: a trava otimista do arrasto (`expected_updated_at` não bate).
  lead_stage_changed_concurrent: "lead_stage_changed_concurrent",
  // 422, o motivo da perda: exigido quando a escrita fecharia o negócio como
  // perdido, e recusado quando não está no vocabulário do funil. Um pede
  // informar, o outro pede escolher da lista — colapsá-los mandaria quem já
  // informou um motivo digitar outra vez.
  lost_reason_required: "lost_reason_required",
  lost_reason_invalid: "lost_reason_invalid",
  // 422, a fronteira do funil (P-01): a etapa é de outro funil, e o caminho para
  // levar o negócio até lá é o clone, não o arrasto.
  pipeline_immutable_use_clone: "pipeline_immutable_use_clone",
  stage_pipeline_mismatch: "stage_pipeline_mismatch",
  // 422, as recusas do clone — cada uma pede uma ação diferente de quem lê:
  // escolher outro funil, reabrir o negócio, escolher outra etapa, configurar
  // uma etapa de entrada, ou configurar uma etapa de perda no funil de origem.
  pipeline_unchanged: "pipeline_unchanged",
  lead_not_open: "lead_not_open",
  stage_destino_terminal: "stage_destino_terminal",
  pipeline_without_initial_stage: "pipeline_without_initial_stage",
  pipeline_no_lost_stage: "pipeline_no_lost_stage",
  // 404: o funil de destino não existe (ou não é desta organização).
  pipeline_not_found: "pipeline_not_found",
  // 409 (issue #1538): o funil é `novo_negocio` e a escrita reabriria um
  // negócio encerrado. Os QUATRO caminhos (arrasto, lote, IA/automação, MCP)
  // devolvem este mesmo código, e a tela o reconhece para oferecer a retomada.
  reabertura_cria_novo: "reabertura_cria_novo",
  // 422: chamaram `/retomar` num negócio que continua ABERTO — não há o que
  // retomar, e criar aí duplicaria o card que já está no quadro.
  reabertura_lead_aberto: "reabertura_lead_aberto",
  // ─── CAMPOS OBRIGATÓRIOS E MOTIVO DE GANHO (issue #1536) ───
  //
  // As duas recusas do núcleo novo, cada uma com a sua demanda: a primeira pede
  // PREENCHER (o `details.faltando` nomeia chave e rótulo de cada campo — e
  // quando o que falta é o motivo de ganho a chave é `won_reason`, um caso do
  // mesmo contrato, não um código à parte), a segunda pede ESCOLHER da lista
  // cadastrada (`settings.won_reasons`). Colapsá-las mandaria quem já informou
  // escolher sem lista, e quem não informou digitar sem caminho.
  required_fields_missing: "required_fields_missing",
  won_reason_invalid: "won_reason_invalid",

  // ─── AVISO DE CASO NO WHATSAPP (migration 0292, onda 8) ───
  //
  // Declarados aqui pelo mesmo motivo dos blocos acima: `fail()` aceita
  // `(string & {})`, e um código que nasce no call site vira contrato de wire
  // sem ninguém decidir que virou. Estes quatro precisam ser distinguíveis
  // porque a TELA faz uma coisa diferente com cada um:
  //
  //   • `aviso_numero_de_cliente` NÃO é uma recusa final — é uma PERGUNTA. O
  //     número digitado já é um cliente desta organização, e confirmar
  //     significa que as mensagens dessa pessoa param de chegar ao CRM. A tela
  //     mostra o aviso e reenvia com `confirma_contato: true`;
  //   • `aviso_numero_da_propria_org` é final: é o laço robô↔robô, e não há
  //     confirmação que o torne aceitável;
  //   • `aviso_canal_invalido` manda escolher outra conexão;
  //   • `aviso_nao_configurado` é do botão de teste, e manda salvar antes.
  //
  // Os quatro são 422 — recusa semântica sobre um corpo bem formado.
  aviso_numero_de_cliente: "aviso_numero_de_cliente",
  aviso_numero_da_propria_org: "aviso_numero_da_propria_org",
  aviso_canal_invalido: "aviso_canal_invalido",
  aviso_nao_configurado: "aviso_nao_configurado",

  // ─── Módulo CAMPANHAS (migration 0264, Spec 12 §17) ───
  campanha_nao_encontrada: "campanha_nao_encontrada", // 404
  // 409: a ação não cabe no estado atual. A mensagem diz os DOIS estados, porque
  // "estado inválido" sem dizer qual manda o operador adivinhar.
  campanha_estado_invalido: "campanha_estado_invalido",
  campanha_nao_editavel: "campanha_nao_editavel", // 409: só rascunho aceita edição
  campanha_preparando: "campanha_preparando", // 409: preparação em andamento
  campanha_sem_audiencia: "campanha_sem_audiencia", // 422: o recorte não achou ninguém
  // 422: achou gente, e nenhuma pode receber (todos bloqueados/sem telefone). É
  // diferente de audiência vazia: o filtro está certo e a lista é que não presta.
  campanha_sem_elegiveis: "campanha_sem_elegiveis",
  campanha_canal_indisponivel: "campanha_canal_indisponivel", // 409: conexão fora do ar ou de outra org
  campanha_agenda_invalida: "campanha_agenda_invalida", // 422: data no passado
  campanha_conteudo_invalido: "campanha_conteudo_invalido", // 422: texto vazio ou variável que não existe
  campanha_base_legal_invalida: "campanha_base_legal_invalida", // 422: interesse legítimo sem referência da LIA
  // Migration 0710 (#1330, fatia 4): campanha pelo número oficial com modelo aprovado.
  campanha_modo_do_canal: "campanha_modo_do_canal", // 422: texto livre em número oficial, ou modelo em número por QR
  campanha_modelo_um_numero: "campanha_modelo_um_numero", // 422: modelo aprovado sai por um número só
  campanha_modelo_inexistente: "campanha_modelo_inexistente", // 422: o modelo não existe neste número
  campanha_modelo_nao_aprovado: "campanha_modelo_nao_aprovado", // 422: modelo pausado, recusado ou em análise
  campanha_modelo_variaveis: "campanha_modelo_variaveis", // 422: variável sem origem, ou origem que não serve
  // Migration 0711: envio rápido (rajada) só pelo número oficial, com modelo aprovado.
  campanha_velocidade_do_canal: "campanha_velocidade_do_canal", // 422: envio rápido ou broadcast pedido num número que não o faz
  campanha_transmissao_variavel: "campanha_transmissao_variavel", // 422: broadcast com variável que muda por pessoa (migration 0712)
  campanha_transmissao_entregue: "campanha_transmissao_entregue", // 409: a velocidade não muda depois que a lista foi entregue ao provedor
  // Migration 9334 (#3441, fatia E2a): campanha por e-mail, sem número.
  campanha_email_sem_conta: "campanha_email_sem_conta", // 409: a organização não conectou a conta Resend
  campanha_email_conta_indisponivel: "campanha_email_conta_indisponivel", // 503: a conta existe e não pôde ser lida — não é ausência
  campanha_email_assunto_vazio: "campanha_email_assunto_vazio", // 422: e-mail sem assunto não sai
  campanha_email_assunto_longo: "campanha_email_assunto_longo", // 422: assunto acima do limite do envio
  campanha_email_corpo_vazio: "campanha_email_corpo_vazio", // 422: e-mail sem corpo não sai
  campanha_email_corpo_longo: "campanha_email_corpo_longo", // 422: corpo acima do limite do envio
  campanha_email_desligada: "campanha_email_desligada", // 404: o módulo `campanha_por_email` está desligado na instalação (doc 305)

  // 500 / upstream
  internal_error: "internal_error",
  upstream_unavailable: "upstream_unavailable",
  unavailable: "unavailable", // 503: dependência de config ausente (ex.: pool do engine sem SUPABASE_DB_URL)

  // ─── CEP DO CONTATO (issue #3494, fatia 2) ───
  //
  // 404 próprio, e não `not_found` genérico: a tela do cadastro precisa dizer
  // "este CEP não existe" ao lado do campo, sem confundir com "contato não
  // encontrado" — mesmo motivo de `campanha_nao_encontrada`. A falha do serviço
  // de terceiro continua `upstream_unavailable` (502), que é o código que o
  // cliente já trata como "tente de novo".
  cep_nao_encontrado: "cep_nao_encontrado",

  base_publica_nao_configurada: "base_publica_nao_configurada", // 503: NEXT_PUBLIC_APP_URL ausente, placeholder ou da própria máquina — o webhook único da instalação não é registrado
  waha_error: "waha_error",
  channel_unavailable: "channel_unavailable", // 502: o transporte do canal não respondeu (ex.: listar grupos com a sessão fora de WORKING)
  // 409: a agenda do aparelho não é entregue com o store da sessão desligado
  // (a opção "Guardar o histórico anterior à vinculação"). Código próprio, e
  // não `channel_unavailable`, porque a AÇÃO é outra: ligar uma opção na tela
  // de Conexões — não esperar a conexão voltar (#4102).
  channel_acervo_desligado: "channel_acervo_desligado",
  channel_timeout: "channel_timeout", // 504: o canal não respondeu dentro do teto (relógio, não recusa)
  channel_session_rejected: "channel_session_rejected", // 502: o WhatsApp recusou a sessão do número (401/403)
  wacalls_error: "wacalls_error", // 502: o serviço de chamada de voz recusou ou não respondeu
  wacalls_not_connected: "wacalls_not_connected", // 503 + Retry-After: sessão pareada cujo socket com o WhatsApp caiu por um instante (ver `wacallsSemConexao`)
  ai_provider_error: "ai_provider_error",
  // 502: o teste do agente (#4020) não chegou a rodar porque o BANCO não
  // atendeu — `Query read timeout`, `statement timeout`, socket derrubado ou o
  // pool do engine sem `SUPABASE_DB_URL`. Código próprio, e não `preview_failed`,
  // porque a tela e quem depura precisam distinguir "a configuração do agente
  // está errada" (422, `preview_failed`) de "a dependência não respondeu"
  // (502, este — não 503, que o cliente repete) — a primeira manda conferir
  // modelo/credencial, a segunda manda esperar e tentar de novo.
  preview_banco_indisponivel: "preview_banco_indisponivel",
  nuvemshop_error: "nuvemshop_error",
} as const;

export type ApiErrorCode = (typeof ApiErrorCodes)[keyof typeof ApiErrorCodes];
