/**
 * A conexão caiu — e alguém precisa SABER.
 *
 * ─── O defeito, medido ──────────────────────────────────────────────────────
 *
 * A sessão do canal saiu de `WORKING` às 13:19. O estado foi gravado na coluna,
 * o operador não recebeu nada, e a descoberta aconteceu horas depois, quando ele
 * foi olhar por conta própria — depois de estranhar que ninguém escrevia. Não
 * havia bug em lugar nenhum: o webhook chegou, a coluna atualizou. O que faltava
 * era a única parte que importa para quem atende — o aviso.
 *
 * ─── Por que este arquivo existe, se o mecanismo já estava desenhado ────────
 *
 * Estava: `channel_session_health.escalated_status` nasceu com o comentário
 * "Status já escalado (agent_inbox_items kind='qr_rescan') no EPISÓDIO corrente
 * — dedup do 'exatamente 1×'. Volta a null quando a sessão volta a WORKING", e
 * `SESSION_ESCALATION_STATUSES` foi declarada em `edge/crm/session-watchdog.ts`.
 * A coluna, a constante e o kind existiam; o código que escala nunca foi
 * escrito. `git grep` achava os três e nenhum consumidor — promessa de esqueleto,
 * sem músculo. Este arquivo é o músculo, e usa a peça desenhada em vez de
 * inventar uma paralela.
 *
 * ─── Dois gatilhos, porque falham de formas diferentes ─────────────────────
 *
 * O webhook de status avisa em segundos — e emudece justamente quando o
 * transporte morre, que é o pior caso. O vigia PERGUNTA, e por isso enxerga o
 * silêncio. Um sem o outro deixa um buraco: só webhook não vê o transporte
 * morto; só vigia demora até a próxima rodada. Os dois chamam esta mesma
 * função, então a regra de quando avisar existe uma vez só.
 */
import type { SupabaseClient } from "@supabase/supabase-js";

import { traduzir } from "@/lib/i18n/dicionario";
import type { Idioma } from "@/lib/i18n/idiomas";

import { PROVIDERS_DE_MENSAGEM } from "./capabilities";
import {
  DETALHE_NAO_RECEBE,
  PREFIXO_ENVIO_RESTRITO,
  defeitoDoNumero,
  type DefeitoDoNumero,
} from "./defeito-do-numero";
import { ehNomeDeSessaoE2E } from "./sessoes-e2e";

/** Único estado em que mensagem entra e sai. Contrato do CRM (uppercase). */
export const STATUS_SAUDAVEL = "WORKING";

/**
 * Estados que merecem aviso.
 *
 * `SCAN_QR_CODE` e `FAILED` vêm de `SESSION_ESCALATION_STATUSES`, declarada
 * quando o mecanismo foi desenhado. `STOPPED` foi ACRESCENTADO aqui: para quem
 * atende, uma sessão parada é indistinguível de uma quebrada — em ambos os casos
 * ninguém recebe mensagem, que é a única coisa que o operador sente. Parar de
 * propósito e não ser avisado é um incômodo pequeno; não ser avisado de uma
 * parada que ninguém pediu é o defeito que motivou este arquivo.
 *
 * `STARTING` NÃO entra: é o estado normal de todo boot, e avisar nele faria o
 * sistema gritar a cada reinício — o caminho mais curto para o operador aprender
 * a ignorar o aviso.
 */
export const STATUS_QUE_AVISAM = ["SCAN_QR_CODE", "FAILED", "STOPPED"] as const;

/** Marca os avisos desta origem, para resolver o do canal CERTO depois. */
export const REF_KIND_SESSAO = "channel_session";

/**
 * O transporte respondeu, e a resposta foi "sua credencial não vale".
 *
 * Vive aqui, e não no adapter, porque quem lê este código é a regra de aviso
 * logo abaixo: é um contrato entre os dois, e um literal repetido nas duas
 * pontas divergiria no primeiro renomeio — com o gate verde, porque nenhum
 * `includes` reclama de string que ninguém mais escreve.
 */
export const DETALHE_CREDENCIAL_RECUSADA = "credencial_recusada_pelo_transporte";

