import { NextResponse } from 'next/server'
import { timingSafeEqual } from 'crypto'
import { db } from '@/lib/db'
import { saveAudio } from '@/lib/media'
import { triggerAgent } from '@/lib/agent-trigger'
import { sendTelegram } from '@/lib/telegram'

export const runtime = 'nodejs'

// POST /api/ingest/telegram — Telegram pushes every message here.
//
// This is the only genuinely public door into the system, so it is deliberately
// paranoid and deliberately quiet:
//
//  - Telegram signs each call with the secret set at setWebhook time, echoed in
//    X-Telegram-Bot-Api-Secret-Token. Without a match nothing is read.
//  - The sending chat must map to an account (User.telegramChatId). An update
//    from anyone else is dropped, so finding the bot buys nothing.
//  - Every outcome returns 200. Telegram retries non-2xx with backoff and will
//    eventually disable a webhook that keeps failing, so a rejected stranger
//    must look exactly like a success from the outside.

interface TgFile { file_id?: string; file_unique_id?: string; duration?: number; mime_type?: string }
interface TgMessage {
  message_id?: number
  chat?: { id?: number | string }
  text?: string
  caption?: string
  voice?: TgFile
  audio?: TgFile
}

function secretOk(req: Request): boolean {
  const expected = process.env.TELEGRAM_WEBHOOK_SECRET
  // Unset means the webhook was never configured — refuse rather than run open.
  if (!expected) return false
  const got = req.headers.get('x-telegram-bot-api-secret-token') || ''
  const a = Buffer.from(got)
  const b = Buffer.from(expected)
  return a.length === b.length && timingSafeEqual(a, b)
}

// A voice note arrives as an id, not bytes: resolve it, then fetch from the
// file host. Bot API downloads cap at 20MB, which a voice note never reaches.
async function downloadVoice(token: string, fileId: string): Promise<{ buf: Buffer; mime: string } | null> {
  try {
    const metaRes = await fetch(`https://api.telegram.org/bot${token}/getFile?file_id=${encodeURIComponent(fileId)}`,
      { signal: AbortSignal.timeout(10_000) })
    const meta = await metaRes.json()
    const path = meta?.result?.file_path
    if (!path) return null
    const fileRes = await fetch(`https://api.telegram.org/file/bot${token}/${path}`,
      { signal: AbortSignal.timeout(30_000) })
    if (!fileRes.ok) return null
    return {
      buf: Buffer.from(await fileRes.arrayBuffer()),
      // Telegram voice notes are OGG/Opus; process-capture.sh normalises
      // through ffmpeg either way, and whisper then gets the vocabulary
      // biasing that Siri's own dictation cannot do.
      mime: 'audio/ogg',
    }
  } catch (e) {
    console.error('[telegram] voice download failed:', e)
    return null
  }
}

export async function POST(req: Request) {
  if (!secretOk(req)) return NextResponse.json({ ok: true })

  const update = await req.json().catch(() => null)
  const msg: TgMessage | undefined = update?.message ?? update?.edited_message
  const chatId = msg?.chat?.id
  if (!msg || chatId === undefined || chatId === null) return NextResponse.json({ ok: true })

  const owner = await db.user.findUnique({
    where: { telegramChatId: String(chatId) },
    select: { id: true },
  })
  if (!owner) {
    console.warn(`[telegram] update from unmapped chat ${chatId} — ignored`)
    return NextResponse.json({ ok: true })
  }

  const token = process.env.TELEGRAM_BOT_TOKEN?.trim()
  const text = (msg.text || msg.caption || '').trim()
  const voiceId = msg.voice?.file_id || msg.audio?.file_id

  let audioPath: string | null = null
  let audioMime: string | null = null
  let durationMs: number | null = null
  if (voiceId && token) {
    const dl = await downloadVoice(token, voiceId)
    if (dl) {
      audioMime = dl.mime
      audioPath = await saveAudio(dl.buf, dl.mime)
      const secs = msg.voice?.duration ?? msg.audio?.duration
      durationMs = typeof secs === 'number' ? Math.round(secs * 1000) : null
    } else {
      await sendTelegram("I couldn't fetch that voice note — try again, or type it.", String(chatId))
      return NextResponse.json({ ok: true })
    }
  }

  // A bare "/start" or a sticker is not a capture; say nothing and move on.
  if (!audioPath && !text) return NextResponse.json({ ok: true })
  if (!audioPath && text.startsWith('/')) {
    await sendTelegram('Say or type what you want done — reminders, lists, chores, timers, sales.', String(chatId))
    return NextResponse.json({ ok: true })
  }

  const capture = await db.capture.create({
    data: { ownerId: owner.id, source: 'TELEGRAM', audioPath, audioMime, durationMs, textNote: audioPath ? null : text },
    select: { id: true },
  })

  // Same contract as every other entry point: a trigger failure leaves the
  // capture PENDING for the stuck-capture sweep rather than losing it.
  triggerAgent(capture.id).catch(err =>
    console.error(`[telegram ${capture.id}] agent trigger failed:`, err?.message || err))

  return NextResponse.json({ ok: true })
}
