/**
 * Unified attachments routes — unified-attachments (P065).
 *
 * POST   /api/attachments                    → upload file for any entity
 * GET    /api/attachments                    → list attachments for entity
 * GET    /api/attachments/:id/url            → get fresh signed URL (KV-cached 55 min)
 * DELETE /api/attachments/:id               → soft-delete + enqueue R2 delete
 *
 * R2 bucket: STORAGE binding
 * KV:        ATTACHMENT_URL_CACHE binding (`url:{id}` → { url, expires_at })
 * Queue:     QUEUE binding — dispatches 'r2.delete' job for async R2 deletion
 *
 * R2 key format: {tenantId}/{entityType}/{entityId}/{uuid}-{safeFilename}
 *
 * Entity types (spec 41):
 *   task_message | expense | ticket_message | kb_article | vendor
 *
 * Permissions:
 *   Reads:   <entity-module>:read  (mapped per entity type below)
 *   Writes:  <entity-module>:write
 */
import { Hono } from 'hono'
import type { Context } from 'hono'
import type { AppEnv } from '../types'
import { authMiddleware } from '../middleware/auth'
import {
  listAttachmentsForEntity,
  getAttachment,
  insertAttachment,
  softDeleteAttachment,
  getExpense,
  getVendor,
  getTaskMessageById,
  getTicketMessageById,
  kbTenantQuery,
  type AttachmentEntityType,
} from '@zync/db/queries'
import { createSignedDownloadUrl } from '../lib/portal-file-storage'
import { UploadContentRejectedError, validateUploadContent } from '../lib/upload-mime-guard'
import type { SessionPayload } from '@zync/types'

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

/** Global hard cap; per-entity limits enforced in validation matrix below. */
const GLOBAL_MAX_BYTES = 25 * 1024 * 1024

/** Signed URL TTL in seconds (1 hour). */
const SIGNED_URL_TTL = 3600

/** KV TTL for cached signed URLs — 55 min gives ≥5 min buffer before expiry. */
const KV_CACHE_TTL_SEC = 3300

// ── Per-entity validation matrix (spec 41) ────────────────────────────────────

const ENTITY_LIMITS: Record<
  AttachmentEntityType,
  { maxBytes: number; allowedMime: Set<string> }
> = {
  task_message: {
    maxBytes: 25 * 1024 * 1024,
    allowedMime: new Set([
      'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',
      'application/pdf',
      'text/plain', 'text/csv',
      'application/msword',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'application/vnd.ms-excel',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      'application/zip',
      'video/mp4', 'video/quicktime',
    ]),
  },
  expense: {
    maxBytes: 10 * 1024 * 1024,
    allowedMime: new Set([
      'image/jpeg', 'image/png', 'image/heic', 'image/heif', 'application/pdf',
    ]),
  },
  ticket_message: {
    maxBytes: 25 * 1024 * 1024,
    allowedMime: new Set([
      'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',
      'application/pdf',
      'text/plain', 'text/csv',
      'application/msword',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'application/vnd.ms-excel',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      'application/zip',
      'video/mp4', 'video/quicktime',
    ]),
  },
  kb_article: {
    maxBytes: 25 * 1024 * 1024,
    allowedMime: new Set([
      'image/jpeg', 'image/png', 'image/gif', 'image/webp',
      'application/pdf',
    ]),
  },
  vendor: {
    maxBytes: 10 * 1024 * 1024,
    allowedMime: new Set([
      'application/pdf', 'image/jpeg', 'image/png',
    ]),
  },
}

const VALID_ENTITY_TYPES = new Set<AttachmentEntityType>(
  Object.keys(ENTITY_LIMITS) as AttachmentEntityType[],
)

// ── Permission map ─────────────────────────────────────────────────────────────

function attachmentUrlCacheKey(attachmentId: string): string {
  return `url:${attachmentId}`
}

