import { requireSupportWrite } from "@/lib/impersonate/support";
/**
 * TIPOS DE AGENDAMENTO — criar, alterar e desativar pela API.
 *
 * ─── O buraco que esta rota fecha ────────────────────────────────────────
 *
 * A tabela `calendar_event_types` tem dez categorias no CHECK
 * (`consulta`, `procedimento`, `retorno`, `visita`, `vistoria`, `reuniao`,
 * `call`, `orcamento`, `demonstracao`, `outro`), duração, buffers, antecedência
 * mínima, janela de agendamento e local — e **não havia como criar ou editar um
 * tipo por lugar nenhum**: nem rota, nem tela. Uma organização recebia três
 * tipos semeados e ficava com eles para sempre.
 *
 * ─── Desativar, nunca apagar ─────────────────────────────────────────────
 *
 * `calendar_appointments.event_type_id` aponta para cá. Apagar o tipo levaria
 * junto a história — que consulta foi feita, de que tipo, quanto durava. O
 * DELETE aqui grava `is_active = false`: some da tela de marcar e continua
 * respondendo pelo passado. É o mesmo raciocínio do anti-pattern 7 da doutrina
 * (cascade fantasma).
 *
 * ─── E a volta mora AO LADO, não aqui ────────────────────────────────────
 *
 * Reativar é `POST /api/v1/agenda/tipos/reativar`. `is_active` está fora de
 * `camposDoTipo` DE PROPÓSITO: aceitá-lo no PATCH deixaria o mesmo pedido que
 * muda a duração poder desligar o tipo, e a trilha registraria a religada como
 * `agenda.tipo_alterado { campos: ["is_active"] }` — indistinguível de uma
 * alteração de campo qualquer.
 *
 * ⚠️ Essa exclusão é silenciosa e já custou: Zod DESCARTA chave desconhecida sem
 * dizer nada, então o botão "Reativar" da tela mandou `is_active` para cá
 * durante toda a vida dele e recebeu 422 "Nenhum campo para alterar." — uma
 * recusa que não nomeia o que foi descartado. Quem vigia a travessia hoje é
 * `tests/unit/agenda-reativar-tipo.test.ts`.
 *
 * Auth: sessão de navegador OU Bearer `dsk_...` (api_tokens) via
 * `lib/api/auth-dual.ts` — a mesma dualidade das demais rotas de configuração
 * que aceitam token. No ramo do token, a org sai da linha do token.
 */
import { randomUUID } from "node:crypto";
import { type NextRequest } from "next/server";
import { z } from "zod";

import { resolveAuthDual, tetoDeEscritaDoToken } from "@/lib/api/auth-dual";
import { fail, ok } from "@/lib/api/wrappers";
import { audit } from "@/lib/audit";
import { createAdminClient } from "@/lib/supabase/admin";
import { listaTiposDeAtendimento } from "@/lib/agenda/consulta";
import { TETO_DE_LEMBRETES_EXTRAS } from "@/lib/agenda/lembretes";
import { variaveisQueOLembretePreenche } from "@/lib/agenda/lembrete-por-modelo";
import { transportaMensagem } from "@/lib/channels/capabilities";
import { traduzir } from "@/lib/i18n/dicionario";

export const dynamic = "force-dynamic";

/** As dez do CHECK da tabela. Fora daqui o Postgres recusa — melhor recusar antes. */
const CATEGORIAS = [
  "consulta",
  "procedimento",
  "retorno",
  "visita",
  "vistoria",
  "reuniao",
  "call",
  "orcamento",
  "demonstracao",
  "outro",
] as const;

const LOCAIS = ["in_person", "phone", "whatsapp", "video_link", "google_meet"] as const;

/**
 * Os limites são os MESMOS do CHECK do banco, e isso é deliberado.
 *
 * Zod aqui não substitui a constraint: ela é a verdade e continua valendo para
 * quem escrever por SQL. O que a validação faz é transformar um 500 de constraint
 * — que aparece como "erro interno" para quem está usando — numa recusa 422 que
 * diz o que está fora.
 */
