"use client";

import { useState, useTransition } from "react";

import { updateComportamento } from "@/app/actions/settings/updateComportamento";
import { updateModuloDaInstalacao } from "@/app/actions/settings/updateModuloDaInstalacao";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { useT } from "@/hooks/i18n/useT";
import { caminhoDaPorta, ondeOModuloAparece } from "@/lib/navigation/onde-o-modulo-aparece";
import { MENSAGEM_DA_RECUSA_DE_ESCRITA, ehRecusaDeEscrita } from "@/lib/auth/recusa-de-escrita-de-admin";
import type {
  ChaveDeOrcamentoDaInstalacao,
  ComportamentoDaInstalacao,
} from "@/lib/instalacao/comportamento";
import type { ModuloOpcional, MODULOS_OPCIONAIS_POR_FLAG } from "@/lib/instalacao/modulos";

/** Só os módulos que esta tela liga/desliga — nunca "honorarios" (módulo de
 * tabela, ADR-0002): `updateModuloDaInstalacao` não aceita esse valor, e o tipo
 * aqui existe pra isso dar erro em build, não silenciosamente em runtime. */
type ModuloPorFlag = (typeof MODULOS_OPCIONAIS_POR_FLAG)[number];

/** O marcador das frases da assinatura; fora do JSX porque não é prosa. */
const MOMENTO = "{momento}";

/**
 * O que as últimas entregas do WhatsApp disseram sobre a assinatura (doc 99,
 * opção A), com os instantes já formatados pelo servidor. `null` = a leitura
 * falhou, e a tela não diz nada. A regra do "sim" mora em
 * `lib/channels/assinatura-das-entregas.ts`.
 */
export interface AssinaturaNaTela {
  readonly assinadas: boolean | null;
  readonly ultimaAssinada: string | null;
  readonly ultimaSemAssinatura: string | null;
}

/**
 * Cada interruptor salva na hora, sem botão de confirmar — mesmo desenho do
 * formulário da política de cadastro, e pelo mesmo motivo: é reversível com um
 * clique e o registro de quem trocou (e de qual valor para qual) fica na trilha
 * de auditoria, que é onde a pergunta "por que a IA não parou?" é respondida.
 *
 * A gravação manda o estado INTEIRO das quatro chaves, e não só a que mudou: é
 * um `upsert` de uma linha só, e mandar o estado todo evita que duas telas
 * abertas se sobrescrevam em campos que ninguém tocou.
 */