function permissionsForEntityType(entityType: string): { read: string; write: string } {
  switch (entityType) {
    case 'task_message':
      return { read: 'tasks:read', write: 'tasks:write' }
    case 'ticket_message':
      return { read: 'tasks:read', write: 'tasks:write' }
    case 'expense':
      return { read: 'expenses:read', write: 'expenses:write' }
    case 'kb_article':
      return { read: 'kb:read', write: 'kb:write' }
    case 'vendor':
      return { read: 'invoices:read', write: 'invoices:write' }
    default:
      return { read: 'invoices:read', write: 'invoices:write' }
  }
}

// ── R2 helpers ─────────────────────────────────────────────────────────────────

/** Strip bidi overrides, null bytes, path separators; cap length. */
function sanitizeFilenameComponent(original: string): string {
  return original
    .replace(/[/\\]/g, '_')
    .replace(/\0/g, '')
    .replace(/[‪-‮⁦-⁩]/g, '')
    .slice(0, 200)
}

function attachmentR2Key(
  tenantId: string,
  entityType: string,
  entityId: string,
  uuid: string,
  filename: string,
): string {
  const safe = sanitizeFilenameComponent(filename)
  return `${tenantId}/${entityType}/${entityId}/${uuid}-${safe}`
}

function isUuid(value: string): boolean {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
}

function isElevatedRole(role: string | undefined): boolean {
  return role === 'ADMIN' || role === 'OWNER'
}

function listResponseFromRow(row: Awaited<ReturnType<typeof listAttachmentsForEntity>>[number]) {
  return {
    id: row.id,
    filename: row.filename,
    mime_type: row.mime_type,
    file_size_bytes: row.file_size_bytes,
    uploader_id: row.uploader_id,
    created_at: row.created_at,
  }
}

function uploadResponseFromRow(row: Awaited<ReturnType<typeof insertAttachment>>) {
  return {
    id: row.id,
    filename: row.filename,
    mime_type: row.mime_type,
    file_size_bytes: row.file_size_bytes,
    created_at: row.created_at,
  }
}

type UploadFile = {
  arrayBuffer(): Promise<ArrayBuffer>
  name: string
  type: string
  size: number
}

async function verifyAttachmentEntityOwnership(
  db: Parameters<typeof getExpense>[0],
  tenantId: string,
  entityType: AttachmentEntityType,
  entityId: string,
): Promise<boolean> {
  switch (entityType) {
    case 'expense':
      return !!(await getExpense(db, tenantId, entityId))
    case 'vendor':
      return !!(await getVendor(db, tenantId, entityId))
    case 'kb_article':
      return !!(await kbTenantQuery(db, tenantId).getArticleById(entityId))
    case 'task_message':
      return !!(await getTaskMessageById(db, tenantId, entityId))
    case 'ticket_message':
      return !!(await getTicketMessageById(db, tenantId, entityId))
    default:
      return false
  }
}

async function signAttachmentUrl(
  env: AppEnv['Bindings'],
  r2Key: string,
  filename: string,
): Promise<string> {
  return createSignedDownloadUrl(env, r2Key, {
    expiresIn: SIGNED_URL_TTL,
    filename,
  })
}

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

export const unifiedAttachmentsRouter = new Hono<AppEnv>()

unifiedAttachmentsRouter.use('*', authMiddleware)

function requireUserSession(c: Context<AppEnv>) {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return null
  }
  return session as SessionPayload & { tid: string }
}

// ── GET /api/attachments/:id/url ──────────────────────────────────────────────

/**
 * Return a fresh 1-hour signed URL for a single attachment.
 * Result is cached in ATTACHMENT_URL_CACHE KV for 55 minutes so repeat
 * requests are served from cache without re-signing.
 */
