export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'

import { NextResponse } from 'next/server'
import Groq from 'groq-sdk'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { fetch as undiciFetch, ProxyAgent } from 'undici'

import { voiceBus } from '@/lib/voice-bus'
import { systemPrompt } from '@/lib/voice-prompts'
import { TOOL_SCHEMAS, executeTool } from '@/lib/tools/_registry'
import { tryLocalAgent } from '@/lib/agent-local'
import { cleanForSpeech, stripFillers, isResetCommand } from '@/lib/voice-text'
import { loadHistory, saveHistory, resetHistory, HistoryMessage } from '@/lib/voice-history'
import { promises as nodeFs } from 'node:fs'

const MAX_TOKENS = parseInt(process.env.VOICE_MAX_TOKENS || '300', 10)
const MAX_TOOL_ROUNDS = 4
const RATE_LIMIT_PER_MINUTE = parseInt(process.env.VOICE_RATE_LIMIT || '20', 10)

const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ''
const ANTHROPIC_PROXY = process.env.ANTHROPIC_PROXY || ''
const GROQ_API_KEY = process.env.GROQ_API_KEY || ''
const GROQ_PROXY = process.env.GROQ_PROXY || ''

const SETTINGS_PATH = '/data/settings.json'
const SETTINGS_DEFAULTS = { voiceProvider: 'anthropic', anthropicModel: 'claude-haiku-4-5-20251001', groqModel: 'meta-llama/llama-4-scout-17b-16e-instruct' }

async function getVoiceSettings() {
  try {
    const raw = await nodeFs.readFile(SETTINGS_PATH, 'utf-8')
    return { ...SETTINGS_DEFAULTS, ...JSON.parse(raw) }
  } catch { return { ...SETTINGS_DEFAULTS } }
}

// ——— Rate limit ———
const rateBuckets = new Map<string, { count: number; resetAt: number }>()
function rateLimit(key: string): boolean {
  const now = Date.now()
  const b = rateBuckets.get(key)
  if (!b || b.resetAt <= now) { rateBuckets.set(key, { count: 1, resetAt: now + 60_000 }); return true }
  if (b.count >= RATE_LIMIT_PER_MINUTE) return false
  b.count++; return true
}
let lastSweep = 0
function sweep() {
  const now = Date.now()
  if (now - lastSweep < 5 * 60_000) return
  lastSweep = now
  for (const [k, v] of rateBuckets) if (v.resetAt <= now) rateBuckets.delete(k)
}

// ——— Groq client ———
let _groqClient: Groq | null = null
function groqClient(): Groq {
  if (_groqClient) return _groqClient
  if (!GROQ_API_KEY) throw new Error('GROQ_API_KEY not set')
  const httpAgent = GROQ_PROXY ? new HttpsProxyAgent(GROQ_PROXY) : undefined
  _groqClient = new Groq({ apiKey: GROQ_API_KEY, httpAgent } as any)
  return _groqClient
}

// ——— Anthropic helpers ———
function toAnthropicTools(tools: any[]): any[] {
  return tools.map(t => ({
    name: t.function.name,
    description: t.function.description || '',
    input_schema: t.function.parameters || { type: 'object', properties: {} },
  }))
}

function historyToAnthropicMessages(history: HistoryMessage[]): any[] {
  const result: any[] = []
  for (const msg of history) {
    if ((msg.role as string) === 'system') continue
    if (msg.role === 'user') {
      result.push({ role: 'user', content: msg.content || '' })
    } else if (msg.role === 'assistant') {
      if (msg.tool_calls?.length) {
        const content: any[] = []
        if (msg.content) content.push({ type: 'text', text: msg.content })
        for (const tc of msg.tool_calls) {
          let input: any = {}
          try { input = JSON.parse(tc.function.arguments) } catch {}
          content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input })
        }
        result.push({ role: 'assistant', content })
      } else {
        result.push({ role: 'assistant', content: msg.content || '' })
      }
    } else if (msg.role === 'tool') {
      const last = result[result.length - 1]
      const block = { type: 'tool_result', tool_use_id: msg.tool_call_id, content: msg.content || '' }
      if (last?.role === 'user' && Array.isArray(last.content)) last.content.push(block)
      else result.push({ role: 'user', content: [block] })
    }
  }
  return result
}

