/**
 * ProtocolEngine — Cérebro Stateful da Secretária IA
 *
 * Princípios de economia de tokens:
 * 1. Lê o ESTADO atual do protocolo (summary), não o histórico completo
 * 2. Carrega apenas as últimas N mensagens (context_window)
 * 3. Sumarização ativa a cada 10 trocas para manter o resumo atualizado
 * 4. Nós do cérebro são compostos apenas com os ativos
 */

import { Knex } from 'knex'
import type { PluginConfigStore } from '../../backend/src/core/plugin-config'
import type { HookBus } from '../../backend/src/core/hook-bus'
import { type ToolDef, type ToolContext, resolveTools, ALL_TOOL_NAMES } from './tools'

export class ProtocolEngine {
  constructor(
    private readonly db: Knex,
    private readonly config: PluginConfigStore,
  ) {}

  // ── Chat ─────────────────────────────────────────────────────────────────

  async chat(
    conversationId: string,
    userMessage: string,
    opts?: {
      contextData?: Record<string, unknown>
      systemExtra?: string
      tools?: string[]       // nomes das tools a habilitar (padrão: todas)
      hooks?: HookBus
      tenantId?: string
    },
  ): Promise<string> {
    const conversation = await this.db('sec_conversations').where({ id: conversationId }).first()
    if (!conversation) throw new Error('Conversa não encontrada')

    const agent = await this.db('sec_agents').where({ id: conversation.agent_id }).first()
    if (!agent) throw new Error('Agente não encontrado')

    // Monta system prompt a partir dos nós ativos + contexto externo
    const systemPrompt = await this.buildSystemPrompt(agent, conversation, opts)

    // Carrega apenas as últimas N mensagens (não o histórico completo)
    const contextWindow: number = agent.context_window ?? 8
    const recentMessages = await this.db('sec_messages')
      .where({ conversation_id: conversationId })
      .orderBy('created_at', 'desc')
      .limit(contextWindow)
      .then((rows: any[]) => rows.reverse())

    // Salva a mensagem do usuário
    await this.db('sec_messages').insert({
      id: this.uuid(),
      conversation_id: conversationId,
      role: 'user',
      content: userMessage,
      created_at: new Date(),
    })

    // Chama a IA — usa node_model do nó persona se definido (sobrepõe o agente)
    const personaNode = await this.db('sec_brain_nodes')
      .where({ agent_id: conversation.agent_id, type: 'persona', active: true })
      .orderBy('sort_order')
      .first()

    const agentOverride = personaNode?.node_model
      ? { ...agent, model: personaNode.node_model }
      : agent

    const messages = [
      ...recentMessages.map((m: any) => ({ role: m.role, content: m.content })),
      { role: 'user', content: userMessage },
    ]

    // Resolve tools: usa lista passada por opts, ou todas as builtins por padrão
    const toolNames = opts?.tools ?? ALL_TOOL_NAMES
    const toolDefs  = resolveTools(toolNames)

    const toolCtx: ToolContext = {
      db:             this.db,
      conversationId,
      extChatId:      conversation.ext_chat_id ?? undefined,
      tenantId:       opts?.tenantId,
      hooks:          opts?.hooks,
    }

    let response: string
    let usageInfo: any = null
    let providerUsed: string | null = null
    let modelUsed: string | null = null
    try {
      if (toolDefs.length > 0) {
        response = await this.callAIWithTools(agentOverride, systemPrompt, messages, toolDefs, toolCtx)
        // Telemetria escrita pelos tool loops via side channel (toolCtx._telemetry)
        if (toolCtx._telemetry) {
          usageInfo    = toolCtx._telemetry.usage
          providerUsed = toolCtx._telemetry.provider
          modelUsed    = toolCtx._telemetry.model
        }
      } else {
        const result = await this.callAI(agentOverride, systemPrompt, messages)
        response     = result.text
        usageInfo    = result.usage
        providerUsed = result.provider
        modelUsed    = result.model
      }
    } catch (err: any) {
      response = `[Erro ao chamar IA: ${err.message}. Verifique a API Key nas configurações do plugin.]`
    }

    // Salva resposta da IA com telemetria de tokens
    await this.db('sec_messages').insert({
      id: this.uuid(),
      conversation_id: conversationId,
      role: 'assistant',
      content: response,
      usage_tokens:  usageInfo ? JSON.stringify(usageInfo) : null,
      provider_used: providerUsed,
      model_used:    modelUsed,
      created_at: new Date(),
    })

    // Atualiza conversa + sumariza a cada 10 trocas
    const totalMsgs = await this.db('sec_messages')
      .where({ conversation_id: conversationId })
      .count('id as c')
      .first()
      .then((r: any) => Number(r?.c ?? 0))

    let summary = conversation.summary
    if (totalMsgs > 0 && (totalMsgs % 10 === 0 || totalMsgs === 5)) {
      summary = await this.summarize(agent, recentMessages, userMessage, response)
    }

    await this.db('sec_conversations')
      .where({ id: conversationId })
      .update({ updated_at: new Date(), summary })

    return response
  }