unifiedAttachmentsRouter.get(
  '/:id/url',
  async (c) => {
    const session = requireUserSession(c)
    if (!session) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    const id = c.req.param('id')
    const db = c.get('db')
    const att = await getAttachment(db, session.tid, id)

    if (!att) {
      return c.json({ error: 'ENTITY_NOT_FOUND' }, 404)
    }

    const { read: readPermission } = permissionsForEntityType(att.entity_type)
    if (!session.permissions.includes(readPermission)) {
      return c.json({ error: 'FORBIDDEN' }, 403)
    }

    const cacheKey = attachmentUrlCacheKey(id)

    // KV cache check — only after authz; key is user-scoped
    if (c.env.ATTACHMENT_URL_CACHE) {
      const cached = await c.env.ATTACHMENT_URL_CACHE.get(cacheKey).catch(() => null)
      if (cached) {
        const parsed = JSON.parse(cached) as { url: string; expires_at: string }
        return c.json({ url: parsed.url, expires_at: parsed.expires_at })
      }
    }

    const expiresAt = new Date(Date.now() + SIGNED_URL_TTL * 1000).toISOString()
    let url: string
    try {
      url = await signAttachmentUrl(c.env, att.r2_key, att.filename)
    } catch {
      return c.json({ error: 'Failed to generate download URL' }, 500)
    }

    // Write to KV
    if (c.env.ATTACHMENT_URL_CACHE) {
      await c.env.ATTACHMENT_URL_CACHE.put(
        cacheKey,
        JSON.stringify({ url, expires_at: expiresAt }),
        { expirationTtl: KV_CACHE_TTL_SEC },
      ).catch(() => {})
    }

    return c.json({ url, expires_at: expiresAt })
  },
)

async function listAttachmentsHandler(
  c: Context<AppEnv>,
  input?: { entityType?: string; entityId?: string },
) {
  const session = requireUserSession(c)
  if (!session) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const entityType = input?.entityType ?? c.req.query('entity_type')
  const entityId = input?.entityId ?? c.req.query('entity_id')

  if (!entityType || !entityId) {
    return c.json({ error: 'MISSING_FIELD' }, 400)
  }

  if (!VALID_ENTITY_TYPES.has(entityType as AttachmentEntityType)) {
    return c.json({ error: 'INVALID_ENTITY_TYPE' }, 400)
  }

  if (!isUuid(entityId)) {
    return c.json({ error: 'MISSING_FIELD' }, 400)
  }

  const { read: readPermission } = permissionsForEntityType(entityType)
  if (!session.permissions.includes(readPermission)) {
    return c.json({ error: 'FORBIDDEN' }, 403)
  }

  const db = c.get('db')
  const owned = await verifyAttachmentEntityOwnership(
    db,
    session.tid,
    entityType as AttachmentEntityType,
    entityId,
  )
  if (!owned) {
    return c.json({ error: 'ENTITY_NOT_FOUND' }, 404)
  }

  const rows = await listAttachmentsForEntity(
    db,
    session.tid,
    entityType as AttachmentEntityType,
    entityId,
  )

  rows.sort((a, b) => a.created_at.localeCompare(b.created_at))

  return c.json({ attachments: rows.map(listResponseFromRow) })
}

unifiedAttachmentsRouter.get('/', (c) => listAttachmentsHandler(c))

unifiedAttachmentsRouter.get('/:entityType/:entityId', (c) =>
  listAttachmentsHandler(c, {
    entityType: c.req.param('entityType'),
    entityId: c.req.param('entityId'),
  }))

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