/**
 * O NÚMERO ESTÁ DE PÉ, MAS NÃO ESTÁ INTEIRO.
 *
 * ─── O defeito que isto fecha (27/10/2026) ──────────────────────────────────
 *
 * A sonda do canal oficial devolvia `WORKING` para dois números que não
 * estavam inteiros, porque só olhava se o elo com a plataforma respondia:
 *
 *   - o número que ENVIA mas não RECEBE: a plataforma informa que a assinatura
 *     do webhook de mensagens do número caiu, e as mensagens dos clientes não
 *     chegam ao CRM. Ninguém percebe pelo envio, que segue funcionando;
 *   - o número com envio RESTRITO pela plataforma (pagamento, conta bloqueada,
 *     política): as mensagens saem do CRM e são recusadas lá.
 *
 * ─── Por que o status continua WORKING ─────────────────────────────────────
 *
 * `WORKING` é o que deixa o envio sair (`messages/_handler`), e nos dois casos
 * barrar o envio é PIOR que o defeito: o número que não recebe ainda atende
 * quem já está na conversa; e a restrição, segundo o provedor, só se desfaz
 * quando uma mensagem volta a ser entregue — barrar o envio a deixaria de pé
 * para sempre. Então o status fica, e o defeito vai por dois caminhos que já
 * existem: o MOTIVO em `channel_sessions.status_reason` (a tela de Conexões o
 * mostra) e o aviso de conexão da Central (abaixo), que fecha sozinho quando a
 * sonda volta limpa.
 *
 * Os dois detalhes são contrato entre o adapter, que os escreve, e esta regra,
 * que os lê — como `DETALHE_CREDENCIAL_RECUSADA`. Moram em
 * `./defeito-do-numero.ts`, puro, porque a tela de Conexões (cliente) também os lê.
 */
export { DETALHE_NAO_RECEBE, PREFIXO_ENVIO_RESTRITO, defeitoDoNumero, type DefeitoDoNumero };

/**
 * O que gravar em `channel_sessions.status_reason` depois desta observação.
 *
 * `undefined` = não mexer. A regra só cuida do motivo que ELA escreve: com o
 * número de pé, grava o defeito, ou limpa o defeito que ela mesma tinha
 * gravado. Qualquer outro motivo (o reparo de conexão, por exemplo) é de outro
 * dono. E observação que não deu para fazer, ou de número caído, não mexe — a
 * queda tem o seu próprio caminho.
 */
export function motivoParaGravar(
  saude: SaudeObservada,
  motivoAtual: string | null | undefined,
): string | null | undefined {
  if (!saude.reachable || (saude.status ?? "").toUpperCase() !== STATUS_SAUDAVEL) return undefined;
  const defeito = defeitoDoNumero(saude.detail) ? saude.detail : null;
  if (defeito) return defeito === motivoAtual ? undefined : defeito;
  if (defeitoSegueSemMedida(saude, motivoAtual)) return undefined;
  return defeitoDoNumero(motivoAtual) ? null : undefined;
}

function avisoDoDefeito(defeito: DefeitoDoNumero, apelido: string, idioma: Idioma): AvisoDeConexao {
  const t = (texto: string) => traduzir(texto, idioma);
  if (defeito.tipo === "nao_recebe") {
    return {
      kind: "channel_number_alert",
      severity: "critical",
      title: t('O número "{apelido}" não recebe mensagens').replace("{apelido}", apelido),
      body: t(
        "A Meta informa que as mensagens dos clientes para este número não chegam ao CRM, embora ele ainda consiga enviar. Reconecte o número em Conexões. Se continuar assim, o número pode estar ligado a outra conta do WhatsApp Business (por exemplo, depois de ser vinculado a uma página do Facebook).",
      ),
      episodio: "NAO_RECEBE",
    };
  }
  // O porquê, pelo código da plataforma. Um literal por ramo, e não um mapa:
  // a cerca de i18n só cobra o espanhol do que consegue ler escrito.
  const motivo =
    defeito.codigo === "131042"
      ? t(
          "A Meta recusou mensagens por um problema de pagamento ou de elegibilidade da conta do WhatsApp Business (código 131042). Confira a forma de pagamento da conta no Gerenciador do WhatsApp, na Meta.",
        )
      : defeito.codigo === "131031"
        ? t(
            "A Meta bloqueou a conta do WhatsApp Business deste número (código 131031). Veja o motivo e peça a revisão no Gerenciador do WhatsApp, na Meta.",
          )
        : defeito.codigo === "368"
          ? t(
              "A Meta bloqueou temporariamente o envio deste número por violação de política (código 368). Veja o motivo no Gerenciador do WhatsApp, na Meta.",
            )
          : t("A Meta está recusando mensagens deste número (código {codigo}).").replace("{codigo}", defeito.codigo);
  return {
    kind: "channel_number_alert",
    severity: "critical",
    title: t('A Meta restringiu o envio do número "{apelido}"').replace("{apelido}", apelido),
    body: `${motivo} ${t("Este aviso fecha sozinho quando uma mensagem voltar a ser entregue.")}`,
    episodio: `ENVIO_RESTRITO_${defeito.codigo}`,
  };
}