  // ── Protocol Number ───────────────────────────────────────────────────────

  static generateProtocolNumber(): string {
    const now = new Date()
    const p = (n: number, d = 2) => String(n).padStart(d, '0')
    return `${p(now.getDate())}${p(now.getMonth() + 1)}${String(now.getFullYear()).slice(-2)}${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`
  }

  // ── System Prompt Builder ─────────────────────────────────────────────────

  private async buildSystemPrompt(
    agent: any,
    conversation: any,
    opts?: { contextData?: Record<string, unknown>; systemExtra?: string },
  ): Promise<string> {
    const nodes = await this.db('sec_brain_nodes')
      .where({ agent_id: agent.id, active: true })
      .orderBy('sort_order')

    let prompt = ''

    // Data/hora real — impede o modelo de alucinar a data
    const nowReal = new Date()
    const dtStr = nowReal.toLocaleString('pt-BR', {
      timeZone: 'America/Sao_Paulo',
      weekday: 'long', day: '2-digit', month: 'long', year: 'numeric',
      hour: '2-digit', minute: '2-digit',
    })
    prompt += `=== DATA E HORA ATUAL ===\n${dtStr} (horário de Brasília)\n\n`

    // Cabeçalho do protocolo — sempre presente, leve (3 linhas)
    const protocolHeader = [
      `=== PROTOCOLO ATIVO ===`,
      `Número: ${conversation.protocol_number || '—'}`,
      `Contato: ${conversation.contact_name}`,
      `Status: ${conversation.status}`,
      ``,
    ].join('\n')
    prompt += protocolHeader

    for (const node of nodes as any[]) {
      switch (node.type) {
        case 'persona':
          prompt += `${node.content}\n\n`
          break
        case 'knowledge':
          prompt += `=== BASE DE CONHECIMENTO ===\n${node.content}\n\n`
          break
        case 'rules':
          prompt += `=== REGRAS ===\n${node.content}\n\n`
          break
        case 'calendar': {
          const calCtx = await this.getCalendarContext()
          prompt += `=== AGENDA DISPONÍVEL (próximos 7 dias) ===\n${calCtx}\n\nInstruções: ${node.content}\n\n`
          break
        }
        case 'escalation':
          prompt += `=== REGRAS DE ESCALADA ===\n${node.content}\n\n`
          break
        default:
          prompt += `${node.content}\n\n`
      }
    }

    // Injeta contexto local do projeto (enviado pelo plugin satélite)
    if (opts?.contextData && Object.keys(opts.contextData).length > 0) {
      const ctx = JSON.stringify(opts.contextData, null, 2)
      prompt += `=== CONTEXTO DO CLIENTE (dados reais do projeto) ===\n${ctx}\n\n`
    }

    // Prompt extra do plugin (instruções específicas da chamada)
    if (opts?.systemExtra?.trim()) {
      prompt += `=== INSTRUÇÕES ADICIONAIS ===\n${opts.systemExtra.trim()}\n\n`
    }

    // Injeta resumo do estado atual (economia de tokens — evita reler o histórico)
    if (conversation.summary) {
      prompt += `=== ESTADO ATUAL DA CONVERSA ===\n${conversation.summary}\n\n`
    }

    return prompt.trim()
  }