export function FormularioDeComportamento({
  inicial,
  assinatura = null,
}: {
  inicial: ComportamentoDaInstalacao;
  assinatura?: AssinaturaNaTela | null;
}) {
  const t = useT();
  const [valores, setValores] = useState<ComportamentoDaInstalacao>(inicial);
  const [erro, setErro] = useState<string | null>(null);
  const [pendente, startTransition] = useTransition();

  function trocar<K extends keyof ComportamentoDaInstalacao>(
    campo: K,
    valor: ComportamentoDaInstalacao[K],
  ) {
    const anterior = valores;
    setErro(null);
    // Otimista, e com volta explícita no erro: sem a volta, uma falha de
    // gravação deixaria a tela dizendo "desligado" com a proteção ligada — o
    // pior estado possível para uma configuração de comportamento.
    setValores({ ...valores, [campo]: valor });
    startTransition(async () => {
      const r = await updateComportamento({ ...valores, [campo]: valor });
      if (!r.ok) {
        setValores(anterior);
        setErro(t(ehRecusaDeEscrita(r.error) ? MENSAGEM_DA_RECUSA_DE_ESCRITA[r.error] : "Não deu para salvar. Tente de novo em instantes."));
      }
    });
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle>{t("O que esta instalação faz")}</CardTitle>
        <CardDescription>
          {t(
            "Cada escolha vale para todas as empresas daqui. Quem cuida do servidor pode declarar um valor no arquivo de ambiente, mas ele só responde até a primeira leitura do banco: a partir daí, manda o que estiver aqui.",
          )}
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="flex items-start justify-between gap-4 rounded-lg border p-4">
          <div className="space-y-1">
            <Label htmlFor="orcamento-de-ia" className="text-base">
              {t("Proteção de gasto de IA")}
            </Label>
            <p className="text-sm text-muted-foreground">
              {t(
                "Decide o que acontece quando o gasto passa do teto que a empresa escolheu. Não liga a proteção de ninguém: só pode afrouxá-la.",
              )}
            </p>
          </div>
          <Select
            value={valores.orcamento_de_ia}
            onValueChange={(v) => trocar("orcamento_de_ia", v as ChaveDeOrcamentoDaInstalacao)}
            disabled={pendente}
          >
            <SelectTrigger id="orcamento-de-ia" className="w-[200px]" aria-label={t("Proteção de gasto de IA")}>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="on">{t("Respeita o teto de cada empresa")}</SelectItem>
              <SelectItem value="avisar">{t("Só avisa, nunca para a IA")}</SelectItem>
              <SelectItem value="off">{t("Desligada")}</SelectItem>
            </SelectContent>
          </Select>
        </div>

        <div className="flex items-start justify-between gap-4 rounded-lg border p-4">
          <div className="space-y-1">
            <Label htmlFor="assinatura-do-webhook" className="text-base">
              {t("Exigir assinatura nas entregas do canal")}
            </Label>
            <p className="text-sm text-muted-foreground">
              {t(
                "Ligado, toda entrega de webhook precisa vir assinada com o segredo da sessão. Desligado por padrão porque nem todo servidor de canal assina: ligar sem que ele assine corta a entrada de mensagens.",
              )}
            </p>
            {assinatura && (
              <div className="space-y-1 text-sm" data-testid="assinatura-das-entregas">
                {assinatura.assinadas === null ? (
                  <p className="text-muted-foreground">
                    {t("Nenhuma entrega do WhatsApp na última semana para conferir a assinatura.")}
                  </p>
                ) : assinatura.assinadas ? (
                  <p>
                    {t("As últimas entregas do WhatsApp chegaram assinadas: sim (última em {momento}).").replace(
                      MOMENTO,
                      assinatura.ultimaAssinada ?? "",
                    )}
                  </p>
                ) : (
                  <>
                    <p>{t("As últimas entregas do WhatsApp chegaram assinadas: não.")}</p>
                    {assinatura.ultimaAssinada && (
                      <p className="text-muted-foreground">
                        {t("Última assinada: {momento}.").replace(MOMENTO, assinatura.ultimaAssinada)}
                      </p>
                    )}
                  </>
                )}
                {assinatura.ultimaSemAssinatura && (
                  <p className="text-muted-foreground">
                    {t("Última sem assinatura: {momento}.").replace(MOMENTO, assinatura.ultimaSemAssinatura)}
                  </p>
                )}
                {assinatura.assinadas === true && !valores.exigir_assinatura_no_webhook && (
                  <p className="font-medium" role="status">
                    {t("Pode ligar: o WhatsApp já assina.")}
                  </p>
                )}
              </div>
            )}
          </div>
          <Switch
            id="assinatura-do-webhook"
            checked={valores.exigir_assinatura_no_webhook}
            onCheckedChange={(v) => trocar("exigir_assinatura_no_webhook", v)}
            disabled={pendente}
            aria-label={t("Exigir assinatura nas entregas do canal")}
          />
        </div>

        <div className="flex items-start justify-between gap-4 rounded-lg border p-4">
          <div className="space-y-1">
            <Label htmlFor="divulgacao-de-pagamento" className="text-base">
              {t("Divulgação de pagamento no atendimento")}
            </Label>
            <p className="text-sm text-muted-foreground">
              {t(
                "Injetar acrescenta o texto de divulgação à primeira mensagem. Vetar bloqueia o envio sem ele e devolve ao modelo a razão, para ele reescrever.",
              )}
            </p>
          </div>
          <Select
            value={valores.divulgacao_de_pagamento}
            onValueChange={(v) =>
              trocar("divulgacao_de_pagamento", v as ComportamentoDaInstalacao["divulgacao_de_pagamento"])
            }
            disabled={pendente}
          >
            <SelectTrigger
              id="divulgacao-de-pagamento"
              className="w-[200px]"
              aria-label={t("Divulgação de pagamento no atendimento")}
            >
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="inject">{t("Injetar")}</SelectItem>
              <SelectItem value="veto">{t("Vetar")}</SelectItem>
            </SelectContent>
          </Select>
        </div>

        <div className="flex items-start justify-between gap-4 rounded-lg border p-4">
          <div className="space-y-1">
            <Label htmlFor="promessa-semantica" className="text-base">
              {t("Conferência de promessa antes de enviar")}
            </Label>
            <p className="text-sm text-muted-foreground">
              {t(
                "Ligado, cada envio passa por uma conferência de modelo para não prometer o que a empresa não cumpre. Custa uma chamada de modelo por envio.",
              )}
            </p>
          </div>
          <Switch
            id="promessa-semantica"
            checked={valores.promessa_semantica}
            onCheckedChange={(v) => trocar("promessa_semantica", v)}
            disabled={pendente}
            aria-label={t("Conferência de promessa antes de enviar")}
          />
        </div>

        {erro && (
          <p className="text-sm text-destructive" role="alert">
            {erro}
          </p>
        )}
      </CardContent>
    </Card>
  );
}

