/**
 * Attachment upload/delete routes — tasks-detail-communication.
 *
 * POST   /api/attachments          → upload file, returns attachment metadata
 * DELETE /api/attachments/:id      → delete file from R2 + DB row
 *
 * Stored in R2 under: {tenantId}/tasks/{taskId}/{uuid}-{filename}
 * Attachments are initially "unparented" (no message_id); they are re-parented
 * when the comment POST references them via attachmentIds.
 *
 * Permissions: tasks:write
 */
import { Hono } from 'hono'
import type { AppEnv } from '../types'
import { authMiddleware } from '../middleware/auth'
import { requirePermission } from '../middleware/guards'
import { requireModuleEnabled } from '../middleware/require-module-enabled'
import {
  insertDraftAttachment,
  getMessageAttachment,
  deleteMessageAttachmentRow,
  getTask,
} from '@zync/db/queries'
import { createSignedDownloadUrl } from '../lib/portal-file-storage'
import { UploadContentRejectedError, validateUploadContent } from '../lib/upload-mime-guard'

// ── Constants ─────────────────────────────────────────────────────────────────

const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024 // 25MB

const ALLOWED_MIME_TYPES = new Set([
  // Images (SVG excluded — inline XSS risk)
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp',
  'image/bmp',
  'image/tiff',
  // PDFs
  'application/pdf',
  // Office docs
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'application/vnd.ms-excel',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'application/vnd.ms-powerpoint',
  'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  // Archives
  'application/zip',
  'application/x-tar',
  'application/x-gzip',
  'application/gzip',
  'application/x-7z-compressed',
  'application/x-rar-compressed',
  // Text
  'text/plain',
  'text/csv',
])

function r2Key(tenantId: string, taskId: string, uuid: string, filename: string): string {
  return `${tenantId}/tasks/${taskId}/${uuid}-${filename}`
}

// ── Router ────────────────────────────────────────────────────────────────────

export const attachmentsRouter = new Hono<AppEnv>()

attachmentsRouter.use('*', authMiddleware)
attachmentsRouter.use('*', requireModuleEnabled('tasks'))

// ── POST /api/attachments ─────────────────────────────────────────────────────

attachmentsRouter.post(
  '/',
  requirePermission('tasks:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    // Parse multipart form data
    let formData: FormData
    try {
      formData = await c.req.formData()
    } catch {
      return c.json({ error: 'Invalid multipart form data' }, 400)
    }

    const file = formData.get('file') as File | null
    const taskId = formData.get('taskId') as string | null

    if (!file || !(file instanceof File)) {
      return c.json({ error: 'file is required' }, 400)
    }
    if (!taskId || typeof taskId !== 'string') {
      return c.json({ error: 'taskId is required' }, 400)
    }

    const db = c.get('db')
    const tenantId = session.tid

    const task = await getTask(db, tenantId, taskId)
    if (!task) {
      return c.json({ error: 'Not found' }, 404)
    }

    // Size check
    if (file.size > MAX_ATTACHMENT_BYTES) {
      return c.json({ error: `File exceeds maximum size of ${MAX_ATTACHMENT_BYTES} bytes` }, 400)
    }

    // MIME type check
    const mimeType = file.type || 'application/octet-stream'
    if (!ALLOWED_MIME_TYPES.has(mimeType)) {
      return c.json({ error: `MIME type '${mimeType}' is not allowed` }, 400)
    }

    const fileBytes = new Uint8Array(await file.arrayBuffer())
    try {
      validateUploadContent(mimeType, fileBytes)
    } catch (err) {
      if (err instanceof UploadContentRejectedError) {
        return c.json({ error: err.message }, 400)
      }
      throw err
    }

    // Generate a unique R2 key
    const uuid = crypto.randomUUID()
    const safeFilename = file.name.replace(/[^a-zA-Z0-9._-]/g, '_')
    const key = r2Key(tenantId, taskId, uuid, safeFilename)

    // Upload to R2
    try {
      await c.env.STORAGE.put(key, fileBytes, {
        httpMetadata: { contentType: mimeType },
        customMetadata: {
          tenantId,
          taskId,
          originalName: file.name,
        },
      })
    } catch {
      return c.json({ error: 'Failed to upload file' }, 500)
    }

    // Generate signed URL (1h TTL, forced attachment disposition)
    let signedUrl: string
    try {
      signedUrl = await createSignedDownloadUrl(c.env, key, {
        expiresIn: 3600,
        filename: file.name,
      })
    } catch {
      await c.env.STORAGE.delete(key).catch(() => {})
      return c.json({ error: 'Failed to generate download URL' }, 500)
    }

    // Insert a draft attachment row (messageId = null; reparented when comment is posted)
    let attRow: { id: string; url: string; r2Key: string; filename: string; sizeBytes: number; mimeType: string }
    try {
      attRow = await insertDraftAttachment(db, {
        tenantId,
        filename: file.name,
        url: signedUrl,
        r2Key: key,
        sizeBytes: file.size,
        mimeType,
      })
    } catch {
      // Roll back R2 upload on DB failure
      await c.env.STORAGE.delete(key).catch(() => {})
      return c.json({ error: 'Failed to save attachment metadata' }, 500)
    }

    return c.json({
      id: attRow.id,   // DB row UUID — client passes this as attachmentIds[] when posting the comment
      url: signedUrl,
      filename: file.name,
      sizeBytes: file.size,
      mimeType,
    }, 201)
  },
)

// ── DELETE /api/attachments/:id ───────────────────────────────────────────────

attachmentsRouter.delete(
  '/:id',
  requirePermission('tasks:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const db = c.get('db')
    const attachmentId = c.req.param('id')
    const tenantId = session.tid

    const att = await getMessageAttachment(db, tenantId, attachmentId)
    if (!att) {
      return c.json({ error: 'Not found' }, 404)
    }

    // Delete from R2
    try {
      await c.env.STORAGE.delete(att.r2Key)
    } catch {
      // Non-fatal — proceed to delete the DB row
    }

    // Delete from DB
    await deleteMessageAttachmentRow(db, tenantId, attachmentId)

    return c.body(null, 204)
  },
)