  // ── Finalize Protocol ─────────────────────────────────────────────────────

  async finalizeProtocol(conversationId: string): Promise<{ summary: string; protocol_number: string }> {
    const conversation = await this.db('sec_conversations').where({ id: conversationId }).first()
    if (!conversation) throw new Error('Conversa não encontrada')
    if (conversation.status === 'closed') {
      return { summary: conversation.summary ?? '', protocol_number: conversation.protocol_number }
    }

    const agent = await this.db('sec_agents').where({ id: conversation.agent_id }).first()
    if (!agent) throw new Error('Agente não encontrado')

    // Carrega todas as mensagens para gerar resumo completo
    const messages = await this.db('sec_messages')
      .where({ conversation_id: conversationId })
      .orderBy('created_at')

    let summary = conversation.summary ?? ''

    if (messages.length > 0) {
      const transcript = (messages as any[])
        .map((m) => `${m.role === 'user' ? 'Cliente' : 'Ana'}: ${m.content}`)
        .join('\n')

      const summaryPrompt = `Gere um resumo estruturado desta conversa de atendimento para uso futuro como contexto rápido.\nInclua: motivo do contato, o que foi resolvido, próximos passos pendentes (se houver).\nMáximo 5 linhas. Seja objetivo.\n\nProtocolo: ${conversation.protocol_number}\nContato: ${conversation.contact_name}\n\n${transcript}`

      const cheapModel: Record<string, string> = {
        openai: 'gpt-4o-mini', anthropic: 'claude-3-5-haiku-20241022',
        gemini: 'gemini-2.0-flash', ollama: agent.model ?? 'llama3',
      }
      const finalAgent = {
        ...agent, temperature: 0.2, max_tokens: 200,
        model: cheapModel[agent.provider as string] ?? agent.model,
      }
      try {
        const result = await this.callAI(finalAgent, '', [{ role: 'user', content: summaryPrompt }])
        summary = result.text
      } catch {
        summary = conversation.summary ?? `Protocolo ${conversation.protocol_number} encerrado.`
      }
    }

    // Apaga mensagens — contexto comprimido no resumo (economia de tokens)
    await this.db('sec_messages').where({ conversation_id: conversationId }).delete()

    // Fecha o protocolo com resumo persistido
    await this.db('sec_conversations')
      .where({ id: conversationId })
      .update({ status: 'closed', summary, updated_at: new Date() })

    return { summary, protocol_number: conversation.protocol_number }
  }

  // ── AI Call ───────────────────────────────────────────────────────────────

  private buildFallbackChain(agent: any, cfg: any): { provider: string; model: string }[] {
    const chainStr: string = (cfg.fallback_chain as string | undefined) ?? 'openai,gemini,anthropic,ollama'
    const order = chainStr.split(',').map((s: string) => s.trim()).filter(Boolean)

    const defaults: Record<string, string> = {
      openai: 'gpt-4o-mini',
      anthropic: 'claude-3-5-haiku-20241022',
      gemini: 'gemini-2.0-flash',
      ollama: 'llama3',
    }

    const hasKey = (p: string): boolean => {
      if (p === 'openai') return !!(cfg.openai_key as string | undefined)
      if (p === 'anthropic') return !!(cfg.anthropic_key as string | undefined)
      if (p === 'gemini') return !!(cfg.gemini_key as string | undefined)
      if (p === 'ollama') return true  // local, sempre disponível
      return false
    }

    const agentProvider: string = agent.provider ?? 'openai'
    const agentModel: string = agent.model ?? defaults[agentProvider] ?? 'gpt-4o-mini'

    const chain: { provider: string; model: string }[] = [{ provider: agentProvider, model: agentModel }]

    for (const p of order) {
      if (p === agentProvider) continue
      if (!hasKey(p)) continue
      chain.push({ provider: p, model: defaults[p] ?? p })
    }

    return chain
  }