/**
 * Os MÓDULOS OPCIONAIS da instalação — desligados por padrão, e é aqui, e só
 * aqui, que se ligam (doc 24: liga/desliga de configuração geral tem tela, sem
 * `.env`). Mesmo desenho do cartão de cima: salva no clique, volta no erro.
 */
/** Cada módulo, como ele aparece aqui. O texto diz o que ligar ABRE, não só o nome. */
export const MODULOS_NA_TELA: ReadonlyArray<{
  modulo: ModuloPorFlag;
  id: string;
  rotulo: string;
  descricao: string;
  /**
   * Onde o módulo aparece quando ele NÃO cria porta no menu do CRM. Só os que
   * não têm porta declaram; para os outros, `OndeAparece` lê o caminho do
   * próprio menu (`ondeOModuloAparece`) e esta linha não existe.
   *
   * O texto é literal AQUI, e não lido de uma tabela em `lib/`, porque é a
   * forma que as duas catracas de espanhol conseguem ler — a mesma de
   * `descricao`, logo acima. `t()` sobre parâmetro destrinchado
   * (`([k, texto]) => t(texto)`) é reprovado por "parâmetro livre", e `t()`
   * sobre `const` local, por "chave montada em runtime". Quem garante que
   * nenhum módulo fica sem resposta é `tests/unit/porta-do-modulo-ligado.test.tsx`,
   * que cruza esta lista com as portas reais do menu.
   */
  foraDoMenu?: string;
  /**
   * Quando o módulo não cria porta própria mas LIBERA um painel dentro de uma tela que já existe,
   * aqui vai o `href` dessa tela — e o caminho é DERIVADO (`caminhoDaPorta`), nunca escrito.
   *
   * A primeira versão escrevia o caminho dentro do `foraDoMenu`, e errou: dizia
   * "Agente de IA › Credenciais" quando `/app/ai/credentials` também é só-no-hub. Caminho à mão
   * envelhece e ninguém percebe — hoje uma guarda proíbe `›` em `foraDoMenu`.
   */
  painelEm?: string;
}> = [
  {
    modulo: "banco_externo",
    id: "modulo-banco-externo",
    rotulo: "Banco de dados externo",
    descricao:
      "Ligado, cada empresa pode conectar o banco de outro sistema (um ERP, outro CRM) para o agente consultar. Isso guarda a senha daquele banco neste servidor e abre conexão com ele. Desligado, a tela, o menu e as ferramentas do agente somem.",
  },
  {
    modulo: "fluxos_atendimento",
    id: "modulo-fluxos-atendimento",
    rotulo: "Fluxos de atendimento",
    descricao:
      "Ligado, cada empresa pode montar roteiros de perguntas que a IA conduz durante a conversa (nome, CPF, interesse…), e as respostas aparecem na ficha do cliente. Desligado, a tela, o menu e o roteiro no atendimento da IA somem.",
  },
  {
    modulo: "propostas",
    id: "modulo-propostas",
    rotulo: "Propostas comerciais",
    descricao:
      "Ligado, cada empresa ainda precisa ligar o módulo de proposta comercial nas configurações dela — o caminho aparece abaixo. A IA levanta o que o cliente precisa, monta a proposta pelos modelos da empresa e o PDF sai pelo WhatsApp. Desligado, nenhuma empresa vê a tela, o menu nem as ferramentas do agente.",
  },
  {
    modulo: "crm_b2b",
    id: "modulo-crm-b2b",
    rotulo: "Empresas e pessoas (venda para empresas)",
    descricao:
      "Ligado, cada empresa ganha no CRM o cadastro de Empresas (razão social e CNPJ, com os dados públicos preenchidos pela BrasilAPI), as Pessoas que decidem dentro delas, com vários telefones, e a importação de planilha CSV ou Excel. Consultar um CNPJ manda o número para a BrasilAPI. Desligado, as telas e o menu somem.",
  },
  {
    modulo: "cobranca",
    id: "modulo-cobranca",
    rotulo: "Cobrança dos seus clientes",
    descricao:
      "Ligado, você cria planos e cobra as empresas hospedadas aqui, com teste grátis e suspensão automática de quem não paga. Empresas que já existem ficam isentas; você escolhe quem passa a pagar. Desligado, os limites dos planos deixam de valer e as empresas suspensas por falta de pagamento são liberadas; nada é cancelado no provedor de pagamento.",
    foraDoMenu: "Não cria porta no menu do CRM: a tela é Cobrança, aqui no Admin.",
  },
  {
    modulo: "login_codex",
    id: "modulo-login-codex",
    rotulo: "Login do Codex por assinatura",
    descricao:
      "Ligado, cada empresa vê em Credenciais o painel para conectar a própria conta do Codex. Desligado por padrão: sem este interruptor nada aparece para as empresas, e a reserva de chamada continua sendo a chave de API da organização.",
    foraDoMenu: "Não cria porta no menu do CRM: ele libera um painel em",
    painelEm: "/app/ai/credentials",
  },
  {
    modulo: "setores",
    id: "modulo-setores",
    rotulo: "Setores de atendimento",
    descricao:
      "Ligado, cada empresa ganha na Equipe a aba Setores, para agrupar os atendentes por área, e a aba Atendimento passa a mostrar e filtrar o setor de cada um. A conversa com setor definido vai só para quem atende aquele setor, dentro do horário dele; conversa sem setor continua indo pelo número. Desligado, a aba e a coluna somem e a distribuição volta a ser a de antes; os setores já cadastrados ficam guardados.",
    foraDoMenu: "Não cria porta no menu do CRM: ele libera uma aba em",
    painelEm: "/app/team",
  },
  {
    modulo: "avisos",
    id: "modulo-avisos",
    rotulo: "Avisos para a equipe",
    descricao:
      "Ligado, o administrador de cada empresa pode escrever um aviso com título e texto, e ele chega como notificação nos aparelhos da equipe que ativaram as notificações. Desligado, ninguém envia avisos; os já enviados ficam guardados.",
    foraDoMenu: "Não cria porta no menu do CRM: por enquanto o aviso é enviado pela API, em /api/v1/avisos.",
  },  {
    modulo: "conferir_whatsapp",
    id: "modulo-conferir-whatsapp",
    rotulo: "Conferir WhatsApp na prospecção",
    descricao:
      "Ligado, a prospecção pergunta ao WhatsApp, pela conexão por QR da empresa, se cada telefone achado numa busca tem conta, e quem não tem sai da abordagem. A pergunta sai do número da empresa, para todo telefone achado, a qualquer hora; consulta em massa assim pode chamar a atenção do WhatsApp e arriscar o número. Desligado, nada é consultado e a abordagem segue como antes.",
    foraDoMenu: "Não cria porta no menu do CRM: a conferência aparece na lista de empresas em",
    painelEm: "/app/prospecting",
  },
  {
    modulo: "mcp_por_agente",
    id: "modulo-mcp-por-agente",
    rotulo: "Ferramentas externas por agente",
    descricao:
      "Ligado, a tela de cada agente ganha a seção Servidores MCP, para conectar sistemas de terceiros com chave fixa ou com o login de cada cliente, que autoriza a própria conta por um link na conversa. Desligado, a seção some, o agente deixa de usar esses servidores e os links de autorização param de abrir; o que já foi cadastrado fica guardado.",
    foraDoMenu: "Não cria porta no menu do CRM: ele libera uma seção na tela de cada agente, em",
    painelEm: "/app/ai/agents",
  },
  {
    modulo: "cold_call",
    id: "modulo-cold-call",
    rotulo: "Cold Call (fila de ligações)",
    descricao:
      "Ligado, cada empresa ganha no CRM a tela Cold Call: um funil próprio, a importação da lista e a ligação para um lead por vez, pelo canal de voz quando ele está pareado ou pelo discador do aparelho. Quem pediu para não ser incomodado, contato pessoal e contato anonimizado não recebem ligação. Desligado, a tela e o menu somem.",
  },
  {
    modulo: "status_whatsapp",
    id: "modulo-status-whatsapp",
    rotulo: "Status do WhatsApp por integração",
    descricao:
      "Ligado, um sistema conectado ao servidor MCP com token de escrita pode publicar uma imagem no Status de um número conectado por QR Code. A imagem sai de verdade e some em 24 horas. Numa lista de contatos, quem pediu para não ser incomodado fica de fora. Para todos os contatos, a conexão precisa guardar o histórico, e quem vê é decidido pelo WhatsApp, inclusive quem pediu para não ser incomodado. Desligado, a ferramenta some.",
    foraDoMenu: "Não cria porta no menu do CRM: a publicação é uma ferramenta do servidor MCP, sem tela própria.",
  },
  {
    modulo: "painel_atendimento",
    id: "modulo-painel-atendimento",
    rotulo: "Painel de atendimento",
    descricao:
      "Ligado, cada empresa ganha em Análise a tela Painel de atendimento: quem espera resposta, quem está em atendimento e quem foi atendido hoje, por número, com o que cada pessoa já vê no Inbox. Desligado, a tela e o menu somem, e nenhuma conversa muda.",
  },
  {
    modulo: "reservas_de_lugar",
    id: "modulo-reservas-de-lugar",
    rotulo: "Reservas de lugar",
    descricao:
      "Ligado, cada empresa pode cadastrar na agenda um lugar, como mesa, camarote ou quadra, com capacidade em pessoas, e a reserva passa a escolher o lugar e dizer quantas pessoas vêm. Desligado, ninguém cria lugar novo; os lugares e as reservas que já existem ficam guardados.",
    foraDoMenu: "Não cria porta no menu do CRM: por enquanto o lugar é cadastrado e reservado pela API, em /api/v1/agenda/servicos.",
  },
  {
    modulo: "link_de_pagamento",
    id: "modulo-link-de-pagamento",
    rotulo: "Link de pagamento na conversa",
    descricao:
      "Ligado, a atendente registra numa conversa o valor a cobrar e recebe um endereço de aviso, que ela cola como URL do Webhook ao criar o link de pagamento na InfinitePay. Quando o cliente paga, o aviso marca o recebimento como pago e leva o negócio para a etapa de ganho, se o valor bater com o registrado. Desligado, ninguém registra recebimento e os avisos são recusados; os recebimentos já registrados ficam guardados.",
    foraDoMenu: "Não cria porta no menu do CRM: por enquanto o recebimento é registrado pela API, em /api/v1/conversations/[id]/pagamentos.",
  },
  {
    modulo: "campanha_por_email",
    id: "modulo-campanha-por-email",
    rotulo: "Campanha por e-mail (em construção)",
    descricao:
      "Este interruptor ainda não pode ser ligado: a campanha por e-mail guarda o assunto, o corpo e o público, mas esta versão não envia o e-mail nem tem o descadastro e a lista de quem pediu para não receber. Se ele já estiver ligado, dá para desligar: a opção E-mail some da nova campanha e as campanhas já criadas ficam guardadas.",
    foraDoMenu: "Não cria porta no menu do CRM: a opção E-mail aparece dentro de Campanhas, ao criar uma nova.",
  },
  {
    modulo: "revisao_por_km",
    id: "modulo-revisao-por-km",
    rotulo: "Revisão por quilometragem",
    descricao:
      "Ligado, o funil que declarar a régua de revisão por quilometragem passa a calcular a data prevista da próxima revisão quando o km do negócio é informado, pelo card, pela API, pelo MCP ou pela resposta do roteiro, e grava essa data no campo de data que o funil escolheu. Desligado, o km continua sendo gravado e nenhuma data é calculada; as datas já gravadas ficam como estão.",
    foraDoMenu: "Não cria porta no menu do CRM: a régua é declarada por funil, em pipelines.settings.revisao_por_km, e ainda não há tela para declará-la.",
  },
];

