/**
 * Zod source-of-truth para configuração de agents (config jsonb + guardrails jsonb).
 * Importado por backend (route handlers) e frontend (editor) — não duplicar.
 */
import { z } from "zod";

import { DEFAULT_SENTIMENT_THRESHOLD } from "@/lib/ai/prompts/sentiment";
import { TETO_DA_FRASE_DO_AVISO } from "@/lib/escalacao/frase-do-aviso-configurada";

// ---------------------------------------------------------------------------
// Models permitidos (Vercel AI Gateway)
// ---------------------------------------------------------------------------

export const AGENT_MODELS = [
  "anthropic/claude-sonnet-4-6",
  "anthropic/claude-haiku-4-5",
  "anthropic/claude-opus-4-7",
] as const;

export const agentModelSchema = z.enum(AGENT_MODELS);
export type AgentModel = z.infer<typeof agentModelSchema>;

// ---------------------------------------------------------------------------
// Guardrails (5 kinds — Spec 05 §8.1)
// ---------------------------------------------------------------------------

export const guardrailKindEnum = z.enum([
  "regex_output_block",
  "rag_must_hit",
  "regex_input_block",
  "window_check",
  "contact_flag",
]);
export type GuardrailKind = z.infer<typeof guardrailKindEnum>;

const guardrailRegexOutputBlock = z.object({
  kind: z.literal("regex_output_block"),
  pattern: z.string().min(1),
  flags: z.string().optional().default("i"),
  reason: z.string().min(1),
});

const guardrailRagMustHit = z.object({
  kind: z.literal("rag_must_hit"),
  min_citations: z.number().int().min(1).max(10).default(1),
  reason: z.string().min(1),
});

const guardrailRegexInputBlock = z.object({
  kind: z.literal("regex_input_block"),
  pattern: z.string().min(1),
  flags: z.string().optional().default("i"),
  reason: z.string().min(1),
});

const guardrailWindowCheck = z.object({
  kind: z.literal("window_check"),
  start_hour: z.number().int().min(0).max(23),
  end_hour: z.number().int().min(0).max(23),
  timezone: z.string().default("America/Sao_Paulo"),
  reason: z.string().min(1),
});

const guardrailContactFlag = z.object({
  kind: z.literal("contact_flag"),
  field: z.enum(["force_human", "is_blocked", "is_vip"]),
  expected: z.boolean(),
  reason: z.string().min(1),
});

export const guardrailItemSchema = z.discriminatedUnion("kind", [
  guardrailRegexOutputBlock,
  guardrailRagMustHit,
  guardrailRegexInputBlock,
  guardrailWindowCheck,
  guardrailContactFlag,
]);
export type GuardrailItem = z.infer<typeof guardrailItemSchema>;

export const guardrailsSchema = z.array(guardrailItemSchema).max(50);
export type Guardrails = z.infer<typeof guardrailsSchema>;

// ---------------------------------------------------------------------------
// Agent config (vai dentro de ai_agents.config jsonb)
// ---------------------------------------------------------------------------

export const AGENT_VOICE_OPTIONS = [
  "marin",
  "cedar",
  "alloy",
  "ash",
  "ballad",
  "coral",
  "echo",
  "sage",
  "shimmer",
  "verse",
] as const;
export const agentVoiceSchema = z.enum(AGENT_VOICE_OPTIONS);
export type AgentVoice = z.infer<typeof agentVoiceSchema>;

// Modelos Realtime da OpenAI que falam por voz de ponta a ponta (audio in ->
// audio out) -- não é o mesmo catálogo de AGENT_MODELS (texto, Vercel AI
// Gateway): a ligação nunca passa por ali. Default "gpt-realtime" == o que
// já rodava fixo via env OPENAI_REALTIME_MODEL antes deste campo existir.
export const AGENT_VOICE_MODEL_OPTIONS = [
  "gpt-realtime",
  "gpt-realtime-mini",
  "gpt-realtime-2.1",
  "gpt-realtime-2.1-mini",
  "gpt-4o-realtime-preview",
  "gpt-4o-mini-realtime-preview",
] as const;
export const agentVoiceModelSchema = z.enum(AGENT_VOICE_MODEL_OPTIONS);
export type AgentVoiceModel = z.infer<typeof agentVoiceModelSchema>;