const camposDoTipo = {
  name: z.string().trim().min(2).max(80),
  category: z.enum(CATEGORIAS),
  duration_minutes: z.number().int().min(5).max(1440),
  location_kind: z.enum(LOCAIS),
  description: z.string().trim().max(500).nullish(),
  location_details: z.string().trim().max(300).nullish(),
  default_owner_user_id: z.string().uuid().nullish(),
  requires_confirmation: z.boolean().optional(),
  buffer_before_minutes: z.number().int().min(0).max(720).optional(),
  buffer_after_minutes: z.number().int().min(0).max(720).optional(),
  minimum_notice_minutes: z.number().int().min(0).max(43_200).optional(),
  booking_window_days: z.number().int().min(1).max(365).optional(),
  /**
   * O LEMBRETE — os dois campos que o cron `agenda-reminder` lê e que ninguém
   * conseguia escrever.
   *
   * A 0177 criou as colunas, a 0194 as pôs em `default false` deixando escrito
   * que ligar por padrão "fica com o dono do produto NO DIA em que o disparador
   * nascer", e o `99c33257` fez o disparador nascer. Faltava a outra metade do
   * par: `reminder_enabled` não estava em schema nenhum aqui nem na tela, então
   * a rodada do cron devolvia zero linhas em TODA instalação — capacidade que
   * existe e não tem como ser usada (invariante 6 do Sistema Vivo).
   *
   * **Continua nascendo desligado.** Não há `.default(true)`: quem não manda o
   * campo não liga nada, e o default da coluna segue sendo `false`. Mandar
   * mensagem para o telefone de um cliente é irreversível.
   */
  reminder_enabled: z.boolean().optional(),
  /**
   * O BLOCO DO NEGÓCIO na descrição do evento do Google (#4006, doc 272 = B).
   *
   * Nasce desligado (default da coluna, migration 9209) e não tem `.default()`
   * aqui: no Google todo convidado lê a descrição, inclusive o cliente com
   * e-mail na ficha, e atualizar o sistema não pode mandar o nome, o telefone e
   * a etapa do funil para fora sem alguém decidir.
   */
  levar_dados_do_negocio: z.boolean().optional(),
  reminder_channel_session_id: z.string().uuid().nullish(),
  /**
   * O modelo aprovado que sai quando a janela de 24 h do cliente fechou
   * (`lib/agenda/lembrete-por-modelo.ts`). Nulo = como antes: fora da janela o
   * lembrete não sai e a equipe recebe o aviso. Conferido em
   * `recusaDoModeloDoLembrete` antes de gravar.
   */
  reminder_meta_template_id: z.string().uuid().nullish(),
  /**
   * ⚠️ ESTA FAIXA É MAIS ESTREITA QUE O CHECK DO BANCO, E ISSO CONTRARIA O
   * PARÁGRAFO ACIMA DE PROPÓSITO.
   *
   * A coluna aceita `between 0 and 43200`, e os campos vizinhos copiam o CHECK
   * porque lá a borda do banco É a borda do sentido: `buffer_after_minutes = 0`
   * é "sem folga", uma configuração legítima. Aqui as duas bordas do CHECK
   * produzem lembrete que não lembra:
   *
   * - **0 min** nunca sai. `estaNaHora` recusa `comeca <= agora`, então um
   *   lembrete marcado para o próprio instante do compromisso é descartado em
   *   toda rodada até a linha sair da varredura. O piso é 15 min porque o cron
   *   roda a cada 5: abaixo de três ciclos, uma rodada atrasada come a
   *   antecedência inteira e o aviso chega depois de a pessoa já ter saído.
   * - **43200 min (30 dias)** não é lembrete, é convite. O teto é 10080 (7
   *   dias), que cobre o "semana que vem" de clínica e imobiliária.
   *
   * A borda continua sendo do banco para quem escreve por SQL — aqui a recusa é
   * só antes, com nome. Uma linha semeada fora desta faixa (só por SQL direto;
   * o default da 0177 é 1440) segue valendo no banco e o cron a respeita: o que
   * ela perde é poder ser reenviada por esta rota sem entrar na faixa.
   */
  /**
   * O PREÇO PADRÃO do serviço, em centavos.
   *
   * Opcional e sem default: nem todo negócio tem preço fixo, e obrigar um número
   * faria quem cobra por hora inventar um. Vazio significa "digite na hora".
   *
   * É semente do item da comanda, nunca o preço dele — o item congela o seu.
   */
  default_price_cents: z.number().int().min(0).max(100_000_000).nullish(),
  reminder_minutes_before: z
    .number()
    .int()
    .min(15, { message: "O lembrete precisa sair pelo menos 15 minutos antes do compromisso." })
    .max(10_080, { message: "O lembrete não pode sair mais de 7 dias (10080 minutos) antes." })
    .optional(),
  /**
   * Os degraus ADICIONAIS — o "e de novo três horas antes" que faltava.
   *
   * `reminder_minutes_before` continua sendo o degrau principal; estes somam a
   * ele. Vazio é o comportamento anterior, um lembrete só, e por isso o campo
   * não tem `.default()`: quem não manda não ganha aviso nenhum a mais.
   *
   * O teto é o mesmo do CHECK (`fn_degraus_de_lembrete_validos`): guarda contra
   * laço de formulário, não contra a operação. Quem decide quantos avisos o
   * cliente recebe é quem edita o tipo.
   */
  reminder_extra_offsets_minutes: z
    .array(
      z
        .number()
        .int()
        .min(15, { message: "O lembrete precisa sair pelo menos 15 minutos antes do compromisso." })
        .max(10_080, { message: "O lembrete não pode sair mais de 7 dias (10080 minutos) antes." }),
    )
    .max(TETO_DE_LEMBRETES_EXTRAS, {
      message: `No máximo ${TETO_DE_LEMBRETES_EXTRAS} lembretes adicionais por tipo.`,
    })
    // Duplicata não é erro de quem preenche, é ruído: dois degraus iguais
    // produziriam o mesmo aviso duas vezes se algum dia alguém lesse a lista
    // sem deduplicar. Some aqui, uma vez, em vez de virar guarda em cada leitor.
    .transform((v) => [...new Set(v)].sort((a, b) => b - a))
    .optional(),
};