/**
 * ONDE O MÓDULO APARECE — a frase que faltava na tela do interruptor.
 *
 * O defeito: quem administra a instalação ligava um módulo aqui, ia ao CRM, não
 * achava nada e concluía que o interruptor não funcionou. Às vezes com razão
 * (dois módulos não criam porta nenhuma no menu do CRM), às vezes sem — a porta
 * existia, dentro de Configurações, onde ninguém procurou.
 *
 * O caminho é LIDO DO MENU (`ondeOModuloAparece`), nunca escrito aqui: uma
 * segunda lista de endereços divergiria na primeira tela que mudasse de grupo, e
 * a tela passaria a mandar o operador para o lugar errado com ar de certeza.
 */
function OndeAparece({
  modulo,
  ligado,
  foraDoMenu,
  painelEm,
}: {
  modulo: ModuloPorFlag;
  ligado: boolean;
  /** JÁ TRADUZIDO por quem chama — ver o comentário no sítio da chamada. */
  foraDoMenu?: string;
  /** `href` da tela que hospeda o painel; o caminho até ela é derivado aqui. */
  painelEm?: string;
}) {
  const t = useT();
  const portas = ondeOModuloAparece(modulo);

  // Sem porta E sem texto declarado é o único estado que a tela não sabe narrar;
  // aí ela cala, em vez de inventar um caminho e mandar o operador procurar no
  // lugar errado. O invariante de `tests/unit/porta-do-modulo-ligado.test.tsx`
  // reprova esse estado na raiz, para nenhum módulo novo chegar assim.
  if (portas.length === 0) {
    if (!foraDoMenu) return null;
    const painel = painelEm ? caminhoDaPorta(painelEm) : null;
    return (
      <p className="text-sm text-muted-foreground wrap-anywhere">
        {foraDoMenu}
        {painel ? (
          <>
            {" "}
            <span className="font-medium text-foreground">
              {t(painel.grupo)} › {painel.hub ? <>{t(painel.hub)} › </> : null}
              {t(painel.label)}
            </span>
            .
          </>
        ) : null}
      </p>
    );
  }

  return (
    <p className="text-sm text-muted-foreground wrap-anywhere">
      {t(ligado ? "Aparece no menu em:" : "Ao ligar, aparece no menu em:")}{" "}
      {portas.map((porta, i) => (
        <span key={porta.href}>
          {i > 0 && ", "}
          <span className="font-medium text-foreground">
            {/*
              O PASSO DO HUB aparece quando a porta não sobe ao menu lateral — e ele é o conserto
              do relato "liguei e não aparece no CRM": sem ele o texto dizia "CRM › Empresas" e
              mandava procurar no menu diário, onde a porta não está.

              Cada passo passa por `t()` no SÍTIO, sobre o campo do parâmetro de iteração: é a
              forma que a catraca do espanhol resolve. Um array de passos com `t(passo)` seria
              reprovado como "parâmetro livre".
            */}
            {t(porta.grupo)} › {porta.hub ? <>{t(porta.hub)} › </> : null}
            {t(porta.label)}
          </span>
        </span>
      ))}
    </p>
  );
}