/**
 * Marca do episódio aberto por um EMPURRÃO do provedor.
 *
 * A varredura não fecha episódio com esta marca: ela mede credencial e conta,
 * não o estado do número, e um número suspenso continua aparecendo na lista de
 * contas. Sem a marca, o crítico de suspensão sumia da Central em ≤5 minutos.
 */
export const PREFIXO_EMPURRAO = "PUSH:";

/**
 * Quantas observações SEGUIDAS de queda antes de acreditar nela.
 *
 * ─── O defeito que isto existe para impedir ─────────────────────────────────
 *
 * Medido numa instalação real: a conexão oficial saiu de WORKING e voltou cinco
 * minutos depois. Nesse intervalo o operador mandou duas mensagens, as duas
 * foram para `queued` com `channel_session_not_working`, e nenhuma saiu nunca.
 * Não houve queda de verdade: a pergunta ao provedor falhou UMA vez, e a
 * varredura traduziu isso em queda na primeira tentativa.
 *
 * Uma pergunta que falha não é uma conexão caída. Com a varredura de 5 em 5
 * minutos, exigir DUAS confirmações custa no pior caso ~5 minutos a mais para
 * anunciar uma queda real — e evita anunciar a que não existe, que é a que
 * derruba mensagem de cliente.
 *
 * ─── Por que 2, e não 3 ─────────────────────────────────────────────────────
 *
 * Porque o outro lado do erro também é caro: cada rodada a mais é tempo em que
 * uma queda REAL segue sem aviso. Dois é o menor número que descarta o pisco
 * isolado, que foi o padrão observado.
 *
 * A coluna `consecutive_health_fails` existe desde o desenho original e a spec
 * descreve incrementá-la — nenhum código o fazia. Este é o consumidor que
 * faltava; ela é zerada ao reconectar, e volta a zero em qualquer observação boa.
 */
export const CONFIRMACOES_PARA_QUEDA = 2;

export interface JulgamentoDaQueda {
  /** Acreditar na queda AGORA (só então o status vai ao banco e o aviso abre). */
  confirmada: boolean;
  /** O que gravar em `consecutive_health_fails`. */
  contador: number;
}

/**
 * Uma observação ruim confirma a queda, ou ainda é pisco?
 *
 * Observação BOA zera na hora — recuperação nunca espera confirmação: o custo de
 * acreditar cedo numa volta é nenhum, e o de demorar é seguir barrando envio de
 * um canal que já voltou.
 *
 * "Não deu para perguntar" (`reachable: false`) conta como ruim de propósito:
 * do lado de quem atende, um provedor que não responde e um canal caído são a
 * mesma coisa — ninguém recebe mensagem. O que muda é o texto do aviso, não se
 * ele existe.
 */
export function julgarQueda(saude: SaudeObservada, contadorAtual: number): JulgamentoDaQueda {
  const status = (saude.status ?? "").toUpperCase();
  const ruim = !saude.reachable || (STATUS_QUE_AVISAM as readonly string[]).includes(status);

  if (!ruim) return { confirmada: false, contador: 0 };

  // Para no teto: a sessão caída não paga um UPDATE por rodada para um número
  // que ninguém mais lê.
  const contador = Math.min(contadorAtual + 1, CONFIRMACOES_PARA_QUEDA);
  return { confirmada: contador >= CONFIRMACOES_PARA_QUEDA, contador };
}