const criarSchema = z.object(camposDoTipo);
// Somente a criação pode nascer como serviço em rascunho. O PATCH legado não
// ganha uma porta de ativação: a programação é validada pelo núcleo de serviços.
const criarComRascunhoSchema = criarSchema.extend({ service_draft: z.boolean().optional() });
// `.partial()` em vez de repetir os doze campos como opcionais: repetir criaria
// duas listas para manter em sincronia, e a segunda envelhece calada.
const alterarSchema = criarSchema.partial().extend({
  id: z.string().uuid(),
  /**
   * O TEXTO que o cron manda. Vazio/nulo = a frase padrão. Distinto de
   * `reminder_template_name` (nome do template no provedor oficial).
   *
   * Mora só no PATCH de propósito: o tipo nasce com a frase de fábrica, e
   * quem quer outra escreve depois. No POST, o campo nem entra — senão um
   * `""` no nascimento gravaria nulo por cima do default, e a ausência no
   * formulário de criação deixaria de ser ausência.
   *
   * Transforma string em branco em `null` para o PATCH poder VOLTAR ao padrão
   * sem um campo-sentinela: quem apaga o textarea está pedindo o texto de
   * fábrica, não uma mensagem vazia no WhatsApp.
   */
  reminder_body: z
    .string()
    .max(1000, { message: "A mensagem do lembrete cabe em 1000 caracteres." })
    .nullish()
    .transform((v) => (v == null ? v : v.trim() === "" ? null : v.trim())),
  /**
   * Texto de cada extra. Chave = minutos antes. String em branco some do mapa
   * (cai na frase de fábrica). Mora só no PATCH pelo mesmo motivo de
   * `reminder_body`: o tipo nasce sem texto próprio.
   */
  reminder_bodies: z
    .record(
      z.string().regex(/^\d+$/),
      z.string().max(1000, { message: "A mensagem do lembrete cabe em 1000 caracteres." }),
    )
    .optional()
    .transform((v) => {
      if (!v) return v;
      const out: Record<string, string> = {};
      for (const [k, corpo] of Object.entries(v)) {
        const t = corpo.trim();
        if (t) out[k] = t;
      }
      return out;
    }),
});
const desativarSchema = z.object({ id: z.string().uuid() });

/**
 * O MODELO DO LEMBRETE FORA DA JANELA — conferido antes de gravar.
 *
 * Três recusas, cada uma com o que fazer, em vez do erro cru do banco:
 *   - de outra organização (a FK composta da 9092 também recusaria, mas como 500);
 *   - com variável que o lembrete não tem de onde preencher — o cron nunca o
 *     mandaria, e a escolha ficaria gravada sem efeito;
 *   - de OUTRO número que o escolhido no mesmo pedido: o modelo só sai pelo número
 *     dono dele, e o lembrete nunca troca de número sozinho.
 *
 * O status (aprovado, pausado) NÃO é recusa aqui: muda depois da escolha, e quem
 * confere na hora de mandar é o cron (`lerModeloDoLembrete`).
 */