export const agentConfigSchema = z.object({
  temperature: z.number().min(0).max(2).default(0.4),
  max_tokens: z.number().int().min(64).max(4096).default(1024),
  context_message_window: z.number().int().min(1).max(50).default(20),
  rag_top_k: z.number().int().min(1).max(20).default(5),
  rag_similarity_threshold: z.number().min(0).max(1).default(0.4),
  // O campo de LIMIAR DE CONFIANÇA saiu daqui (issue #1660): o único leitor
  // era o bloco G3 de `workers/ai-response-worker.ts`, inalcançável desde que
  // `elegivelParaWorkerLegado()` passou a devolver `false` (07/09) — a tela
  // vendia "escala para humano abaixo do limiar" e nada escutava. A chave
  // continua no jsonb gravado (default da baseline) e o Zod a descarta como
  // desconhecida.
  //
  // `sentiment_threshold` foi o MESMO descarte com o MESMO custo, e saiu de lá
  // na issue #2209: o worker já lia a chave (`ai-sentiment-worker.ts`), mas o
  // schema do PATCH a ignorava, então um `PATCH /api/v1/ai/agents/{id}`
  // respondia 200 sem gravar e nenhuma tela oferecia o campo. Diferente do
  // limiar de confiança, esta chave TEM leitor vivo — por isso ela fica aqui,
  // no mesmo `config` jsonb, sem migration: um valor por agente, lido na
  // hora em que o clima decide se a conversa passa para uma pessoa.
  // O default é o `DEFAULT_SENTIMENT_THRESHOLD` de `lib/ai/prompts/sentiment`,
  // importado para não haver dois 0.3 divergentes entre a tela e o worker.
  sentiment_threshold: z.number().min(0).max(1).default(DEFAULT_SENTIMENT_THRESHOLD),
  // Só usados por agentes do canal "voice" (audioSocketBridge.ts) — ficam no
  // mesmo config jsonb dos demais, em vez de uma coluna nova, pelo mesmo
  // motivo do rag_top_k: um valor por versão publicada, sem tabela extra.
  voice: agentVoiceSchema.default("marin"),
  // Faixa aceita pela Realtime API da OpenAI é 0.25–1.5 — fora disso a
  // sessão rejeita a configuração.
  voice_speed: z.number().min(0.25).max(1.5).default(0.85),
  voice_model: agentVoiceModelSchema.default("gpt-realtime"),
  /**
   * A voz do AGENTE para a resposta falada por TTS (#4228) — `voice_id` do
   * ElevenLabs, DIFERENTE de `voice` acima, que é a da Realtime (OpenAI) do
   * canal de ligação. `null`/ausente = herda a voz da organização
   * (`organizations.settings.voz.voice_id`), o comportamento de antes.
   *
   * Sem `.default()` de propósito: um PATCH de outra chave não pode regravar a
   * voz de todo agente que nunca escolheu (mesmo motivo de `sentiment_threshold`).
   */
  voz_voice_id: z.string().trim().min(1).max(200).nullable().optional(),
  /**
   * Aceita os comandos de controle `#on`/`#off` enviados pelo CELULAR do
   * operador (C-076)? `false` (default do produto) = o ingest NÃO reconhece os
   * comandos; qualquer mensagem do celular continua pausando a IA normalmente.
   *
   * O default é `false` de propósito: um comando digitado no chat do CLIENTE é
   * uma decisão de produto com efeito visível (o cliente pode ver a mensagem),
   * então não se liga por migration — se liga na tela do agente.
   */
  aceita_comandos_celular: z.boolean().default(false),
  /**
   * Carrega o servidor MCP externo da organização (#2147) também em turno de
   * ATENDIMENTO, com a identidade do contato em cabeçalho que o modelo não
   * controla (`lib/mcp/servidor-externo/carregar.ts`). `false` (default) = o
   * turno com contato segue sem servidor externo, como antes da opção existir.
   * Só quem é admin grava `config` (rota PATCH e RLS de `ai_agents`).
   */
  mcp_externo_no_atendimento: z.boolean().default(false),
  /**
   * COMO este agente lê a base de conhecimento (#4133):
   *
   *  - `busca` (default) = o caminho de sempre: `search_knowledge` por embedding;
   *  - `leitura_direta` = índice de documentos no contexto + `ler_documento`,
   *    sem provedor de embedding nenhum (uma chave só, a do LLM da conversa).
   *
   * O default é `busca` de propósito: o modo novo muda o custo por turno (o
   * índice ocupa contexto), então não se liga por migration — se liga por agente.
   * Espelhado em `PublishedAgentConfig.knowledgeMode` e resolvido em
   * `lib/agent-engine/agent/modo-de-conhecimento.ts`.
   */
  knowledge_mode: z.enum(["busca", "leitura_direta"]).default("busca"),
  /**
   * A frase que o CLIENTE lê quando a IA passa a conversa para uma pessoa
   * (#3725, tela desde o #3816). `null`/ausente = a frase padrão do produto,
   * curta e humana (`textoDoAviso`). Lida em runtime por
   * `lerFraseDoAvisoConfigurada`, que é quem valida tamanho e conteúdo — aqui o
   * teto é o MESMO `TETO_DA_FRASE_DO_AVISO`, para o PATCH recusar com 400 o que
   * a leitura recusaria em silêncio.
   */
  aviso_de_handoff: z.string().max(TETO_DA_FRASE_DO_AVISO).nullable().optional(),
});
export type AgentConfig = z.infer<typeof agentConfigSchema>;

