/**
 * O que a API aceita de fora. Zod em todo input externo, como manda a doutrina —
 * e nada aqui aceita `organization_id`: a organização vem do `requireRole()`,
 * nunca do corpo.
 */
import { z } from "zod";

import { filtroDeAudienciaSchema } from "./audiencia";
import { LIMITE_DO_ASSUNTO_DO_EMAIL, LIMITE_DO_CORPO_DO_EMAIL } from "./conteudo-de-email";
import { variaveisDaCampanhaSchema } from "./modelo";
import { MODOS_DA_MENSAGEM, VELOCIDADES_DA_CAMPANHA } from "./tipos";

/** O teto do corpo de TEXTO no WhatsApp — o que já era, e o que o envio comporta. */
export const LIMITE_DO_TEXTO_DO_WHATSAPP = 4096;

/**
 * Os canais de CAMPANHA (#3441). `whatsapp` é o que sempre existiu — um
 * `channel_sessions` na coluna `channel_session_id`; `email` não tem número
 * nenhum e a conta de envio é a Resend da organização.
 */
export const CANAIS_DA_CAMPANHA = ["whatsapp", "email"] as const;
export type CanalDaCampanha = (typeof CANAIS_DA_CAMPANHA)[number];

/**
 * O ritmo próprio. Todos opcionais e anuláveis: `null` devolve a decisão ao
 * canal, que é o default de quem nunca abriu esta seção.
 *
 * As faixas são as do CHECK da migration 0375 — validação de entrada do
 * operador, não default de comportamento (os números de pacing são fonte única
 * em `lib/agent-engine/pacing/defaults.ts`).
 */
export const ritmoSchema = z.object({
  intervalo_segundos: z.number().int().min(1).max(86_400).nullable().optional(),
  janela_inicio_hora: z.number().int().min(0).max(23).nullable().optional(),
  janela_fim_hora: z.number().int().min(1).max(24).nullable().optional(),
  teto_diario: z.number().int().min(1).max(10_000).nullable().optional(),
  teto_horario: z.number().int().min(1).max(10_000).nullable().optional(),
  /**
   * Migration 0711. Ausente = a que já está (`gradual` numa campanha nova). A
   * rota confere contra o número: rápida só com modelo aprovado.
   */
  velocidade: z.enum(VELOCIDADES_DA_CAMPANHA).optional(),
  /**
   * Migration 0765. Texto em balões, um por parágrafo; ausente = o que já está
   * (`false` numa campanha nova). Modelo aprovado nunca se divide.
   */
  split_messages: z.boolean().optional(),
});

const baseDaCampanha = {
  name: z.string().trim().min(1).max(160),
  description: z.string().trim().max(4000).nullable().optional(),
  /**
   * O canal da campanha. Ausente = `whatsapp`, que é toda campanha anterior ao
   * e-mail. Em `email` não há número: `channel_session_id` fica de fora.
   */
  canal: z.enum(CANAIS_DA_CAMPANHA).optional(),
  /** Obrigatório no WhatsApp; proibido no e-mail (lá não há número). */
  channel_session_id: z.string().uuid().optional(),
  /**
   * O corpo é UM só (`message_body`), com o teto do MAIOR canal: o e-mail aceita
   * 20.000. O teto do WhatsApp (4.096) continua cobrado, e por canal — ver
   * `corpoDoWhatsappCabe`.
   */
  message_body: z.string().trim().max(LIMITE_DO_CORPO_DO_EMAIL).nullable().optional(),
  /**
   * Migration 9334 (#3441). Assunto do e-mail; ausente nos demais canais. O
   * limite aqui é o mesmo da conferência (`conferirConteudoDeEmail`), para o
   * erro de tamanho chegar antes do banco.
   */
  email_subject: z.string().trim().max(LIMITE_DO_ASSUNTO_DO_EMAIL).nullable().optional(),
  /**
   * Migration 0710 (#1330, fatia 4). Ausente = `text`, que é o que toda campanha
   * anterior é. A rota confere contra o número: o modo é o que o canal aceita
   * (`modoDeCampanhaDoCanal`), nunca uma escolha solta do corpo.
   */
  message_mode: z.enum(MODOS_DA_MENSAGEM).optional(),
  provider_template_name: z.string().trim().min(1).max(512).nullable().optional(),
  provider_template_language: z.string().trim().min(2).max(15).nullable().optional(),
  provider_template_variables: variaveisDaCampanhaSchema.optional(),
  base_legal: z.enum(["consent", "legitimate_interest"]),
  lia_ref: z.string().trim().max(120).nullable().optional(),
  audience_filter: filtroDeAudienciaSchema.optional(),
  /**
   * Os números EXTRAS do rodízio (migration 0377). O principal continua em
   * `channel_session_id`; estes entram junto. Vazio = campanha de um número só,
   * que é como toda campanha existente se comporta.
   */
  channel_session_ids: z.array(z.string().uuid()).max(10).optional(),
  /**
   * Onde o card de quem responde nasce, e quem atende (migration 0378).
   * `null` devolve a decisão ao número, que é o comportamento de sempre.
   */
  pipeline_id: z.string().uuid().nullable().optional(),
  stage_id: z.string().uuid().nullable().optional(),
  agent_id: z.string().uuid().nullable().optional(),
};