async function recusaDoModeloDoLembrete(
  admin: ReturnType<typeof createAdminClient>,
  organizationId: string,
  campos: { reminder_meta_template_id?: string | null; reminder_channel_session_id?: string | null },
  t: (texto: string) => string,
  requestId: string,
): Promise<Response | null> {
  if (!campos.reminder_meta_template_id) return null;
  const { data: modelo, error } = await admin
    .from("meta_templates")
    .select("id, name, language, channel_session_id, parameter_format, components")
    .eq("organization_id", organizationId)
    .eq("id", campos.reminder_meta_template_id)
    .maybeSingle();
  if (error)
    return fail("internal_error", t("Não foi possível consultar o modelo do lembrete."), 500, {
      requestId,
    });
  if (!modelo)
    return fail("validation_failed", t("Escolha um modelo aprovado desta organização."), 422, {
      requestId,
    });
  if (variaveisQueOLembretePreenche(modelo) === null)
    return fail(
      "validation_failed",
      t(
        "Este modelo não serve para o lembrete: ele só pode ter variáveis de texto no corpo, de {{1}} a {{4}}, sem pular número.",
      ),
      422,
      { requestId },
    );
  if (
    campos.reminder_channel_session_id &&
    modelo.channel_session_id !== campos.reminder_channel_session_id
  )
    return fail(
      "validation_failed",
      t("O modelo escolhido é de outro número. Escolha um modelo do número que envia os lembretes."),
      422,
      { requestId },
    );
  return null;
}

/**
 * O slug sai do NOME, e é estável depois de criado.
 *
 * A ferramenta MCP aceita `event_type_slug`, então o slug é endereço público: se
 * ele mudasse ao renomear o tipo, todo playbook e toda automação que o citam
 * parariam de achar — em silêncio, porque a busca por slug devolve "não existe"
 * e não "mudou de nome". Por isso o PATCH nunca o toca.
 */
function slugDe(nome: string): string {
  return (
    nome
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "")
      .slice(0, 60) || "tipo"
  );
}

export async function GET(req: NextRequest): Promise<Response> {
  const requestId = req.headers.get("x-request-id") ?? randomUUID();
  const authz = await resolveAuthDual(req, {
    requestId,
    resource: "calendar_event_types",
    role: "viewer",
    scope: "mcp:read",
  });
  if (!authz.ok) return authz.response;

  // A MESMA coleta que a ferramenta MCP usa. Esta query era inline aqui, e havia
  // outras três iguais no repo — a tela e a IA respondendo por recortes
  // diferentes sobre o que a organização atende. Ver `listaTiposDeAtendimento`.
  //
  // `incluirInativos: true` porque quem chama esta rota administra o cadastro:
  // esconder o tipo desativado tiraria dele a única porta para reativá-lo.
  const r = await listaTiposDeAtendimento(createAdminClient(), authz.organizationId, {
    incluirInativos: true,
  });
  if (!r.ok) return fail("internal_error", r.motivoParaOperador, 500, { requestId });
  // O wire desta rota é snake_case e a tela já o consome assim; o coletor fala a
  // língua do domínio. A tradução é aqui, na borda, e não no coletor — que
  // também serve a IA, cujo vocabulário é outro.
  return ok(
    r.tipos.map((t) => ({
      id: t.id,
      name: t.nome,
      slug: t.slug,
      description: t.descricao,
      category: t.categoria,
      duration_minutes: t.duracaoMin,
      location_kind: t.localKind,
      location_details: t.localDetalhes,
      default_owner_user_id: t.donoPadraoId,
      requires_confirmation: t.precisaConfirmacao,
      is_active: t.ativo,
      buffer_before_minutes: t.bufferAntesMin,
      buffer_after_minutes: t.bufferDepoisMin,
      minimum_notice_minutes: t.antecedenciaMinimaMin,
      booking_window_days: t.janelaDeAgendamentoDias,
      // Sem estes dois, quem chama a rota não tem como SABER se o lembrete está
      // ligado — só como pedir que ligue. Um PATCH cego sobre um estado que a
      // leitura não conta é o mesmo controle decorativo, do outro lado.
      reminder_enabled: t.lembreteLigado,
      levar_dados_do_negocio: t.levarDadosDoNegocio,
      reminder_channel_session_id: t.lembreteCanalId ?? null,
      reminder_meta_template_id: t.lembreteModeloId ?? null,
      reminder_minutes_before: t.lembreteAntecedenciaMin,
      reminder_extra_offsets_minutes: t.lembreteDegrausExtras,
      reminder_body: t.lembreteMensagem,
      reminder_bodies: t.lembreteMensagens,
      default_price_cents: t.precoPadraoCents,
      ...(t.servico ? { service_enabled: true, booking_mode: t.servico.booking_mode, service_capacity: t.servico.capacity,
        service_revision: t.servico.revision, service_publication_mode: t.servico.publication_mode } : {}),
    })),
    { requestId },
  );
}