export interface AvisoDeConexao {
  kind: "qr_rescan" | "channel_number_alert";
  severity: "warn" | "critical";
  title: string;
  body: string | null;
  /** O que grava em `escalated_status` — o episódio já escalado. */
  episodio: string;
}

export interface SaudeObservada {
  reachable: boolean;
  status: string | null;
  detail: string | null;
  defeitosSemMedida?: DefeitoDoNumero["tipo"][];
}

/**
 * O defeito gravado segue valendo porque a sonda não mediu o que o fecharia?
 * Sem isto, "não informado" contava como cura: o cartão voltava a «Conectado» e
 * a Central fechava o crítico com o defeito de pé.
 */
function defeitoSegueSemMedida(saude: SaudeObservada, motivoAtual: string | null | undefined): boolean {
  const aberto = defeitoDoNumero(motivoAtual);
  return aberto !== null && (saude.defeitosSemMedida ?? []).includes(aberto.tipo);
}

/**
 * O que foi observado merece aviso? `null` quando está tudo bem.
 *
 * `apelido` é como o operador chama esta conexão na tela. Entra no título porque
 * com dois números ligados "WhatsApp desconectado" não diz QUAL — e a primeira
 * pergunta de quem lê o aviso é exatamente essa.
 */
export function avisoDaConexao(
  saude: SaudeObservada,
  apelido: string,
  /** Só os avisos novos do número de pé com defeito saem no idioma da organização; os de queda seguem em português. */
  idioma: Idioma = "pt-BR",
): AvisoDeConexao | null {
  if (!saude.reachable) {
    // "Não deu para perguntar" tem DOIS motivos que pedem ações opostas, e
    // tratá-los igual foi o defeito medido: numa VPS real a chave do WAHA foi
    // trocada por uma segunda cópia do repo, TUDO parou, e por três dias a
    // Central mostrou apenas um `warn` dizendo "não foi possível verificar" —
    // a frase que se usa para descrever um soluço de rede. O dono só descobriu
    // ao tentar conectar um número e ver o 401 na cara.
    //
    // Quando o transporte RESPONDE recusando a credencial, não há incerteza
    // sobre a causa nem sobre o que fazer — e escanear o QR, que é o reflexo de
    // quem lê "conexão caída", não resolve nada.
    if (saude.detail === DETALHE_CREDENCIAL_RECUSADA) {
      return {
        kind: "channel_number_alert",
        severity: "critical",
        title: `Conexão "${apelido}": o servidor de WhatsApp recusou a chave de acesso`,
        body:
          "Escanear o QR não resolve: a chave que o CRM usa para falar com o servidor de WhatsApp não confere com a que o servidor espera. Enquanto isso durar, nenhuma mensagem entra nem sai por NENHUMA conexão. Quem cuida do servidor precisa conferir a WAHA_API_KEY do .env e recriar o contêiner do WhatsApp.",
        episodio: "CREDENCIAL_RECUSADA",
      };
    }

    // Não deu para perguntar mesmo. NÃO é o mesmo que estar caído, e o aviso
    // não pode afirmar o que não sabe: a ação aqui é olhar o serviço, não
    // escanear um QR. `warn` e não `critical` porque uma oscilação de rede cabe
    // neste ramo, e gritar por ela ensinaria o operador a ignorar a cor.
    return {
      kind: "channel_number_alert",
      severity: "warn",
      title: `Não foi possível verificar a conexão "${apelido}"`,
      body: saude.detail,
      episodio: "UNREACHABLE",
    };
  }

  const status = (saude.status ?? "").toUpperCase();
  if (status === STATUS_SAUDAVEL) {
    // De pé, mas sem receber ou com o envio restrito: avisa sem derrubar.
    const defeito = defeitoDoNumero(saude.detail);
    return defeito ? avisoDoDefeito(defeito, apelido, idioma) : null;
  }
  if (!status) return null;
  if (!(STATUS_QUE_AVISAM as readonly string[]).includes(status)) return null;

  if (status === "SCAN_QR_CODE") {
    return {
      kind: "qr_rescan",
      severity: "critical",
      title: `WhatsApp "${apelido}" desconectado — precisa escanear o QR de novo`,
      body: "Enquanto isso não acontecer, nenhuma mensagem entra nem sai por esta conexão.",
      episodio: status,
    };
  }

  return {
    kind: "channel_number_alert",
    severity: "critical",
    title: `WhatsApp "${apelido}" fora do ar (${status})`,
    body: "Nenhuma mensagem entra nem sai por esta conexão até ela voltar.",
    episodio: status,
  };
}

