import path from 'node:path'
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import JSZip from 'jszip'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { fileExportContract } from '@/lib/api/contracts/storage-transfer'
import { parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { extractEmbeddedImageIds } from '@/lib/copilot/tools/server/files/embedded-image-refs'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
import type { StorageContext } from '@/lib/uploads/config'
import { USE_BLOB_STORAGE } from '@/lib/uploads/config'
import { downloadFile } from '@/lib/uploads/core/storage-service'
import { getFileMetadataById } from '@/lib/uploads/server/metadata'
import { verifyFileAccess } from '@/app/api/files/authorization'
import { encodeFilenameForHeader } from '@/app/api/files/utils'

const logger = createLogger('FilesExportAPI')

const MARKDOWN_MIME_TYPES = new Set(['text/markdown', 'text/x-markdown'])
const MARKDOWN_EXTENSIONS = new Set(['md', 'markdown'])

function isMarkdown(originalName: string, contentType: string): boolean {
  if (MARKDOWN_MIME_TYPES.has(contentType)) return true
  const ext = originalName.split('.').pop()?.toLowerCase() ?? ''
  return MARKDOWN_EXTENSIONS.has(ext)
}

function safeFilename(name: string): string {
  return path
    .basename(name)
    .replace(/["\\]/g, '_')
    .replace(/[\r\n\t]/g, '')
}

function deduplicatedFilename(preferred: string, existing: Set<string>, imageId: string): string {
  if (!existing.has(preferred)) return preferred
  const ext = path.extname(preferred)
  const base = path.basename(preferred, ext)
  const short = `${base}_${imageId.slice(0, 8)}${ext}`
  if (!existing.has(short)) return short
  return `${base}_${imageId}${ext}`
}

export const GET = withRouteHandler(
  async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
    const parsed = await parseRequest(fileExportContract, request, context)
    if (!parsed.success) return parsed.response

    const { id } = parsed.data.params

    const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
    if (!authResult.success || !authResult.userId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    const userId = authResult.userId

    const record = await getFileMetadataById(id)
    if (!record) {
      logger.warn('File not found by ID', { id })
      return NextResponse.json({ error: 'Not found' }, { status: 404 })
    }

    const hasAccess = await verifyFileAccess(record.key, userId)
    if (!hasAccess) {
      logger.warn('Unauthorized file export attempt', { id, userId })
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
    }

    /**
     * Records the egress only at a real success exit (serve redirect, plain
     * markdown, or bundled zip) so a mid-export failure never logs a download
     * that never happened.
     */
    const auditExport = (format: 'file' | 'markdown' | 'zip', assetCount: number) => {
      recordAudit({
        workspaceId: record.workspaceId ?? null,
        actorId: userId,
        action: AuditAction.FILE_DOWNLOADED,
        resourceType: AuditResourceType.FILE,
        resourceId: record.id,
        resourceName: record.originalName,
        description: `Exported file "${record.originalName}"`,
        metadata: {
          fileId: record.id,
          fileName: record.originalName,
          bytes: record.size,
          format,
          assetCount,
        },
        request,
      })
      captureServerEvent(
        userId,
        'file_downloaded',
        {
          ...(record.workspaceId ? { workspace_id: record.workspaceId } : {}),
          is_bulk: assetCount > 0,
          file_count: 1 + assetCount,
        },
        record.workspaceId ? { groups: { workspace: record.workspaceId } } : undefined
      )
    }

    if (!isMarkdown(record.originalName, record.contentType)) {
      const storagePrefix = USE_BLOB_STORAGE ? 'blob' : 's3'
      const servePath = `/api/files/serve/${storagePrefix}/${encodeURIComponent(record.key)}`
      auditExport('file', 0)
      return NextResponse.redirect(new URL(servePath, request.url), { status: 302 })
    }

    const mdBuffer = await downloadFile({
      key: record.key,
      context: record.context as StorageContext,
    })
    let mdContent = mdBuffer.toString('utf-8')

    const imageIds = extractEmbeddedImageIds(mdContent)

    logger.info('Exporting markdown', { id, imageCount: imageIds.length })

    if (imageIds.length === 0) {
      const mdName = safeFilename(record.originalName)
      const mdBytes = Buffer.from(mdContent, 'utf-8')
      auditExport('markdown', 0)
      return new NextResponse(new Uint8Array(mdBytes), {
        status: 200,
        headers: {
          'Content-Type': 'text/markdown; charset=utf-8',
          'Content-Disposition': `attachment; ${encodeFilenameForHeader(mdName)}`,
          'Content-Length': String(mdBytes.length),
        },
      })
    }

    const fetchResults = await Promise.allSettled(
      imageIds.map(async (imageId) => {
        const imgRecord = await getFileMetadataById(imageId)
        if (!imgRecord) return null
        const imgHasAccess = await verifyFileAccess(imgRecord.key, userId)
        if (!imgHasAccess) return null
        const imgBuffer = await downloadFile({
          key: imgRecord.key,
          context: imgRecord.context as StorageContext,
        })
        return { imageId, originalName: imgRecord.originalName, buffer: imgBuffer }
      })
    )

    const assetMap = new Map<string, { filename: string; buffer: Buffer }>()
    const usedFilenames = new Set<string>()

    for (let i = 0; i < fetchResults.length; i++) {
      const result = fetchResults[i]
      if (result.status === 'rejected') {
        logger.warn('Failed to fetch asset for export', {
          imageId: imageIds[i],
          error: toError(result.reason).message,
        })
        continue
      }
      if (!result.value) continue
      const { imageId, originalName, buffer } = result.value
      const preferred = safeFilename(originalName)
      const filename = deduplicatedFilename(preferred, usedFilenames, imageId)
      usedFilenames.add(filename)
      assetMap.set(imageId, { filename, buffer })
    }

    for (const [imageId, asset] of assetMap) {
      const escapedId = imageId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      const replacement = `./assets/${asset.filename}`
      // Rewrite both embed spellings the extractor resolves to this id — the view URL and the in-app
      // `/workspace/<ws>/files/<id>` path — so a bundled asset never leaves a broken link in the export.
      mdContent = mdContent
        .replace(new RegExp(`/api/files/view/${escapedId}`, 'g'), () => replacement)
        .replace(new RegExp(`/workspace/[A-Za-z0-9-]+/files/${escapedId}`, 'g'), () => replacement)
    }

    const zip = new JSZip()
    zip.file(safeFilename(record.originalName), mdContent)
    const assetsFolder = zip.folder('assets')!
    for (const { filename, buffer } of assetMap.values()) {
      assetsFolder.file(filename, buffer)
    }

    const zipBuffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' })
    const zipName = safeFilename(`${record.originalName.replace(/\.[^.]+$/, '')}.zip`)

    auditExport('zip', assetMap.size)
    return new NextResponse(new Uint8Array(zipBuffer), {
      status: 200,
      headers: {
        'Content-Type': 'application/zip',
        'Content-Disposition': `attachment; ${encodeFilenameForHeader(zipName)}`,
        'Content-Length': String(zipBuffer.length),
      },
    })
  }
)
