/**
 * Classificador de intenção do Intent Router (Fase 3 — Task 3). Roda um modelo
 * BARATO (router.classifierModel, config da org via ai_routers) pelo MESMO seam
 * agnóstico (runModelCall, purpose 'intent_router') pra decidir pra qual agente
 * do router o turno deve ir. Mesmo padrão de stage-classifier.ts: o classificador
 * SUGERE, nunca escreve estado — quem decide o roteamento é o chamador (Task 4).
 *
 * Defesa contra saída de modelo (não-confiável): parseIntentVerdict NUNCA lança —
 * JSON malformado, campo faltando, intenção alucinada (fora da lista de members
 * do router) ou confidence fora de [0,1]/não-numérico tudo vira veredito nulo
 * ({ intentName: null, confidence: 0 }). classifyIntent embrulha tudo em
 * try/catch: qualquer erro (falha do modelo, LlmModelNotEnabledError, timeout)
 * vira log.warn + null — o chamador cai no fallbackAgentId do router.
 */
import type pg from 'pg';

import { extrairObjetoJsonDoTexto } from '@/lib/agent-engine/texto/extrair-json-do-texto';
import { contextoDoClassificador, type ClassifierContextMessage } from '@/lib/ai/classifier-context';

import type { Logger } from '../obs/logger';
import { runModelCall, type LlmEdgeConfig } from '../edge/llm/run-model-call';
import { CABECALHO_DOS_FATOS_NO_PROMPT, formatarFatosDoCrm, type FatosDoCrm } from '@/lib/leads/fatos-do-crm-no-prompt';
import type { LoadedRouter, RouterMember } from './router-config';

export interface IntentVerdict {
  intentName: string | null;
  confidence: number;
  /**
   * A saída não era uma resposta: sem JSON, JSON inválido, sem intenção, ou uma
   * intenção fora da lista. O roteamento de hoje a lê como "nenhuma" (sticky ou
   * `no_match`), e isso não muda; o que muda é que ninguém a trata como uma
   * escolha da IA — o Jev decidindo não vale no lugar dela (R2), como na
   * manipulação (`classifyJailbreak`, `falhou`).
   */
  falhou?: true;
}

export type { ClassifierContextMessage } from '@/lib/ai/classifier-context';

/** Instrução final fixa — pede JSON estrito, marcador estável pros testes/prompt. */
const JSON_INSTRUCTION =
  'Responda SOMENTE JSON: {"intent": "<nome exato de uma intenção da lista ou none>", "confidence": <0 a 1>}';

/**
 * `recentMessages` é histórico curto (poucas mensagens, mais antiga → mais
 * recente), NUNCA a conversa inteira: o classificador roda em TODO turno,
 * inclusive com sticky ativo (resolve-turn-agent.ts regra 2), então o custo é
 * por mensagem — histórico completo pagaria esse preço a cada turno pra um
 * modelo cuja única pergunta é "mudou de assunto?". Existe só pra resolver
 * ambiguidade de resposta curta (“Primeira”, “sim”) que sem a pergunta
 * anterior parece bater em qualquer intenção — não pra dar memória ao
 * classificador.
 */
export function buildClassifierPrompt(
  members: RouterMember[],
  signal: string,
  recentMessages: ClassifierContextMessage[] = [],
  /**
   * Fatos do CRM do contato (#3917): funis/etapas com negócio ABERTO e
   * etiquetas. Opcional — ausente ou vazio, o prompt é byte a byte o de antes,
   * e o classificador segue decidindo pelo texto como sempre fez.
   * Vai para o provedor: cada nome chega com dado pessoal mascarado
   * (`normalizarNome`, scrub antes do corte de tamanho).
   */
  fatosDoCrm?: FatosDoCrm | null,
): string {
  const list = members
    .map((m) => {
      const examples = m.examples.length > 0 ? ` Exemplos: ${m.examples.join('; ')}.` : '';
      return `- ${m.intentName}: ${m.intentDescription}.${examples}`;
    })
    .join('\n');
  const contexto =
    recentMessages.length > 0
      ? [
          '',
          'Contexto recente da conversa (mais antiga primeiro — só pra desambiguar, NÃO é o que classificar):',
          ...contextoDoClassificador(recentMessages, 16).map((m) => `${m.direction === 'inbound' ? 'Lead' : 'Agente'}: ${m.body}`),
        ]
      : [];
  // Entre o histórico e a mensagem: são fatos do CONTATO (quem ele é), não do
  // turno. O cabeçalho diz em voz alta que não é o que se classifica, senão o
  // modelo passa a responder sobre o funil em vez de escolher a intenção.
  const textoDosFatos = formatarFatosDoCrm(fatosDoCrm);
  const fatos =
    textoDosFatos === ''
      ? []
      : [
          '',
          CABECALHO_DOS_FATOS_NO_PROMPT,
          textoDosFatos,
        ];
  return [
    'Você é um classificador auxiliar de intenção (NÃO responde ao lead).',
    'Intenções possíveis:',
    list,
    '- none: nenhuma das intenções acima se aplica.',
    ...contexto,
    ...fatos,
    '',
    'Mensagem do lead a classificar:',
    signal,
    '',
    JSON_INSTRUCTION,
  ].join('\n');
}