/** O que a faixa do topo mostra: conexão caída, com nome. */
export interface ConexaoCaida {
  id: string;
  apelido: string;
  status: string;
}

/**
 * As conexões desta organização que estão fora do ar AGORA — e que QUEM PEDE
 * pode ver.
 *
 * Mora aqui, e não na tela, por duas razões que o repo já pagou caro: telas que
 * montam a consulta de `channel_sessions` à mão foram o que deixou três
 * seletores oferecendo canal arquivado (vigiado por `canais-selecionaveis`); e o
 * filtro de estados precisa ser LITERALMENTE o mesmo que decide o aviso — uma
 * segunda lista divergiria com o tempo, e a faixa deixaria de aparecer para um
 * estado que a Central considera grave.
 *
 * O cliente recebido determina se a consulta respeita a RLS. O layout passa
 * o cliente da sessão, como GET /api/v1/channel-sessions; o cliente de serviço
 * atravessaria as policies. O filtro explícito de organização vale nos dois.
 *
 * A policy nativa de SELECT permite leitura por organização e acesso de
 * plataforma. Não há nela divisão de canais por atendente. O recorte adicional
 * da instalação do relato #3088 só será herdado aqui se estiver na RLS; um patch
 * exclusivo da rota não é aplicado por esta consulta direta.
 *
 * Arquivada fica de fora: foi desligada de propósito, e anunciar que uma conexão
 * aposentada está parada é o ruído que ensina a ignorar a faixa.
 */
export async function listarConexoesCaidas(
  cliente: SupabaseClient,
  organizationId: string,
): Promise<ConexaoCaida[]> {
  const { data } = await cliente
    .from("channel_sessions")
    .select("id, display_name, phone_number, status, waha_session_name")
    .eq("organization_id", organizationId)
    .is("archived_at", null)
    // A faixa diz "nenhuma mensagem entra nem sai por esta conexão" e leva a
    // Conexões. A linha de chamada de voz (spec 18) não é vigiada pelo cron de
    // saúde (nenhum adapter a consulta), então o `status` dela envelhece parado:
    // anunciá-la aqui seria a faixa permanente que ensina a ignorar a faixa.
    .in("provider", [...PROVIDERS_DE_MENSAGEM])
    .in("status", [...STATUS_QUE_AVISAM]);

  return (data ?? [])
    // A linha de seed do e2e (#1032) é o MESMO caso por outra porta: ninguém a
    // criou pela tela, ninguém a remove, e o `status` dela é `STOPPED` para
    // sempre — a faixa anunciaria como conexão de alguém algo que não é
    // conexão de ninguém, a mesma faixa permanente do parágrafo acima. Quem não
    // é seed passa por este filtro intacto, inclusive caído.
    .filter((s) => !ehNomeDeSessaoE2E(s.waha_session_name as string | null))
    // ─── E sessão SEM NÚMERO E SEM NOME também não (#3416) ────────────────────
    //
    // Abriu o QR, não leu, o código expirou: a linha fica `FAILED` sem nunca
    // ter tido telefone, e a faixa dizia "WhatsApp sem nome está desconectado —
    // nenhuma mensagem entra nem sai". Não entra nem sai nada porque nunca houve
    // número aqui; para quem só olha o produto no teste grátis, isso é um
    // quebrado que não quebrou.
    //
    // As DUAS réguas juntas, e a segunda veio medida no e2e
    // (`tests/e2e/atender-pelo-celular.spec.ts`, caso A-6): um canal CAÍDO com
    // apelido e sem número gravado continua sendo conexão de alguém e tem de
    // aparecer — a faixa é o único aviso dele. O que não é conexão de ninguém é
    // a linha que não tem nem número (o apelido cairia em "sem nome") nem nome.
    //
    // As duas colunas já vinham no select (é delas que sai o apelido), então o
    // filtro não custa consulta. Número é a régua principal: é o que a rota de
    // saúde de Conexões grava quando a sessão está `WORKING`
    // (`app/api/v1/channel-sessions/[id]/route.ts`, `numeroObservadoDaSessao`).
    .filter(
      (s) =>
        Boolean((s.phone_number as string | null)?.trim()) ||
        Boolean((s.display_name as string | null)?.trim()),
    )
    .map((s) => ({
    id: s.id as string,
    apelido: (s.display_name as string | null) ?? (s.phone_number as string | null) ?? "sem nome",
    status: (s.status as string | null) ?? "",
  }));
}