export async function POST(req: NextRequest): Promise<Response> {
  const supportDenied = await requireSupportWrite();
  if (supportDenied) return supportDenied;

  const requestId = req.headers.get("x-request-id") ?? randomUUID();
  const authz = await resolveAuthDual(req, {
    requestId,
    resource: "calendar_event_types",
    role: "manager",
    scope: "mcp:write",
  });
  if (!authz.ok) return authz.response;
  const t = (texto: string) => traduzir(texto, authz.idioma ?? "pt-BR");

  const teto = await tetoDeEscritaDoToken(authz, "agenda_tipos", requestId ?? "");
  if (teto) return teto;

  const lido = criarComRascunhoSchema.safeParse(await req.json().catch(() => ({})));
  if (!lido.success) {
    // A mensagem do Zod passa pelo dicionário, e não direto ao corpo da resposta:
    // as recusas de `reminder_minutes_before` são escritas em português nesta
    // rota, e quem opera em espanhol as receberia cruas. Texto sem entrada
    // degrada para ele mesmo — que é o contrato de `traduzir`.
    return fail("validation_failed", t(lido.error.issues[0]?.message ?? "corpo inválido"), 422, {
      requestId,
    });
  }

  const admin = createAdminClient();
  if (lido.data.reminder_channel_session_id) {
    const { data: canal, error: erroCanal } = await admin
      .from("channel_sessions")
      .select("id, provider, archived_at")
      .eq("organization_id", authz.organizationId)
      .eq("id", lido.data.reminder_channel_session_id)
      .maybeSingle();
    if (erroCanal)
      return fail("internal_error", t("Não foi possível consultar o canal do lembrete."), 500, {
        requestId,
      });
    if (!canal || canal.archived_at || !transportaMensagem(canal.provider)) {
      return fail(
        "validation_failed",
        t("Escolha um canal de mensagem não arquivado desta organização."),
        422,
        { requestId },
      );
    }
  }
  const recusaDoModelo = await recusaDoModeloDoLembrete(admin, authz.organizationId, lido.data, t, requestId);
  if (recusaDoModelo) return recusaDoModelo;
  const { service_draft, ...fields } = lido.data;
  const { data, error } = await admin
    .from("calendar_event_types")
    .insert({ ...fields, ...(service_draft ? { service_enabled: true, is_active: false, default_owner_user_id: null } : {}), organization_id: authz.organizationId, slug: slugDe(lido.data.name) })
    .select("id, slug")
    .single();

  if (error) {
    // 23505 é o slug repetido — recusa esperada, não erro de sistema.
    if (error.code === "23505") {
      return fail("conflict", `Já existe um tipo com o nome "${lido.data.name}".`, 409, {
        requestId,
      });
    }
    return fail("internal_error", error.message, 500, { requestId });
  }

  await audit({
    actorUserId: authz.actor.type === "user" ? authz.actor.id : null,
    actorApiTokenId: authz.apiTokenId ?? null,
    action: "agenda.tipo_criado",
    organizationId: authz.organizationId,
    resourceType: "calendar_event_types",
    resourceId: data.id,
    metadata: {
      nome: lido.data.name,
      categoria: lido.data.category,
      duracao: lido.data.duration_minutes,
    },
  });
  return ok(data, { requestId, status: 201 });
}