export const AGENT_CONFIG_DEFAULTS: AgentConfig = {
  temperature: 0.4,
  max_tokens: 1024,
  context_message_window: 20,
  rag_top_k: 5,
  rag_similarity_threshold: 0.4,
  voice: "marin",
  voice_speed: 0.85,
  voice_model: "gpt-realtime",
  aceita_comandos_celular: false,
  mcp_externo_no_atendimento: false,
  knowledge_mode: "busca",
  sentiment_threshold: DEFAULT_SENTIMENT_THRESHOLD,
};

// ---------------------------------------------------------------------------
// PATCH / CREATE schemas
// ---------------------------------------------------------------------------

// Parcial SEM defaults. No Zod 4, `.partial()` mantém o `.default()` de cada
// campo: `agentConfigSchema.partial().parse({ rag_top_k: 10 })` devolve os DEZ
// campos, e a junção da rota (`{ ...atual, ...patch.config }`) regravava os
// ajustes que o cliente nem mandou. O cartão "Comandos pelo celular" manda uma
// chave só e zerava temperatura/RAG. Todo campo com default entra aqui — o teste
// `patch-de-config-grava-so-o-que-veio` reprova o que ficar de fora.
const cfg = agentConfigSchema.shape;
export const agentConfigPatchSchema = agentConfigSchema
  .extend({
    temperature: cfg.temperature.removeDefault(),
    max_tokens: cfg.max_tokens.removeDefault(),
    context_message_window: cfg.context_message_window.removeDefault(),
    rag_top_k: cfg.rag_top_k.removeDefault(),
    rag_similarity_threshold: cfg.rag_similarity_threshold.removeDefault(),
    voice: cfg.voice.removeDefault(),
    voice_speed: cfg.voice_speed.removeDefault(),
    voice_model: cfg.voice_model.removeDefault(),
    aceita_comandos_celular: cfg.aceita_comandos_celular.removeDefault(),
    mcp_externo_no_atendimento: cfg.mcp_externo_no_atendimento.removeDefault(),
    // Mesma regra: sem `.default()`, um PATCH de outra chave regravaria o modo
    // de conhecimento de todo agente que nunca o configurou (#4133).
    knowledge_mode: cfg.knowledge_mode.removeDefault(),
    // Sem `.default()`, senão um PATCH de outra chave reescrevia o limiar de
    // todo agente que nunca o configurou — o mesmo defeito de #1631.
    sentiment_threshold: cfg.sentiment_threshold.removeDefault(),
  })
  .partial();

export const agentPatchSchema = z
  .object({
    operation_mode: z.enum(["automatic", "assisted"]).optional(),
    paused_at: z.iso.datetime().nullable().optional(),
    name: z.string().min(2).max(120).optional(),
    description: z.string().max(500).nullable().optional(),
    is_active: z.boolean().optional(),
    model: agentModelSchema.optional(),
    system_prompt: z.string().min(20).max(10000).optional(),
    config: agentConfigPatchSchema.optional(),
    guardrails: guardrailsSchema.optional(),
  })
  .strict();
export type AgentPatch = z.infer<typeof agentPatchSchema>;

export const agentCreateSchema = z
  .object({
    name: z.string().min(2).max(120),
    description: z.string().max(500).nullable().optional(),
    model: agentModelSchema.optional(),
    system_prompt: z
      .string()
      .min(20)
      .max(10000)
      .default(
        "Você é um assistente da loja. Responda com clareza e cordialidade, em português do Brasil. Use a base de conhecimento abaixo quando relevante.",
      ),
  })
  .strict();
export type AgentCreate = z.infer<typeof agentCreateSchema>;

// ---------------------------------------------------------------------------
// Placeholders disponíveis no system prompt (helper UI)
// ---------------------------------------------------------------------------

export const SYSTEM_PROMPT_PLACEHOLDERS: Array<{ token: string; description: string }> = [
  { token: "{{vocabulary.lead}}", description: "Vocabulário do tenant para 'lead' (ex: cliente)" },
  { token: "{{vocabulary.deal}}", description: "Vocabulário do tenant para 'deal' (ex: pedido)" },
  { token: "{{vocabulary.won}}", description: "Vocabulário do tenant para 'won' (ex: pago)" },
  {
    token: "{{vocabulary.lost}}",
    description: "Vocabulário do tenant para 'lost' (ex: cancelado)",
  },
  { token: "{{contact_name}}", description: "Nome do contato em atendimento" },
  { token: "{{contact_locale}}", description: "Locale do contato (ex: pt-BR)" },
  { token: "{{recent_messages}}", description: "Últimas N mensagens da conversa" },
  { token: "{{retrieved_chunks}}", description: "Trechos da base de conhecimento (RAG)" },
];