interface SessaoParaVigiar {
  id: string;
  organization_id: string;
  status: string | null;
  /** O motivo gravado, quando quem chamou o leu: é onde mora o defeito do número aberto. */
  status_reason?: string | null;
  /**
   * O NÚMERO da conexão, quando quem chamou leu a linha.
   *
   * `null` EXPLÍCITO = a sessão não tem número, nunca teve: ela nasceu de um QR
   * aberto que ninguém leu (#3416). `undefined` = a chamada não leu esta coluna,
   * e NADA muda — quem não sabe o número não muda de comportamento, senão um
   * caller que deixasse de selecionar `phone_number` silenciaria o aviso de uma
   * conexão de verdade.
   */
  phone_number?: string | null;
}

/**
 * Escala uma vez por episódio e RESOLVE quando volta.
 *
 * O "uma vez" mora em `escalated_status`: sem ele, um vigia de 5 em 5 minutos
 * abriria doze avisos por hora do mesmo problema, e a Central viraria uma lista
 * do mesmo aviso — que é a forma mais rápida de tornar a Central inútil.
 *
 * Resolver na volta é a outra metade, e a menos óbvia: aviso que não fecha
 * sozinho ensina que a Central mostra coisa velha, e aí o operador para de
 * confiar no que está aberto. Só fecha os avisos DESTA sessão (`ref_id`) —
 * fechar por organização apagaria o alerta de um outro canal que segue caído.
 */