/**
 * O canal pedido é e-mail? `message_mode: "email"` também responde sim: a tela
 * pode mandar o modo sem repetir o canal.
 */
function ehCampanhaDeEmail(c: { canal?: CanalDaCampanha; message_mode?: string }): boolean {
  return c.canal === "email" || c.message_mode === "email";
}

/** O corpo do WhatsApp não cabe acima de 4.096 — o teto é do envio, não do schema. */
function corpoDoWhatsappCabe(c: {
  canal?: CanalDaCampanha;
  message_mode?: string;
  message_body?: string | null;
}): boolean {
  if (ehCampanhaDeEmail(c)) return true;
  return (c.message_body ?? "").length <= LIMITE_DO_TEXTO_DO_WHATSAPP;
}

export const criarCampanhaSchema = z
  .object({ ...baseDaCampanha })
  .merge(ritmoSchema)
  .refine((c) => ehCampanhaDeEmail(c) || c.channel_session_id !== undefined, {
    message: "Escolha o número que envia a campanha de WhatsApp.",
    path: ["channel_session_id"],
  })
  .refine((c) => !ehCampanhaDeEmail(c) || c.channel_session_id === undefined, {
    message: "Campanha de e-mail não usa número: a conta de envio é a Resend da organização.",
    path: ["channel_session_id"],
  })
  .refine((c) => !ehCampanhaDeEmail(c) || (c.channel_session_ids ?? []).length === 0, {
    message: "Campanha de e-mail não usa rodízio de números.",
    path: ["channel_session_ids"],
  })
  .refine(corpoDoWhatsappCabe, {
    message: `O texto do WhatsApp passa de ${LIMITE_DO_TEXTO_DO_WHATSAPP} caracteres.`,
    path: ["message_body"],
  })
  .refine(
    (c) => c.base_legal !== "legitimate_interest" || (c.lia_ref ?? "").trim() !== "",
    {
      message:
        "Interesse legítimo exige a referência da avaliação (LIA) — é ela que responde a quem " +
        "perguntar com base em quê recebeu a mensagem.",
      path: ["lia_ref"],
    },
  )
  .refine((c) => c.stage_id == null || c.pipeline_id != null, {
    message: "Escolha o funil antes da etapa — etapa sem funil seria um card sem coluna.",
    path: ["stage_id"],
  })
  .refine(
    (c) =>
      c.janela_inicio_hora == null ||
      c.janela_fim_hora == null ||
      c.janela_fim_hora > c.janela_inicio_hora,
    { message: "A janela precisa terminar depois de começar.", path: ["janela_fim_hora"] },
  );