export async function PATCH(req: NextRequest): Promise<Response> {
  const supportDenied = await requireSupportWrite();
  if (supportDenied) return supportDenied;

  const requestId = req.headers.get("x-request-id") ?? randomUUID();
  const authz = await resolveAuthDual(req, {
    requestId,
    resource: "calendar_event_types",
    role: "manager",
    scope: "mcp:write",
  });
  if (!authz.ok) return authz.response;
  const t = (texto: string) => traduzir(texto, authz.idioma ?? "pt-BR");

  const teto = await tetoDeEscritaDoToken(authz, "agenda_tipos", requestId ?? "");
  if (teto) return teto;

  const lido = alterarSchema.safeParse(await req.json().catch(() => ({})));
  if (!lido.success) {
    // Idem ao POST: o dicionário na borda, para a recusa do lembrete chegar
    // legível a quem opera em espanhol.
    return fail("validation_failed", t(lido.error.issues[0]?.message ?? "corpo inválido"), 422, {
      requestId,
    });
  }
  const { id, ...bruto } = lido.data;
  const campos = Object.fromEntries(Object.entries(bruto).filter(([, v]) => v !== undefined));
  if (Object.keys(campos).length === 0) {
    // Recusa em vez de UPDATE vazio: "alterei" sobre nada é a mesma família de
    // mentira que o "Marcado ✓" sem linha no banco.
    return fail("validation_failed", t("Nenhum campo para alterar."), 422, { requestId });
  }

  const admin = createAdminClient();
  if (lido.data.reminder_channel_session_id) {
    const { data: canal, error: erroCanal } = await admin
      .from("channel_sessions")
      .select("id, provider, archived_at")
      .eq("organization_id", authz.organizationId)
      .eq("id", lido.data.reminder_channel_session_id)
      .maybeSingle();
    if (erroCanal)
      return fail("internal_error", t("Não foi possível consultar o canal do lembrete."), 500, {
        requestId,
      });
    if (!canal || canal.archived_at || !transportaMensagem(canal.provider)) {
      return fail(
        "validation_failed",
        t("Escolha um canal de mensagem não arquivado desta organização."),
        422,
        { requestId },
      );
    }
  }
  const recusaDoModelo = await recusaDoModeloDoLembrete(admin, authz.organizationId, lido.data, t, requestId);
  if (recusaDoModelo) return recusaDoModelo;
  const { data, error } = await admin
    .from("calendar_event_types")
    .update(campos)
    .eq("id", id)
    .eq("organization_id", authz.organizationId)
    .select("id")
    .maybeSingle();

  if (error) return fail("internal_error", error.message, 500, { requestId });
  if (!data) return fail("not_found", t("Tipo de agendamento não encontrado."), 404, { requestId });

  await audit({
    actorUserId: authz.actor.type === "user" ? authz.actor.id : null,
    actorApiTokenId: authz.apiTokenId ?? null,
    action: "agenda.tipo_alterado",
    organizationId: authz.organizationId,
    resourceType: "calendar_event_types",
    resourceId: id,
    metadata: { campos: Object.keys(campos) },
  });
  return ok(data, { requestId });
}

export async function DELETE(req: NextRequest): Promise<Response> {
  const supportDenied = await requireSupportWrite();
  if (supportDenied) return supportDenied;

  const requestId = req.headers.get("x-request-id") ?? randomUUID();
  const authz = await resolveAuthDual(req, {
    requestId,
    resource: "calendar_event_types",
    role: "manager",
    scope: "mcp:write",
  });
  if (!authz.ok) return authz.response;
  const t = (texto: string) => traduzir(texto, authz.idioma ?? "pt-BR");

  const teto = await tetoDeEscritaDoToken(authz, "agenda_tipos", requestId ?? "");
  if (teto) return teto;

  const lido = desativarSchema.safeParse(await req.json().catch(() => ({})));
  if (!lido.success) return fail("validation_failed", t("corpo inválido"), 422, { requestId });

  const admin = createAdminClient();
  const { data, error } = await admin
    .from("calendar_event_types")
    .update({ is_active: false })
    .eq("id", lido.data.id)
    .eq("organization_id", authz.organizationId)
    .select("id")
    .maybeSingle();

  if (error) return fail("internal_error", error.message, 500, { requestId });
  if (!data) return fail("not_found", t("Tipo de agendamento não encontrado."), 404, { requestId });

  await audit({
    actorUserId: authz.actor.type === "user" ? authz.actor.id : null,
    actorApiTokenId: authz.apiTokenId ?? null,
    action: "agenda.tipo_desativado",
    organizationId: authz.organizationId,
    resourceType: "calendar_event_types",
    resourceId: lido.data.id,
    metadata: {},
  });
  return ok(data, { requestId });
}