export async function sincronizarSaudeDaConexao(
  admin: SupabaseClient,
  sessao: SessaoParaVigiar,
  saude: SaudeObservada,
  apelido: string,
  /**
   * QUEM observou — e é o que decide se esta observação pode FECHAR o aviso.
   *
   * ─── O defeito que isto existe para impedir ───────────────────────────────
   *
   * As duas fontes medem coisas DIFERENTES:
   *
   *   - o empurrão do provedor fala do NÚMERO ("suspenso", "desconectado");
   *   - a varredura fala da CREDENCIAL e da conta ("a chave responde, a conta
   *     está na lista").
   *
   * Um número suspenso continua aparecendo na lista de contas. Então a
   * varredura via `WORKING`, `avisoDaConexao` devolvia `null`, e o ramo de
   * baixo marcava como resolvido o crítico que o empurrão tinha aberto minutos
   * antes — com o número ainda suspenso. O aviso sumia da Central em ≤5 min e
   * o provedor não reenvia o evento, então ninguém ficava sabendo.
   *
   * Trocar "aviso que nunca fecha" por "aviso que fecha sozinho sem o problema
   * ter acabado" é o pior dos dois, porque o segundo parece que funciona.
   *
   * Regra: só fecha quem sabe do que está falando. O empurrão fecha qualquer
   * episódio (ele é a autoridade sobre o número); a varredura fecha só o que
   * ela mesma poderia ter aberto.
   */
  origem: "varredura" | "empurrao" = "varredura",
  /** O idioma da organização, para os avisos que já saem traduzidos (`avisoDaConexao`). */
  idioma: Idioma = "pt-BR",
): Promise<"avisado" | "ja_avisado" | "resolvido" | "sem_mudanca"> {
  // ─── A sessão SEM NÚMERO não é conexão fora do ar (#3416) ─────────────────
  //
  // Abre o QR e não lê: a sessão nasce `SCAN_QR_CODE` e o WAHA a derruba em
  // `FAILED` quando o código expira. Do ponto de vista do aviso isso é uma
  // queda — e era: o assistente do primeiro acesso já abria o crítico
  // "precisa escanear o QR de novo" no segundo em que a pessoa pedia o código,
  // e a expiração abria o segundo ("fora do ar (FAILED)"). Duas gritas para uma
  // pessoa que está com o celular na mão olhando PARA o QR, e nenhum número
  // para perder: "nenhuma mensagem entra nem sai por esta conexão" só diz algo
  // quando há um número no ar.
  //
  // Nulo EXPLÍCITO é a regra; `undefined` (quem não leu a coluna) segue avisando
  // como sempre. E o ramo de `!aviso` abaixo é o que FECHA um episódio já
  // aberto — então esta mesma chamada limpa o aviso que a própria sessão
  // abriu antes de expirar, sem precisar de um segundo caminho.
  const aviso = sessao.phone_number === null ? null : avisoDaConexao(saude, apelido, idioma);

  const { data: linha } = await admin
    .from("channel_session_health")
    .select("escalated_status")
    .eq("organization_id", sessao.organization_id)
    .eq("channel_session_id", sessao.id)
    .maybeSingle();
  const jaEscalado = (linha?.escalated_status as string | null) ?? null;

  if (!aviso) {
    if (!jaEscalado) return "sem_mudanca";
    // O episódio veio do provedor e quem está observando é a varredura: ela não
    // tem como saber se o número deixou de estar suspenso. Não fecha.
    if (origem === "varredura" && jaEscalado.startsWith(PREFIXO_EMPURRAO)) {
      return "sem_mudanca";
    }
    if (defeitoSegueSemMedida(saude, sessao.status_reason)) return "sem_mudanca";
    await admin
      .from("agent_inbox_items")
      .update({ status: "resolved" })
      .eq("organization_id", sessao.organization_id)
      .eq("ref_kind", REF_KIND_SESSAO)
      .eq("ref_id", sessao.id)
      .eq("status", "open");
    await gravarEpisodio(admin, sessao, null);
    return "resolvido";
  }

  // O episódio carrega QUEM o observou. Sem isso, a varredura não teria como
  // saber que aquele crítico veio do provedor — e voltaria a fechá-lo.
  const episodio =
    origem === "empurrao" ? `${PREFIXO_EMPURRAO}${aviso.episodio}` : aviso.episodio;

  // Mesmo episódio: já foi avisado, e repetir é ruído. Um episódio DIFERENTE
  // (caiu por QR, agora está FAILED) avisa de novo — mudou o que fazer.
  if (jaEscalado === episodio) return "ja_avisado";

  await admin.from("agent_inbox_items").insert({
    organization_id: sessao.organization_id,
    kind: aviso.kind,
    severity: aviso.severity,
    title: aviso.title,
    body: aviso.body,
    ref_kind: REF_KIND_SESSAO,
    ref_id: sessao.id,
  });
  await gravarEpisodio(admin, sessao, episodio);
  return "avisado";
}

/**
 * A CONEXÃO FOI REMOVIDA — o aviso dela não pode ficar aberto para sempre.
 *
 * ─── O defeito que isto fecha (#1023) ───────────────────────────────────────
 *
 * `sincronizarSaudeDaConexao` é o ÚNICO lugar que fecha um episódio, e ela roda
 * quando a sessão MANDA um status novo (`handleSessionStatus`). Arquivar ou
 * excluir uma conexão tira o único emissor que existia: o arquivamento a remove
 * do WAHA e a rota de webhook recusa evento de canal arquivado, por desenho. O
 * crítico ficava aberto apontando para uma linha que a tela já não carrega —
 * "Este contexto não está disponível para você" —, um alarme permanente sobre
 * algo que o próprio operador já resolveu, enquanto a conexão NOVA, com o mesmo
 * número, aparecia `WORKING` nos dois lados.
 *
 * ─── O que este caminho fecha, e o que ele NÃO faz ──────────────────────────
 *
 * Fecha os avisos ABERTOS desta sessão (`ref_kind`/`ref_id`), que é a MESMA
 * chave do caminho de volta — e mais ninguém: fechar por organização apagaria o
 * alerta de outro canal que segue caído.
 *
 * E devolve `escalated_status` a null sem criar a linha de saúde: `upsert` aqui
 * inventaria episódio para um canal que nunca avisou e, no ramo do delete, a
 * linha já caiu com a sessão (`channel_session_health.channel_session_id` é
 * `on delete cascade`) — um insert morreria com violação de FK e derrubaria uma
 * exclusão que o operador já pediu e que já aconteceu.
 *
 * ─── Por que LER antes de escrever ──────────────────────────────────────────
 *
 * Não é economia de bytes: é contrato da rota. O teste dela afirma a contagem
 * EXATA de escritas do arquivamento, e o canal virgem — o caso mais comum de
 * exclusão — nunca teve aviso nem linha de saúde. Sem nada aberto, nada é
 * escrito.
 */
