import { prisma } from '@/lib/prisma'
import { NextRequest, NextResponse } from 'next/server'
import bcrypt from 'bcrypt'
import fs from 'fs/promises'
import { TransferStatus } from '@prisma/client'
import { decryptFileStream } from '@/lib/server-encryption'
import { rateLimit, resetRateLimit, clientKey } from '@/lib/rate-limit'

// 10 password attempts per transfer, per client, per 15 minutes.
const ATTEMPT_LIMIT = 10
const ATTEMPT_WINDOW_MS = 15 * 60 * 1000

// Node APIs (fs handles, crypto) — this route cannot run on the edge runtime.
export const runtime = 'nodejs'

/**
 * Build a Content-Disposition value that survives quotes, newlines and
 * non-ASCII characters in a user-supplied filename.
 */
function contentDisposition(filename: string): string {
  const fallback = filename.replace(/[^\w.\-]+/g, '_') || 'download'
  const encoded = encodeURIComponent(filename)
  return `attachment; filename="${fallback}"; filename*=UTF-8''${encoded}`
}

/**
 * POST (not GET) so the password travels in the body — as a query parameter it
 * would be recorded in access logs, proxy logs and Referer headers.
 */
export async function POST(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params

  if (!id) {
    return NextResponse.json({ error: 'Missing ID' }, { status: 400 })
  }

  let password = ''
  let requestedFileId: number | undefined
  try {
    const body = await req.json()
    password = typeof body?.password === 'string' ? body.password : ''
    if (typeof body?.fileId === 'number' && Number.isInteger(body.fileId)) {
      requestedFileId = body.fileId
    }
  } catch {
    return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
  }

  if (!password) {
    return NextResponse.json({ error: 'Password required' }, { status: 401 })
  }

  const rateKey = `file:${id}:${clientKey(req.headers)}`
  const limit = rateLimit(rateKey, ATTEMPT_LIMIT, ATTEMPT_WINDOW_MS)
  if (!limit.allowed) {
    return NextResponse.json(
      { error: 'Too many attempts. Please try again later.' },
      { status: 429, headers: { 'Retry-After': String(limit.retryAfter) } }
    )
  }

  const transfer = await prisma.transfer.findUnique({
    where: { downloadUrl: id },
    include: { files: true },
  })

  if (!transfer || !transfer.files.length) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 })
  }

  // Expiry and soft-deletion are enforced here, not just displayed in the UI.
  if (transfer.status !== TransferStatus.ACTIVE) {
    return NextResponse.json({ error: 'This transfer is no longer available' }, { status: 410 })
  }

  if (transfer.expiresAt <= new Date()) {
    if (transfer.status === TransferStatus.ACTIVE) {
      await prisma.transfer.update({
        where: { id: transfer.id },
        data: { status: TransferStatus.EXPIRED },
      })
    }
    return NextResponse.json({ error: 'This transfer has expired' }, { status: 410 })
  }

  // Verify the password against the stored hash BEFORE touching the file or
  // recording a download. The GCM tag check later is a second, independent gate.
  if (!transfer.passwordHash) {
    return NextResponse.json({ error: 'This transfer is not downloadable' }, { status: 409 })
  }

  const passwordValid = await bcrypt.compare(password, transfer.passwordHash)
  if (!passwordValid) {
    return NextResponse.json({ error: 'Incorrect password' }, { status: 401 })
  }

  resetRateLimit(rateKey)

  // Pick the requested file, scoped to this transfer so a file id from another
  // transfer cannot be fetched. Defaults to the first file.
  const file = requestedFileId === undefined
    ? transfer.files[0]
    : transfer.files.find((f) => f.id === requestedFileId)

  if (!file) {
    return NextResponse.json({ error: 'File not found in this transfer' }, { status: 404 })
  }

  try {
    await fs.access(file.path)
  } catch {
    return NextResponse.json({ error: 'File not found' }, { status: 404 })
  }

  // Only now, after a successful authentication, does this count as a download.
  await prisma.fileDownload.create({
    data: {
      fileId: file.id,
      ip: req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || null,
      userAgent: req.headers.get('user-agent') || null,
    },
  })

  // Probe the first frame before committing to a 200, so a corrupt or
  // legacy-format file produces a proper status code rather than a truncated body.
  const frames = decryptFileStream(file.path, password)
  let firstFrame: Buffer | undefined
  try {
    const first = await frames.next()
    if (!first.done) firstFrame = first.value
  } catch (err: unknown) {
    await frames.return(undefined as never).catch(() => {})
    if (err instanceof Error && err.message === 'UNSUPPORTED_FORMAT') {
      console.error(`Legacy-format file for transfer ${transfer.id}: ${file.path}`)
      return NextResponse.json(
        { error: 'This transfer was created with an older, incompatible version and cannot be decrypted. Please ask the sender to resend it.' },
        { status: 422 }
      )
    }
    console.error('Decrypt error:', err)
    return NextResponse.json({ error: 'Unable to decrypt this file' }, { status: 422 })
  }

  // Stream the remaining frames so large transfers are never fully buffered.
  const stream = new ReadableStream<Uint8Array>({
    async start(controller) {
      if (firstFrame) controller.enqueue(new Uint8Array(firstFrame))
    },
    async pull(controller) {
      try {
        const next = await frames.next()
        if (next.done) {
          controller.close()
          return
        }
        controller.enqueue(new Uint8Array(next.value))
      } catch (err) {
        console.error('Decrypt error mid-stream:', err)
        controller.error(err)
      }
    },
    async cancel() {
      await frames.return(undefined as never).catch(() => {})
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'application/octet-stream',
      'Content-Disposition': contentDisposition(file.name),
      'Cache-Control': 'no-store',
      'X-Content-Type-Options': 'nosniff',
    },
  })
}