/** Edição de rascunho: tudo opcional, e a mesma checagem de base legal na rota. */
export const editarCampanhaSchema = z
  .object({
    name: baseDaCampanha.name.optional(),
    description: baseDaCampanha.description,
    channel_session_id: baseDaCampanha.channel_session_id,
    message_body: baseDaCampanha.message_body,
    email_subject: baseDaCampanha.email_subject,
    message_mode: baseDaCampanha.message_mode,
    provider_template_name: baseDaCampanha.provider_template_name,
    provider_template_language: baseDaCampanha.provider_template_language,
    provider_template_variables: baseDaCampanha.provider_template_variables,
    base_legal: baseDaCampanha.base_legal.optional(),
    lia_ref: baseDaCampanha.lia_ref,
    audience_filter: filtroDeAudienciaSchema.optional(),
    channel_session_ids: baseDaCampanha.channel_session_ids,
    pipeline_id: baseDaCampanha.pipeline_id,
    stage_id: baseDaCampanha.stage_id,
    agent_id: baseDaCampanha.agent_id,
  })
  .merge(ritmoSchema);

export const previaSchema = z.object({
  audience_filter: filtroDeAudienciaSchema,
  /**
   * O mesmo teto do corpo de campanha (#3441): a prévia de um e-mail renderiza
   * um texto que o salvar aceitaria, e recusar aqui um corpo de 5.000
   * caracteres faria a tela mentir sobre o que o canal comporta.
   */
  message_body: z.string().max(LIMITE_DO_CORPO_DO_EMAIL).default(""),
  /**
   * Por onde a prévia conta o público. Ausente = WhatsApp, que é o que toda
   * prévia anterior era; `email` exige `contacts.email` no lugar do telefone.
   */
  canal: z.enum(CANAIS_DA_CAMPANHA).optional(),
  campaign_id: z.string().uuid().optional(),
  /**
   * Com modelo, quem sai por `variavel_ausente` é decidido pelo MAPA de
   * variáveis, não pelo texto — a prévia precisa dele para contar certo.
   */
  modelo: z
    .object({
      channel_session_id: z.string().uuid(),
      name: z.string().trim().min(1).max(512),
      language: z.string().trim().min(2).max(15),
      variables: variaveisDaCampanhaSchema.default({}),
    })
    .optional(),
});

export const criarTemplateSchema = z.object({
  name: z.string().trim().min(1).max(120),
  body: z.string().trim().min(1).max(4096),
});

export const editarTemplateSchema = z.object({
  name: z.string().trim().min(1).max(120).optional(),
  body: z.string().trim().min(1).max(4096).optional(),
});

/**
 * O telefone entra CRU e sai em hash — a normalização e o hash moram em
 * `lib/campanhas/exclusoes.ts`, para o mesmo cálculo valer na gravação e na
 * consulta. Aqui só se exige que algo tenha sido digitado.
 */
export const criarExclusaoSchema = z.object({
  address: z.string().trim().min(8).max(32),
  reason: z.string().trim().max(240).nullable().optional(),
  contact_id: z.string().uuid().nullable().optional(),
});

export const agendarSchema = z.object({ scheduled_at: z.string().datetime() });
export const testarSchema = z.object({ contact_id: z.string().uuid() });

export const listarCampanhasSchema = z.object({
  status: z.string().trim().max(20).optional(),
  cursor: z.string().min(1).optional(),
  limit: z.coerce.number().int().min(1).max(100).default(30),
});

export const listarDestinatariosSchema = z.object({
  status: z.string().trim().max(20).optional(),
  cursor: z.string().min(1).optional(),
  limit: z.coerce.number().int().min(1).max(100).default(50),
});

/**
 * Cursor opaco `created_at|id`, o mesmo formato de `lib/schemas/lead-captures.ts`.
 * Sem HMAC, como os outros: ele só ordena, não autoriza — a organização vem do
 * papel conferido, e um cursor forjado não alcança linha de outro tenant.
 */
export function codificarCursor(c: { created_at: string; id: string }): string {
  return Buffer.from(`${c.created_at}|${c.id}`, "utf8").toString("base64url");
}

export function decodificarCursor(bruto: string): { created_at: string; id: string } | null {
  try {
    const [created_at, id] = Buffer.from(bruto, "base64url").toString("utf8").split("|");
    if (!created_at || !id) return null;
    return { created_at, id };
  } catch {
    return null;
  }
}