export async function resolverSaudeDaConexaoRemovida(
  admin: SupabaseClient,
  sessao: SessaoParaVigiar,
): Promise<"resolvido" | "sem_mudanca"> {
  // O supabase-js não lança em erro do PostgREST: devolve `error`. Engolido,
  // uma leitura que falhou viraria "nenhum aviso aberto" e a auditoria diria
  // "sem_mudanca" com o alarme ainda na Central. Lançar é o que faz a rota
  // registrar "falhou" e logar o motivo. A mensagem leva só a etapa e o código.
  const { data: abertos, error: erroLeitura } = await admin
    .from("agent_inbox_items")
    .select("id")
    .eq("organization_id", sessao.organization_id)
    .eq("ref_kind", REF_KIND_SESSAO)
    .eq("ref_id", sessao.id)
    .eq("status", "open");
  if (erroLeitura) throw falhaNaEtapa("ler os avisos abertos", erroLeitura);

  let fechou = false;

  if ((abertos ?? []).length > 0) {
    // Pelo FILTRO, e não pela lista de ids lida acima: a sessão já não existe,
    // nada mais pode abrir aviso para ela, e um `in (ids)` deixaria de fora
    // justamente o item que aparecesse entre a leitura e a escrita.
    const { error } = await admin
      .from("agent_inbox_items")
      .update({ status: "resolved" })
      .eq("organization_id", sessao.organization_id)
      .eq("ref_kind", REF_KIND_SESSAO)
      .eq("ref_id", sessao.id)
      .eq("status", "open");
    if (error) throw falhaNaEtapa("resolver os avisos abertos", error);
    fechou = true;
  }

  const { data: linha, error: erroSaude } = await admin
    .from("channel_session_health")
    .select("escalated_status")
    .eq("organization_id", sessao.organization_id)
    .eq("channel_session_id", sessao.id)
    .maybeSingle();
  if (erroSaude) throw falhaNaEtapa("ler o episódio de saúde", erroSaude);

  if (((linha?.escalated_status as string | null) ?? null) !== null) {
    const { error } = await admin
      .from("channel_session_health")
      .update({ escalated_status: null, updated_at: new Date().toISOString() })
      .eq("organization_id", sessao.organization_id)
      .eq("channel_session_id", sessao.id);
    if (error) throw falhaNaEtapa("limpar o episódio de saúde", error);
    fechou = true;
  }

  return fechou ? "resolvido" : "sem_mudanca";
}

function falhaNaEtapa(etapa: string, erro: { code?: string }): Error {
  return new Error(`Falha ao ${etapa} da conexão removida (código ${erro.code ?? "desconhecido"})`);
}

/**
 * A linha de saúde pode não existir ainda — `upsert` pelo par único, e não
 * `update`, porque um `update` que não acha linha não escreve nada e o dedup
 * silenciosamente pararia de funcionar: avisaria de novo a cada rodada.
 */
async function gravarEpisodio(
  admin: SupabaseClient,
  sessao: SessaoParaVigiar,
  episodio: string | null,
): Promise<void> {
  await admin.from("channel_session_health").upsert(
    {
      organization_id: sessao.organization_id,
      channel_session_id: sessao.id,
      status: sessao.status ?? "UNKNOWN",
      escalated_status: episodio,
      updated_at: new Date().toISOString(),
    },
    { onConflict: "organization_id,channel_session_id" },
  );
}