export function FormularioDeModulos({
  ligados,
  escondidos,
  suspensasPorCobranca,
}: {
  ligados: readonly ModuloOpcional[];
  /** Módulos que ainda não se ligam nesta versão e estão desligados: ficam sem interruptor. */
  escondidos: readonly ModuloOpcional[];
  /** Spec da cobrança §7(h): quantas o desligar libera. `null` = a contagem falhou. */
  suspensasPorCobranca: number | null;
}) {
  const t = useT();
  const [estado, setEstado] = useState<ReadonlySet<ModuloOpcional>>(new Set(ligados));
  const [erro, setErro] = useState<string | null>(null);
  const [pendente, startTransition] = useTransition();

  function trocar(modulo: ModuloPorFlag, valor: boolean) {
    setErro(null);
    const alternar = (ligar: boolean) =>
      setEstado((atual) => {
        const proximo = new Set(atual);
        if (ligar) proximo.add(modulo);
        else proximo.delete(modulo);
        return proximo;
      });
    alternar(valor);
    startTransition(async () => {
      const r = await updateModuloDaInstalacao({ modulo, ligado: valor });
      if (!r.ok) {
        alternar(!valor);
        setErro(
          t(
            ehRecusaDeEscrita(r.error)
              ? MENSAGEM_DA_RECUSA_DE_ESCRITA[r.error]
              : r.error === "liberacao_falhou"
                ? "A cobrança continua ligada: não deu para liberar as empresas suspensas por falta de pagamento, e nada foi mudado. Tente desligar de novo em instantes."
                : "Não deu para salvar. Tente de novo em instantes.",
          ),
        );
      }
    });
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle>{t("Módulos opcionais")}</CardTitle>
        <CardDescription>
          {t(
            "Recursos que a maioria das instalações não usa. Desligados, eles não aparecem para nenhuma empresa daqui.",
          )}
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">
        {MODULOS_NA_TELA.filter((m) => !escondidos.includes(m.modulo)).map((m) => (
          <div key={m.modulo} className="flex items-start justify-between gap-4 rounded-lg border p-4">
            {/* `min-w-0`: sem ele a coluna de texto não encolhe abaixo da palavra
                mais longa (um caminho de API não quebra) e empurra o interruptor
                para fora do cartão no celular. */}
            <div className="min-w-0 space-y-1">
              <Label htmlFor={m.id} className="text-base">
                {t(m.rotulo)}
              </Label>
              <p className="text-sm text-muted-foreground">{t(m.descricao)}</p>
              <OndeAparece
                modulo={m.modulo}
                ligado={estado.has(m.modulo)}
                /* Traduzido AQUI, e não dentro de `OndeAparece`: a catraca do
                   espanhol recusa `t()` sobre parâmetro de função (seria dado de
                   runtime, que não está em dicionário nenhum) e aceita
                   `t(m.campo)` sobre a linha do módulo — a mesma forma de
                   `t(m.descricao)`, logo acima. O componente recebe texto pronto. */
                foraDoMenu={m.foraDoMenu ? t(m.foraDoMenu) : undefined}
                painelEm={m.painelEm}
              />
              {m.modulo === "cobranca" && estado.has("cobranca") && suspensasPorCobranca !== 0 && (
                <p className="text-sm text-warning-fg">
                  {suspensasPorCobranca === null
                    ? t("Não deu para contar as empresas suspensas por falta de pagamento. Desligar libera todas.")
                    : `${t("Empresas suspensas por falta de pagamento que serão liberadas ao desligar:")} ${suspensasPorCobranca}`}
                </p>
              )}
            </div>
            <Switch
              id={m.id}
              checked={estado.has(m.modulo)}
              onCheckedChange={(valor) => trocar(m.modulo, valor)}
              disabled={pendente}
              aria-label={t(m.rotulo)}
            />
          </div>
        ))}

        {erro && (
          <p className="text-sm text-destructive" role="alert">
            {erro}
          </p>
        )}
      </CardContent>
    </Card>
  );
}