async function claudeRequest(system: string, messages: any[], tools: any[]): Promise<any> {
  const body: any = { model: 'claude-haiku-4-5-20251001', max_tokens: MAX_TOKENS, system, messages }
  if (tools.length > 0) body.tools = tools
  const opts: any = {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' },
    body: JSON.stringify(body),
  }
  if (ANTHROPIC_PROXY) opts.dispatcher = new ProxyAgent(ANTHROPIC_PROXY)
  const res = await (ANTHROPIC_PROXY ? undiciFetch : fetch)('https://api.anthropic.com/v1/messages', opts)
  if (!res.ok) { const err = await res.text(); throw new Error(`anthropic_${res.status}: ${err}`) }
  return res.json()
}

type AgentId = 'cosmo' | 'lusya'
function emitVoice(event: string, agent: AgentId, text?: string) {
  voiceBus.emit('voice', { event, agent, text, timestamp: new Date().toISOString() })
}

export async function POST(req: Request) {
  const cookie = req.headers.get('cookie') || ''
  const tokenMatch = cookie.match(/auth_token=([a-f0-9]{32,})/i)
  const internal = req.headers.get('x-voice-internal') || ''
  const fwd = req.headers.get('x-forwarded-for') || ''
  const ratekey = tokenMatch?.[1] || (internal ? 'internal' : '') || fwd.split(',')[0].trim() || 'anon'
  sweep()
  if (!rateLimit(ratekey)) return NextResponse.json({ error: 'rate_limited' }, { status: 429 })

  const body = await req.json().catch(() => null)
  if (!body || typeof body.text !== 'string' || !body.text.trim()) return NextResponse.json({ error: 'text required' }, { status: 400 })

  const userText = body.text.trim().slice(0, 4000)
  const agent: AgentId = body.agent === 'lusya' ? 'lusya' : 'cosmo'
  emitVoice('command', agent, userText)

  if (isResetCommand(userText)) {
    await resetHistory(agent)
    const msg = 'Начинаю новую сессию.'
    emitVoice('response', agent, msg)
    return NextResponse.json({ text: msg, reset: true })
  }

  const settings = await getVoiceSettings()
  const provider = settings.voiceProvider || 'anthropic'
  const history = await loadHistory(agent)
  const sysPrompt = systemPrompt(agent)
  const newTurns: HistoryMessage[] = [{ role: 'user', content: userText }]
  let finalText = ''

  // ======== Локальный агент (fast-path) ========
  // Если задан AGENT_URL — команды-действия исполняет локальная intent-служба
  // за ~2 мс, минуя облачный LLM. Инфо-запросы и всё неуверенное падают в LLM
  // ниже. Любая ошибка → тоже fallback. Без AGENT_URL — поведение не меняется.
  try {
    const fast = await tryLocalAgent(userText, agent)
    if (fast) {
      const assistant: HistoryMessage = { role: 'assistant', content: fast.text }
      await saveHistory(agent, [...history, ...newTurns, assistant])
      emitVoice('response', agent, fast.text)
      console.log(`[voice/chat] local-agent ${agent} intent=${fast.intent}`)
      return NextResponse.json({ text: fast.text, via: 'local-agent' })
    }
  } catch (e) {
    console.log('[voice/chat] local-agent skip:', (e as Error).message)
  }

  // ======== GROQ ========
  if (provider === 'groq') {
    const groqModel = settings.groqModel || 'llama-3.3-70b-versatile'
    // Нормализовать tool_calls в истории — Groq требует type:'function'
    const normalizedHistory = history.map(m => {
      if (m.role === 'assistant' && m.tool_calls?.length) {
        return { ...m, tool_calls: m.tool_calls.map((tc: any) => ({ type: 'function', ...tc })) }
      }
      return m
    })
    const groqMessages: any[] = [
      { role: 'system', content: sysPrompt },
      ...normalizedHistory,
      { role: 'user', content: userText },
    ]
    let lastToolSig = ''
    try {
      const c = groqClient()
      for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
        const t0 = Date.now()
        const resp = await c.chat.completions.create({
          model: groqModel, max_tokens: MAX_TOKENS,
          messages: groqMessages as any,
          tools: TOOL_SCHEMAS as any,
          tool_choice: 'auto',
        })
        const choice = resp.choices[0]
        const msg = choice.message
        console.log(`[voice/chat] groq ${agent} r${round+1} ${Date.now()-t0}ms stop=${choice.finish_reason}`)
        groqMessages.push(msg)
        if (choice.finish_reason === 'tool_calls' && msg.tool_calls?.length) {
          const sig = msg.tool_calls.map((tc: any) => `${tc.function.name}:${tc.function.arguments}`).sort().join('|')
          if (sig === lastToolSig) { finalText = 'Не удалось выполнить запрос.'; break }
          lastToolSig = sig
          newTurns.push({ role: 'assistant', content: msg.content ?? null, tool_calls: msg.tool_calls })
          for (const tc of msg.tool_calls) {
            let args: any = {}; try { args = JSON.parse(tc.function.arguments) } catch {}
            const result = await executeTool(tc.function.name, args, agent)
            groqMessages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) })
            newTurns.push({ role: 'tool', content: JSON.stringify(result), tool_call_id: tc.id })
          }
          continue
        }
        finalText = msg.content || ''
        newTurns.push({ role: 'assistant', content: finalText })
        break
      }
    } catch (e: any) {
      const errStr = String(e?.message || e)
      console.error('[voice/chat] groq error:', errStr)
      if (errStr.includes('tool_use_failed') || errStr.includes('Failed to call a function')) {
        try {
          const fb = await groqClient().chat.completions.create({
            model: groqModel, max_tokens: MAX_TOKENS,
            messages: [{ role: 'system', content: sysPrompt }, { role: 'user', content: userText }],
          })
          finalText = fb.choices[0]?.message?.content || 'Не удалось выполнить запрос.'
          newTurns.push({ role: 'assistant', content: finalText })
        } catch { finalText = 'Не удалось выполнить запрос.' }
      } else {
        const msg = 'Что-то сломалось.'
        emitVoice('error', agent, msg)
        return NextResponse.json({ error: 'llm_failed', detail: errStr, text: msg }, { status: 502 })
      }
    }

  // ======== ANTHROPIC ========
  } else {
    const anthropicTools = toAnthropicTools(TOOL_SCHEMAS as any[])
    const messages: any[] = [...historyToAnthropicMessages(history), { role: 'user', content: userText }]
    try {
      for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
        const t0 = Date.now()
        const resp = await claudeRequest(sysPrompt, messages, anthropicTools)
        console.log(`[voice/chat] claude ${agent} r${round+1} ${Date.now()-t0}ms stop=${resp.stop_reason} in=${resp.usage?.input_tokens} out=${resp.usage?.output_tokens}`)
        const content: any[] = resp.content || []
        const stopReason = resp.stop_reason || 'end_turn'
        if (stopReason === 'tool_use') {
          const toolUseBlocks = content.filter((b: any) => b.type === 'tool_use')
          const partialText = content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('')
          messages.push({ role: 'assistant', content })
          newTurns.push({
            role: 'assistant', content: partialText || null,
            tool_calls: toolUseBlocks.map((b: any) => ({ id: b.id, function: { name: b.name, arguments: JSON.stringify(b.input) } })),
          })
          const toolResults: any[] = []
          for (const tb of toolUseBlocks) {
            console.log(`[voice/chat] tool ${tb.name}(${JSON.stringify(tb.input).slice(0,200)})`)
            const result = await executeTool(tb.name, tb.input || {}, agent)
            toolResults.push({ type: 'tool_result', tool_use_id: tb.id, content: JSON.stringify(result) })
            newTurns.push({ role: 'tool', content: JSON.stringify(result), tool_call_id: tb.id })
          }
          messages.push({ role: 'user', content: toolResults })
          continue
        }
        finalText = content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('')
        newTurns.push({ role: 'assistant', content: finalText })
        break
      }
    } catch (e: any) {
      const errStr = String(e?.message || e)
      console.error('[voice/chat] claude error:', errStr)
      const msg = 'Что-то сломалось.'
      emitVoice('error', agent, msg)
      return NextResponse.json({ error: 'llm_failed', detail: errStr, text: msg }, { status: 502 })
    }
  }

  if (!finalText.trim()) {
    const msg = 'Не получил ответ.'
    emitVoice('error', agent, msg)
    return NextResponse.json({ text: msg }, { status: 200 })
  }

  await saveHistory(agent, [...history, ...newTurns])
  const cleaned = cleanForSpeech(stripFillers(finalText))
  emitVoice('response', agent, cleaned)
  return NextResponse.json({ text: cleaned })
}