  private isRecoverableError(err: Error): boolean {
    const msg = err.message.toLowerCase()
    return (
      msg.includes('quota') ||
      msg.includes('rate limit') ||
      msg.includes('limite da api') ||
      msg.includes('exceeded') ||
      msg.includes('billing') ||
      msg.includes('insufficient') ||
      msg.includes('invalid_api_key') ||
      msg.includes('econnrefused') ||
      msg.includes('enotfound') ||
      msg.includes('não configurada')
    )
  }

  private async callAI(
    agent: any, systemPrompt: string, messages: any[],
  ): Promise<{ text: string; usage: any; provider: string; model: string }> {
    const cfg = await this.config.get('secretaria')
    const chain = this.buildFallbackChain(agent, cfg)

    let lastError: Error = new Error('Nenhum provider disponível')

    for (const entry of chain) {
      try {
        return await this.callProvider(entry.provider, entry.model, agent, cfg, systemPrompt, messages)
      } catch (err: any) {
        lastError = err
        if (this.isRecoverableError(err)) continue
        throw err
      }
    }

    throw new Error(`Todos os providers falharam. Último erro: ${lastError.message}`)
  }

  /**
   * Chama o provider e retorna { text, usage, provider, model }.
   * usage: { input, output, cached?, total } — chars/tokens consumidos.
   * Reads agent.max_tokens (default 250 — adequado a WhatsApp).
   */
  private async callProvider(
    provider: string, model: string, agent: any, cfg: any, systemPrompt: string, messages: any[],
  ): Promise<{ text: string; usage: any; provider: string; model: string }> {
    const maxTokens = agent.max_tokens ?? 250
    const temperature = agent.temperature ?? 0.7

    // ── OpenAI ────────────────────────────────────────────────────────────────
    if (provider === 'openai') {
      const apiKey = (cfg.openai_key as string | undefined) ?? ''
      if (!apiKey) throw new Error('OpenAI API Key não configurada. Acesse Admin → Plugins → Secretária IA.')

      const res = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
        signal: AbortSignal.timeout(25_000),
        body: JSON.stringify({
          model,
          temperature,
          max_tokens: maxTokens,
          messages: [{ role: 'system', content: systemPrompt }, ...messages],
        }),
      })
      const data = (await res.json()) as any
      if (!res.ok) throw new Error(data.error?.message ?? `OpenAI ${res.status}`)
      const text = data.choices[0].message.content as string
      const usage = {
        input:  data.usage?.prompt_tokens ?? 0,
        output: data.usage?.completion_tokens ?? 0,
        cached: data.usage?.prompt_tokens_details?.cached_tokens ?? 0,
        total:  data.usage?.total_tokens ?? 0,
      }
      return { text, usage, provider, model }
    }

    // ── Anthropic (com prompt caching ephemeral no system) ───────────────────
    if (provider === 'anthropic') {
      const apiKey = (cfg.anthropic_key as string | undefined) ?? ''
      if (!apiKey) throw new Error('Anthropic API Key não configurada. Acesse Admin → Plugins → Secretária IA.')

      const res = await fetch('https://api.anthropic.com/v1/messages', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': apiKey,
          'anthropic-version': '2023-06-01',
          // Header necessário até GA do prompt caching
          'anthropic-beta': 'prompt-caching-2024-07-31',
        },
        signal: AbortSignal.timeout(25_000),
        body: JSON.stringify({
          model,
          max_tokens: maxTokens,
          // System como array com cache_control: trecho fica em cache 5min
          // Próximas chamadas com mesmo systemPrompt pagam ~10% pelo trecho cacheado.
          system: [
            { type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } },
          ],
          messages,
        }),
      })
      const data = (await res.json()) as any
      if (!res.ok) throw new Error(data.error?.message ?? `Anthropic ${res.status}`)
      const text = data.content[0].text as string
      const usage = {
        input:        data.usage?.input_tokens ?? 0,
        output:       data.usage?.output_tokens ?? 0,
        cache_create: data.usage?.cache_creation_input_tokens ?? 0,
        cache_read:   data.usage?.cache_read_input_tokens ?? 0,
        total:        (data.usage?.input_tokens ?? 0) + (data.usage?.output_tokens ?? 0),
      }
      return { text, usage, provider, model }
    }

    // ── Google Gemini ─────────────────────────────────────────────────────────
    if (provider === 'gemini') {
      const apiKey = (cfg.gemini_key as string | undefined) ?? ''
      if (!apiKey) throw new Error('Google Gemini API Key não configurada. Acesse Admin → Plugins → Secretária IA.')

      const geminiModel = model.startsWith('gemini') ? model : 'gemini-2.0-flash'

      const geminiMessages = messages.map((m) => ({
        role: m.role === 'assistant' ? 'model' : 'user',
        parts: [{ text: m.content }],
      }))

      const geminiBody = JSON.stringify({
        systemInstruction: { parts: [{ text: systemPrompt }] },
        contents: geminiMessages,
        generationConfig: { temperature, maxOutputTokens: maxTokens },
      })
      const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`

      const doGeminiCall = async () => fetch(geminiUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        signal: AbortSignal.timeout(25_000),
        body: geminiBody,
      })

      let res = await doGeminiCall()
      let data = (await res.json()) as any

      if (!res.ok && (res.status === 429 || String(data.error?.message ?? '').toLowerCase().includes('quota'))) {
        const msg: string = data.error?.message ?? ''
        const match = msg.match(/retry in ([\d.]+)s/i)
        // Espera no máximo 8s (era 30s) e mínimo 1.5s (era 5s) — limita impacto de quota no tempo total
        const waitMs = match ? Math.min(Math.ceil(parseFloat(match[1])) * 1000, 8_000) : 1_500
        await new Promise((r) => setTimeout(r, waitMs))
        res = await doGeminiCall()
        data = (await res.json()) as any
      }

      if (!res.ok) {
        const errMsg: string = data.error?.message ?? `Gemini ${res.status}`
        if (errMsg.toLowerCase().includes('quota') || res.status === 429) {
          throw new Error('Limite da API Gemini atingido. Aguarde alguns instantes e tente novamente.')
        }
        throw new Error(errMsg)
      }
      const text = data.candidates[0].content.parts[0].text as string
      const usage = {
        input:  data.usageMetadata?.promptTokenCount ?? 0,
        output: data.usageMetadata?.candidatesTokenCount ?? 0,
        total:  data.usageMetadata?.totalTokenCount ?? 0,
      }
      return { text, usage, provider, model: geminiModel }
    }

    // ── Ollama (local) ────────────────────────────────────────────────────────
    if (provider === 'ollama') {
      const baseUrl = (cfg.ollama_url as string | undefined) ?? 'http://localhost:11434'
      const ollamaModel = model || 'llama3'

      const res = await fetch(`${baseUrl}/api/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        signal: AbortSignal.timeout(25_000),
        body: JSON.stringify({
          model: ollamaModel,
          stream: false,
          messages: [{ role: 'system', content: systemPrompt }, ...messages],
          options: { temperature, num_predict: maxTokens },
        }),
      })
      const data = (await res.json()) as any
      if (!res.ok) throw new Error(data.error ?? `Ollama ${res.status}`)
      const text = data.message.content as string
      const usage = {
        input:  data.prompt_eval_count ?? 0,
        output: data.eval_count ?? 0,
        total:  (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0),
      }
      return { text, usage, provider, model: ollamaModel }
    }

    throw new Error(`Provider "${provider}" não suportado. Use: openai, anthropic, gemini, ollama`)
  }

  // ── Calendar Context ──────────────────────────────────────────────────────

  private async getCalendarContext(): Promise<string> {
    const today = new Date().toISOString().split('T')[0]
    const nextWeek = new Date()
    nextWeek.setDate(nextWeek.getDate() + 7)
    const nextWeekStr = nextWeek.toISOString().split('T')[0]

    const slots = await this.db('sec_calendar')
      .whereIn('status', ['available', 'booked'])
      .whereBetween('date', [today, nextWeekStr])
      .orderBy('date')
      .orderBy('time_start')
      .limit(30)

    if (slots.length === 0) return 'Nenhum horário nos próximos 7 dias.'

    const lines = (slots as any[]).map((s) => {
      const time = `${s.date} ${s.time_start.slice(0, 5)}–${s.time_end.slice(0, 5)}`
      if (s.status === 'booked') {
        const who = s.attendee_name ? ` | Paciente: ${s.attendee_name}` : ''
        const phone = s.attendee_phone ? ` (${s.attendee_phone})` : ''
        return `• [AGENDADO] ${time}: ${s.title}${who}${phone}`
      }
      return `• [DISPONÍVEL] ${time}: ${s.title}`
    })
    return lines.join('\n')
  }

  // ── Summarization (token economy) ────────────────────────────────────────

  /**
   * Sumarização com modelo barato (M1.5).
   * Força modelo "mini/haiku/flash" mesmo que o agente principal use modelo caro.
   * Sumário é tarefa simples — não precisa do modelo de produção.
   */
  private async summarize(
    agent: any,
    recentMsgs: any[],
    lastUser: string,
    lastAssistant: string,
  ): Promise<string> {
    const excerpt = [
      ...recentMsgs.slice(-6).map((m: any) => `${m.role}: ${m.content}`),
      `user: ${lastUser}`,
      `assistant: ${lastAssistant}`,
    ].join('\n')

    const prompt = `Resuma em no máximo 2 frases curtas o estado atual desta conversa de atendimento, focando no tema e próximo passo:\n\n${excerpt}`

    // Modelo barato por provider
    const cheapModel: Record<string, string> = {
      openai:    'gpt-4o-mini',
      anthropic: 'claude-3-5-haiku-20241022',
      gemini:    'gemini-2.0-flash',
      ollama:    agent.model ?? 'llama3',
    }
    const summaryAgent = {
      ...agent,
      temperature: 0.3,
      max_tokens:  120,
      model: cheapModel[agent.provider as string] ?? agent.model,
    }
    try {
      const result = await this.callAI(summaryAgent, '', [{ role: 'user', content: prompt }])
      return result.text
    } catch {
      return ''
    }
  }

  // ── Tool Calling ──────────────────────────────────────────────────────────

  private async callAIWithTools(
    agent: any,
    systemPrompt: string,
    inputMessages: any[],
    tools: ToolDef[],
    toolCtx: ToolContext,
  ): Promise<string> {
    const cfg   = await this.config.get('secretaria')
    const chain = this.buildFallbackChain(agent, cfg)

    // Prefere provider com suporte a tool calling; Ollama cai em modo texto
    const TOOL_PROVIDERS = ['openai', 'anthropic', 'gemini']
    const entry = chain.find(e => TOOL_PROVIDERS.includes(e.provider))

    if (!entry) {
      // Nenhum provider com tool calling disponível — usa modo texto normal
      return (await this.callAI(agent, systemPrompt, inputMessages)).text
    }

    try {
      switch (entry.provider) {
        case 'openai':
          return await this.openAIToolLoop(entry.model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx)
        case 'anthropic':
          return await this.anthropicToolLoop(entry.model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx)
        case 'gemini':
          return await this.geminiToolLoop(entry.model, agent, cfg, systemPrompt, inputMessages, tools, toolCtx)
        default:
          return (await this.callAI(agent, systemPrompt, inputMessages)).text
      }
    } catch (err: any) {
      if (this.isRecoverableError(err)) {
        // Provider com tools falhou — tenta sem tools no próximo da chain
        return (await this.callAI(agent, systemPrompt, inputMessages)).text
      }
      throw err
    }
  }

  private async executeTool(
    name: string, rawArgs: string | Record<string, any>, tools: ToolDef[], toolCtx: ToolContext,
  ): Promise<any> {
    const tool = tools.find(t => t.name === name)
    if (!tool) return { error: `Tool "${name}" não encontrada.` }
    const args = typeof rawArgs === 'string' ? JSON.parse(rawArgs || '{}') : rawArgs
    try {
      return await tool.execute(args, toolCtx)
    } catch (e: any) {
      return { error: e.message }
    }
  }

  // ── Telemetria helper para tool loops ─────────────────────────────────────
  private accumTelemetry(
    toolCtx: ToolContext, provider: string, model: string,
    incremental: { input: number; output: number; cache_read?: number; cached?: number },
  ): void {
    if (!toolCtx._telemetry) {
      toolCtx._telemetry = {
        usage: { input: 0, output: 0, total: 0, cache_read: 0, cached: 0 },
        provider, model, iterations: 0,
      }
    }
    toolCtx._telemetry.iterations += 1
    toolCtx._telemetry.usage.input  += incremental.input
    toolCtx._telemetry.usage.output += incremental.output
    toolCtx._telemetry.usage.total  += incremental.input + incremental.output
    if (incremental.cache_read) toolCtx._telemetry.usage.cache_read = (toolCtx._telemetry.usage.cache_read ?? 0) + incremental.cache_read
    if (incremental.cached) toolCtx._telemetry.usage.cached = (toolCtx._telemetry.usage.cached ?? 0) + incremental.cached
  }

  // ── OpenAI tool loop ───────────────────────────────────────────────────────

  private async openAIToolLoop(
    model: string, agent: any, cfg: any,
    systemPrompt: string, inputMessages: any[],
    tools: ToolDef[], toolCtx: ToolContext,
  ): Promise<string> {
    const apiKey = (cfg.openai_key as string | undefined) ?? ''
    if (!apiKey) throw new Error('OpenAI API Key não configurada')

    const oaiTools = tools.map(t => ({
      type: 'function',
      function: { name: t.name, description: t.description, parameters: t.parameters },
    }))

    let msgs = [...inputMessages]
    const MAX_ITER = 5

    for (let i = 0; i < MAX_ITER; i++) {
      const res = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
        signal: AbortSignal.timeout(25_000),
        body: JSON.stringify({
          model,
          temperature:  agent.temperature ?? 0.7,
          max_tokens:   agent.max_tokens ?? 250,
          messages:     [{ role: 'system', content: systemPrompt }, ...msgs],
          tools:        oaiTools,
          tool_choice:  'auto',
        }),
      })
      const data = (await res.json()) as any
      if (!res.ok) throw new Error(data.error?.message ?? `OpenAI ${res.status}`)

      // Telemetria (M1.4)
      this.accumTelemetry(toolCtx, 'openai', model, {
        input:  data.usage?.prompt_tokens ?? 0,
        output: data.usage?.completion_tokens ?? 0,
        cached: data.usage?.prompt_tokens_details?.cached_tokens ?? 0,
      })

      const choice = data.choices[0]
      const assistantMsg = choice.message

      if (choice.finish_reason !== 'tool_calls' || !assistantMsg.tool_calls?.length) {
        return (assistantMsg.content ?? '') as string
      }

      // Execute tools in parallel
      msgs.push(assistantMsg)
      const toolResults = await Promise.all(
        (assistantMsg.tool_calls as any[]).map(async (tc) => {
          const result = await this.executeTool(tc.function.name, tc.function.arguments, tools, toolCtx)
          return { role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) }
        }),
      )
      msgs.push(...toolResults)
    }

    throw new Error('Tool calling: limite de iterações atingido')
  }

  // ── Anthropic tool loop ────────────────────────────────────────────────────

  private async anthropicToolLoop(
    model: string, agent: any, cfg: any,
    systemPrompt: string, inputMessages: any[],
    tools: ToolDef[], toolCtx: ToolContext,
  ): Promise<string> {
    const apiKey = (cfg.anthropic_key as string | undefined) ?? ''
    if (!apiKey) throw new Error('Anthropic API Key não configurada')

    const anthropicTools = tools.map(t => ({
      name: t.name, description: t.description, input_schema: t.parameters,
    }))

    let msgs = [...inputMessages]
    const MAX_ITER = 5

    for (let i = 0; i < MAX_ITER; i++) {
      const res = await fetch('https://api.anthropic.com/v1/messages', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': apiKey,
          'anthropic-version': '2023-06-01',
        },
        signal: AbortSignal.timeout(25_000),
        body: JSON.stringify({
          model, max_tokens: agent.max_tokens ?? 250, system: systemPrompt,
          messages: msgs, tools: anthropicTools,
        }),
      })
      const data = (await res.json()) as any
      if (!res.ok) throw new Error(data.error?.message ?? `Anthropic ${res.status}`)

      // Telemetria (M1.4)
      this.accumTelemetry(toolCtx, 'anthropic', model, {
        input:      data.usage?.input_tokens ?? 0,
        output:     data.usage?.output_tokens ?? 0,
        cache_read: data.usage?.cache_read_input_tokens ?? 0,
      })

      // Texto puro
      if (data.stop_reason !== 'tool_use') {
        const textBlock = (data.content as any[]).find(b => b.type === 'text')
        return (textBlock?.text ?? '') as string
      }

      // Tool calls
      msgs.push({ role: 'assistant', content: data.content })

      const toolResults = await Promise.all(
        (data.content as any[])
          .filter(b => b.type === 'tool_use')
          .map(async (b) => {
            const result = await this.executeTool(b.name, b.input, tools, toolCtx)
            return { type: 'tool_result', tool_use_id: b.id, content: JSON.stringify(result) }
          }),
      )
      msgs.push({ role: 'user', content: toolResults })
    }

    throw new Error('Tool calling (Anthropic): limite de iterações atingido')
  }

  // ── Gemini tool loop ───────────────────────────────────────────────────────

  private async geminiToolLoop(
    model: string, agent: any, cfg: any,
    systemPrompt: string, inputMessages: any[],
    tools: ToolDef[], toolCtx: ToolContext,
  ): Promise<string> {
    const apiKey = (cfg.gemini_key as string | undefined) ?? ''
    if (!apiKey) throw new Error('Gemini API Key não configurada')

    const geminiModel = model.startsWith('gemini') ? model : 'gemini-2.0-flash'
    const url = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`

    const geminiTools = [{
      functionDeclarations: tools.map(t => ({
        name: t.name, description: t.description, parameters: t.parameters,
      })),
    }]

    // Converte msgs para formato Gemini
    let contents: any[] = inputMessages.map(m => ({
      role: m.role === 'assistant' ? 'model' : 'user',
      parts: [{ text: m.content as string }],
    }))

    const MAX_ITER = 5

    for (let i = 0; i < MAX_ITER; i++) {
      const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        signal: AbortSignal.timeout(25_000),
        body: JSON.stringify({
          systemInstruction: { parts: [{ text: systemPrompt }] },
          contents,
          tools: geminiTools,
          generationConfig: { temperature: agent.temperature ?? 0.7, maxOutputTokens: agent.max_tokens ?? 250 },
        }),
      })
      const data = (await res.json()) as any
      if (!res.ok) throw new Error(data.error?.message ?? `Gemini ${res.status}`)

      // Telemetria (M1.4)
      this.accumTelemetry(toolCtx, 'gemini', geminiModel, {
        input:  data.usageMetadata?.promptTokenCount ?? 0,
        output: data.usageMetadata?.candidatesTokenCount ?? 0,
      })

      const candidate = data.candidates?.[0]
      const parts: any[] = candidate?.content?.parts ?? []

      // Verifica se há function calls
      const fnCalls = parts.filter(p => p.functionCall)
      if (!fnCalls.length) {
        const textPart = parts.find(p => p.text)
        return (textPart?.text ?? '') as string
      }

      // Adiciona resposta do modelo ao histórico
      contents.push({ role: 'model', parts })

      // Executa tools e injeta resultados
      const resultParts = await Promise.all(
        fnCalls.map(async (p) => {
          const result = await this.executeTool(p.functionCall.name, p.functionCall.args ?? {}, tools, toolCtx)
          return { functionResponse: { name: p.functionCall.name, response: result } }
        }),
      )
      contents.push({ role: 'user', parts: resultParts })
    }

    throw new Error('Tool calling (Gemini): limite de iterações atingido')
  }

  // ── Utils ─────────────────────────────────────────────────────────────────

  private uuid(): string {
    // Node 14.17+ tem crypto.randomUUID globalmente; fallback para Date-based
    try {
      return (crypto as any).randomUUID()
    } catch {
      return `${Date.now()}-${Math.random().toString(36).slice(2)}`
    }
  }
}