async function uploadAttachmentHandler(
  c: Context<AppEnv>,
  pathParams?: { entityType?: string; entityId?: string },
) {
  const session = requireUserSession(c)
  if (!session) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const formData = await c.req.formData().catch(() => null)
  if (!formData) {
    return c.json({ error: 'MISSING_FIELD' }, 400)
  }

  const entityType = pathParams?.entityType ?? formData.get('entity_type')
  const entityId = pathParams?.entityId ?? formData.get('entity_id')
  const fileEntry = formData.get('file')

  if (!fileEntry || !entityType || !entityId) {
    return c.json({ error: 'MISSING_FIELD' }, 400)
  }
  if (typeof entityType !== 'string' || typeof entityId !== 'string') {
    return c.json({ error: 'MISSING_FIELD' }, 400)
  }
  const uploadFile = fileEntry as Partial<UploadFile> | string
  if (
    typeof uploadFile === 'string' ||
    typeof uploadFile !== 'object' ||
    typeof uploadFile.arrayBuffer !== 'function' ||
    typeof uploadFile.name !== 'string' ||
    typeof uploadFile.type !== 'string' ||
    typeof uploadFile.size !== 'number'
  ) {
    return c.json({ error: 'MISSING_FIELD' }, 400)
  }
  if (!VALID_ENTITY_TYPES.has(entityType as AttachmentEntityType)) {
    return c.json({ error: 'INVALID_ENTITY_TYPE' }, 400)
  }
  if (!isUuid(entityId)) {
    return c.json({ error: 'MISSING_FIELD' }, 400)
  }

  const { write: writePermission } = permissionsForEntityType(entityType)
  if (!session.permissions.includes(writePermission)) {
    return c.json({ error: 'FORBIDDEN' }, 403)
  }

  const db = c.get('db')
  const owned = await verifyAttachmentEntityOwnership(
    db,
    session.tid,
    entityType as AttachmentEntityType,
    entityId,
  )
  if (!owned) {
    return c.json({ error: 'ENTITY_NOT_FOUND' }, 404)
  }

  const file = uploadFile as UploadFile
  const limits = ENTITY_LIMITS[entityType as AttachmentEntityType]

  if (file.size > GLOBAL_MAX_BYTES) {
    return c.body(null, 413)
  }
  if (file.size > limits.maxBytes) {
    return c.json({ error: 'FILE_TOO_LARGE' }, 400)
  }

  const mimeType = file.type || 'application/octet-stream'
  if (!limits.allowedMime.has(mimeType)) {
    return c.json({ error: 'MIME_NOT_ALLOWED' }, 400)
  }

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

  const tenantId = session.tid
  const fileUuid = crypto.randomUUID()
  const r2Key = attachmentR2Key(tenantId, entityType, entityId, fileUuid, file.name)

  try {
    await c.env.STORAGE.put(r2Key, fileBytes, {
      httpMetadata: { contentType: mimeType },
      customMetadata: { tenantId, entityType, entityId, originalName: file.name },
    })
  } catch {
    return c.json({ error: 'Failed to upload file' }, 500)
  }

  let attRow
  try {
    attRow = await insertAttachment(db, tenantId, {
      uploader_id: session.sub,
      entity_type: entityType as AttachmentEntityType,
      entity_id: entityId,
      filename: file.name,
      mime_type: mimeType,
      file_size_bytes: file.size,
      r2_key: r2Key,
    })
  } catch {
    await c.env.STORAGE.delete(r2Key).catch(() => {})
    return c.json({ error: 'Failed to save attachment metadata' }, 500)
  }

  return c.json(uploadResponseFromRow(attRow), 201)
}

unifiedAttachmentsRouter.post('/', (c) => uploadAttachmentHandler(c))

unifiedAttachmentsRouter.post('/:entityType/:entityId', (c) =>
  uploadAttachmentHandler(c, {
    entityType: c.req.param('entityType'),
    entityId: c.req.param('entityId'),
  }))

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

unifiedAttachmentsRouter.delete(
  '/:id',
  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 att = await getAttachment(db, session.tid, c.req.param('id'))

    if (!att) {
      return c.json({ error: 'ENTITY_NOT_FOUND' }, 404)
    }

    // Uploader can always delete own; others need elevated tenant role.
    const isUploader = att.uploader_id === session.sub
    if (!isUploader && !isElevatedRole(session.role)) {
      return c.json({ error: 'FORBIDDEN' }, 403)
    }

    const deleted = await softDeleteAttachment(db, session.tid, att.id, session.sub)
    if (!deleted) {
      return c.json({ error: 'Delete failed' }, 500)
    }

    if (c.env.ATTACHMENT_URL_CACHE) {
      await c.env.ATTACHMENT_URL_CACHE.delete(attachmentUrlCacheKey(att.id)).catch(() => {})
    }

    await c.env.QUEUE.send({
      type: 'r2.delete',
      r2Key: deleted.r2_key,
      tenantId: session.tid,
    }).catch(() => {
    })

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