/**
 * Parse tolerante via `extrairJsonDoTexto` (cerca de código, prosa em volta e
 * repetição do objeto). NUNCA lança — qualquer saída inesperada do modelo
 * vira { intentName: null, confidence: 0 }. Intenção fora de `members` é
 * recusada (defesa contra alucinação): o chamador não pode rotear pra um
 * agentId que o parse inventou.
 */
export function parseIntentVerdict(text: string, members: RouterMember[]): IntentVerdict {
  const nullVerdict: IntentVerdict = { intentName: null, confidence: 0, falhou: true };

  const parsed = extrairObjetoJsonDoTexto(text);
  if (parsed === null || typeof parsed !== 'object') {
    return nullVerdict;
  }
  const raw = parsed as { intent?: unknown; confidence?: unknown };
  const confidence = typeof raw.confidence === 'number' && !Number.isNaN(raw.confidence)
    ? Math.min(1, Math.max(0, raw.confidence))
    : 0;

  if (typeof raw.intent !== 'string') return nullVerdict;
  if (raw.intent === 'none') return { intentName: null, confidence };
  const known = members.some((m) => m.intentName === raw.intent);
  if (!known) return nullVerdict;

  return { intentName: raw.intent, confidence };
}

export interface ClassifyIntentDeps {
  log: Logger;
  runModelCall?: typeof runModelCall;
}

export async function classifyIntent(
  db: pg.Pool,
  llmCfg: LlmEdgeConfig,
  input: {
    tenantId: string;
    /** null quando não há contact_id real (ex.: classificação de teste na UI) — vira contact_id null em llm_calls, nunca um uuid inventado (violaria a FK). */
    leadId: string | null;
    /** null quando não há job_queue real por trás da chamada (mesma razão de leadId). */
    jobId: string | null;
    router: LoadedRouter;
    signal: string;
    /** Últimas mensagens ANTERIORES ao signal, mais antiga → mais recente. Default []. */
    recentMessages?: ClassifierContextMessage[];
    /**
     * Fatos do CRM do contato (#3917). Quem lê o banco é o CHAMADOR
     * (`resolve-turn-agent.ts`), não este módulo: a classificação de teste na UI
     * pode não ter contato, e quem sabe se vale a consulta é o turno.
     */
    fatosDoCrm?: FatosDoCrm | null;
  },
  deps: ClassifyIntentDeps,
): Promise<IntentVerdict | null> {
  const call = deps.runModelCall ?? runModelCall;
  try {
    const { result } = await call(
      db,
      llmCfg,
      {
        tenantId: input.tenantId,
        leadId: input.leadId,
        jobId: input.jobId,
        purpose: 'intent_router',
        // Sem modelo próprio ("Automático"), não passa nenhum: o seam resolve
        // pelo painel de provedores ou pelo padrão da organização.
        ...(input.router.classifierModel ? { model: input.router.classifierModel } : {}),
        // Sem isto, o modelo do roteador viaja para o provedor da ORG: escolher
        // um modelo OpenAI numa org configurada como Anthropic mandava o id para
        // o lugar errado, e a classificação falhava sempre.
        ...(input.router.classifierProvider
          ? { llmOverride: { provider: input.router.classifierProvider } }
          : {}),
        messages: [
          {
            role: 'user',
            content: buildClassifierPrompt(input.router.members, input.signal, input.recentMessages ?? [], input.fatosDoCrm ?? null),
          },
        ],
      },
      { log: deps.log },
    );
    return parseIntentVerdict(result.text, input.router.members);
  } catch (err) {
    // Falha do classificador NUNCA derruba o turno — chamador cai no fallback do router.
    deps.log.warn('intent-classifier: falha ao classificar intenção — turno cai no fallback', {
      error: err instanceof Error ? err.message : String(err),
    });
    return null;
  }
}
