import type { FastifyPluginAsync, FastifyRequest } from 'fastify'
import { webdavBasicAuth } from '../../plugins/webdav-auth.js'
import { WebDavService } from '../../services/webdav.service.js'
import { StoragePermissionService, StorageFolderService, StorageFileService } from '../../services/storage.service.js'
import { buildMultistatus, buildLockResponse, type DavResource } from '../../utils/webdav-xml.js'
import { randomUUID } from 'crypto'

// In-memory lock store for WebDAV LOCK/UNLOCK
const locks = new Map<string, { token: string; owner: string; expires: number }>()

const DAV_METHODS = 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK'
const WEBDAV_PREFIX = '/webdav'

export const webdavRoutes: FastifyPluginAsync = async (app) => {
  // Parse all request bodies as raw buffers for WebDAV, with size limit
  const MAX_UPLOAD_SIZE = 100 * 1024 * 1024 // 100MB
  app.addContentTypeParser('*', function (_request, payload, done) {
    const chunks: Buffer[] = []
    let totalSize = 0
    payload.on('data', (chunk: Buffer) => {
      totalSize += chunk.length
      if (totalSize > MAX_UPLOAD_SIZE) {
        payload.destroy()
        done(new Error('Payload too large'), undefined)
        return
      }
      chunks.push(chunk)
    })
    payload.on('end', () => done(null, Buffer.concat(chunks)))
    payload.on('error', done)
  })

  const auth = webdavBasicAuth(app)

  // Helper: normalize request path relative to /webdav
  function getResourcePath(request: FastifyRequest): string {
    const url = request.url.split('?')[0]
    let path = url.startsWith(WEBDAV_PREFIX) ? url.slice(WEBDAV_PREFIX.length) : url
    // Remove trailing slash for consistency (except root)
    if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1)
    return path || '/'
  }

  // --- OPTIONS ---
  app.route({
    method: 'OPTIONS',
    url: '/*',
    handler: async (_request, reply) => {
      reply
        .header('Allow', DAV_METHODS)
        .header('DAV', '1, 2')
        .header('MS-Author-Via', 'DAV')
        .status(200)
        .send('')
    },
  })

  // Also handle OPTIONS on root /webdav
  app.route({
    method: 'OPTIONS',
    url: '/',
    handler: async (_request, reply) => {
      reply
        .header('Allow', DAV_METHODS)
        .header('DAV', '1, 2')
        .header('MS-Author-Via', 'DAV')
        .status(200)
        .send('')
    },
  })

  // --- PROPFIND ---
  app.route({
    method: 'PROPFIND' as any,
    url: '/*',
    preHandler: auth,
    handler: async (request, reply) => {
      const resourcePath = getResourcePath(request)
      const depth = (request.headers['depth'] as string) ?? '1'
      const resolved = await WebDavService.resolvePath(app.db, resourcePath)

      if (resolved.type === null) {
        return reply.status(404).send('Not Found')
      }

      const resources: DavResource[] = []
      const basePath = `${WEBDAV_PREFIX}${resourcePath === '/' ? '/' : resourcePath + '/'}`

      if (resolved.type === 'root') {
        // Root collection
        resources.push({
          href: `${WEBDAV_PREFIX}/`,
          isCollection: true,
          displayName: 'Files',
        })
        if (depth !== '0') {
          const children = await WebDavService.listChildren(app.db, null, `${WEBDAV_PREFIX}/`)
          // Filter to only folders the user has access to
          const filtered = []
          for (const child of children) {
            if (!child.isCollection) continue // root has no files
            // Extract folder name from href to resolve access
            const folderName = decodeURIComponent(child.href.replace(`${WEBDAV_PREFIX}/`, '').replace(/\/$/, ''))
            const resolved = await WebDavService.resolvePath(app.db, '/' + folderName)
            if (resolved.type === 'folder' && resolved.folder) {
              const level = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
              if (level) filtered.push(child)
            }
          }
          resources.push(...filtered)
        }
      } else if (resolved.type === 'folder' && resolved.folder) {
        // Check access — traverse or higher lets you see the folder
        const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
        if (!accessLevel) return reply.status(403).send('Access Denied')

        resources.push({
          href: basePath.slice(0, -1) + '/',
          isCollection: true,
          displayName: resolved.folder.name,
          lastModified: resolved.folder.updatedAt ? new Date(resolved.folder.updatedAt) : undefined,
          createdAt: resolved.folder.createdAt ? new Date(resolved.folder.createdAt) : undefined,
        })
        if (depth !== '0') {
          // traverse: show subfolders only, no files. view+: show everything
          const children = await WebDavService.listChildren(app.db, resolved.folder.id, basePath, accessLevel === 'traverse')
          resources.push(...children)
        }
      } else if (resolved.type === 'file' && resolved.file && resolved.folder) {
        // File access requires at least view (not traverse)
        const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
        if (!accessLevel || accessLevel === 'traverse') return reply.status(403).send('Access Denied')

        resources.push({
          href: `${WEBDAV_PREFIX}${resourcePath}`,
          isCollection: false,
          displayName: resolved.file.filename,
          contentType: resolved.file.contentType,
          contentLength: resolved.file.sizeBytes,
          lastModified: resolved.file.createdAt ? new Date(resolved.file.createdAt) : undefined,
          createdAt: resolved.file.createdAt ? new Date(resolved.file.createdAt) : undefined,
          etag: resolved.file.id,
        })
      }

      const xml = buildMultistatus(resources)
      return reply
        .header('Content-Type', 'application/xml; charset=utf-8')
        .status(207)
        .send(xml)
    },
  })

  // PROPFIND on root
  app.route({
    method: 'PROPFIND' as any,
    url: '/',
    preHandler: auth,
    handler: async (request, reply) => {
      const depth = (request.headers['depth'] as string) ?? '1'
      const resources: DavResource[] = [{
        href: `${WEBDAV_PREFIX}/`,
        isCollection: true,
        displayName: 'Files',
      }]

      if (depth !== '0') {
        const children = await WebDavService.listChildren(app.db, null, `${WEBDAV_PREFIX}/`)
        const filtered = []
        for (const child of children) {
          if (!child.isCollection) continue
          const folderName = decodeURIComponent(child.href.replace(`${WEBDAV_PREFIX}/`, '').replace(/\/$/, ''))
          const childResolved = await WebDavService.resolvePath(app.db, '/' + folderName)
          if (childResolved.type === 'folder' && childResolved.folder) {
            const level = await StoragePermissionService.getAccessLevel(app.db, childResolved.folder.id, request.user.id)
            if (level) filtered.push(child)
          }
        }
        resources.push(...filtered)
      }

      const xml = buildMultistatus(resources)
      return reply
        .header('Content-Type', 'application/xml; charset=utf-8')
        .status(207)
        .send(xml)
    },
  })

  // --- GET ---
  app.get('/*', { preHandler: auth }, async (request, reply) => {
    const resourcePath = getResourcePath(request)
    const resolved = await WebDavService.resolvePath(app.db, resourcePath)

    if (resolved.type === 'file' && resolved.file && resolved.folder) {
      const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
      if (!accessLevel || accessLevel === 'traverse') return reply.status(403).send('Access Denied')

      const data = await app.storage.get(resolved.file.path)
      return reply
        .header('Content-Type', resolved.file.contentType)
        .header('Content-Length', resolved.file.sizeBytes)
        .header('ETag', `"${resolved.file.id}"`)
        .send(data)
    }

    if (resolved.type === 'folder' || resolved.type === 'root') {
      // Return a simple HTML listing for browsers
      return reply.status(200).header('Content-Type', 'text/plain').send('This is a WebDAV collection. Use a WebDAV client to browse.')
    }

    return reply.status(404).send('Not Found')
  })

  // HEAD is auto-generated by Fastify for GET routes

  // --- PUT (upload/overwrite file) ---
  app.put('/*', { preHandler: auth }, async (request, reply) => {
    const resourcePath = getResourcePath(request)
    const { parentPath, name } = WebDavService.parseParentAndName(resourcePath)

    if (!name) return reply.status(400).send('Invalid path')

    // Check if resource is locked by another user
    const lockCheck = checkLock(resourcePath, request.user.id)
    if (lockCheck.byOther) return reply.status(423).send('Resource is locked by another user')

    // Resolve parent to find the folder
    const parentResolved = await WebDavService.resolvePath(app.db, parentPath)
    if (parentResolved.type !== 'folder' || !parentResolved.folder) {
      return reply.status(409).send('Parent folder not found')
    }

    // Check edit permission — traverse and view are not enough
    const hasEdit = await StoragePermissionService.hasAccess(app.db, parentResolved.folder.id, request.user.id, 'edit')
    if (!hasEdit) {
      return reply.status(403).send('No edit access')
    }

    // Body is a Buffer from our content type parser
    const data = Buffer.isBuffer(request.body) ? request.body : Buffer.from(String(request.body ?? ''))

    // Guess content type from extension
    const contentType = guessContentType(name)

    // Check if file already exists (overwrite)
    const existingResolved = await WebDavService.resolvePath(app.db, resourcePath)
    if (existingResolved.type === 'file' && existingResolved.file) {
      await StorageFileService.delete(app.db, app.storage, existingResolved.file.id)
    }

    const file = await StorageFileService.upload(app.db, app.storage, {
      folderId: parentResolved.folder.id,
      data,
      filename: name,
      contentType,
      uploadedBy: request.user.id,
    })

    return reply.status(existingResolved.type === 'file' ? 204 : 201).header('ETag', `"${file.id}"`).send('')
  })

  // --- DELETE ---
  app.delete('/*', { preHandler: auth }, async (request, reply) => {
    const resourcePath = getResourcePath(request)

    // Check if resource is locked by another user
    const lockCheck = checkLock(resourcePath, request.user.id)
    if (lockCheck.byOther) return reply.status(423).send('Resource is locked by another user')

    const resolved = await WebDavService.resolvePath(app.db, resourcePath)

    if (resolved.type === 'file' && resolved.file && resolved.folder) {
      const hasEdit = await StoragePermissionService.hasAccess(app.db, resolved.folder.id, request.user.id, 'edit')
      if (!hasEdit) return reply.status(403).send('No edit access')
      await StorageFileService.delete(app.db, app.storage, resolved.file.id)
      return reply.status(204).send('')
    }

    if (resolved.type === 'folder' && resolved.folder) {
      const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
      if (accessLevel !== 'admin') return reply.status(403).send('Admin access required')
      await StorageFolderService.delete(app.db, resolved.folder.id)
      return reply.status(204).send('')
    }

    return reply.status(404).send('Not Found')
  })

  // --- MKCOL (create folder) ---
  app.route({
    method: 'MKCOL' as any,
    url: '/*',
    preHandler: auth,
    handler: async (request, reply) => {
      const resourcePath = getResourcePath(request)
      const { parentPath, name } = WebDavService.parseParentAndName(resourcePath)

      if (!name) return reply.status(400).send('Invalid path')

      // Check if resource already exists
      const existing = await WebDavService.resolvePath(app.db, resourcePath)
      if (existing.type !== null) return reply.status(405).send('Resource already exists')

      // Resolve parent
      let parentFolderId: string | undefined
      if (parentPath !== '/') {
        const parentResolved = await WebDavService.resolvePath(app.db, parentPath)
        if (parentResolved.type !== 'folder' || !parentResolved.folder) {
          return reply.status(409).send('Parent folder not found')
        }
        const hasEdit = await StoragePermissionService.hasAccess(app.db, parentResolved.folder.id, request.user.id, 'edit')
        if (!hasEdit) {
          return reply.status(403).send('No edit access to parent folder')
        }
        parentFolderId = parentResolved.folder.id
      } else {
        // Creating top-level folder requires files.admin permission
        if (!request.permissions.has('files.delete')) {
          return reply.status(403).send('Admin permission required to create top-level folders')
        }
      }

      await StorageFolderService.create(app.db, {
        name,
        parentId: parentFolderId,
        isPublic: false,
        createdBy: request.user.id,
      })

      return reply.status(201).send('')
    },
  })

  // --- COPY ---
  app.route({
    method: 'COPY' as any,
    url: '/*',
    preHandler: auth,
    handler: async (request, reply) => {
      const resourcePath = getResourcePath(request)
      const destinationHeader = request.headers['destination'] as string
      if (!destinationHeader) return reply.status(400).send('Destination header required')

      const destUrl = new URL(destinationHeader, `http://${request.headers.host}`)
      let destPath = destUrl.pathname
      if (destPath.startsWith(WEBDAV_PREFIX)) destPath = destPath.slice(WEBDAV_PREFIX.length)
      if (destPath.length > 1 && destPath.endsWith('/')) destPath = destPath.slice(0, -1)
      destPath = destPath || '/'

      const resolved = await WebDavService.resolvePath(app.db, resourcePath)
      if (resolved.type !== 'file' || !resolved.file || !resolved.folder) {
        return reply.status(404).send('Source not found (only file copy supported)')
      }

      // Check view access on source (traverse is not enough to read file data)
      const hasSrcView = await StoragePermissionService.hasAccess(app.db, resolved.folder.id, request.user.id, 'view')
      if (!hasSrcView) return reply.status(403).send('No access to source')

      // Resolve destination parent
      const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)
      const destParent = await WebDavService.resolvePath(app.db, parentPath)
      if (destParent.type !== 'folder' || !destParent.folder) {
        return reply.status(409).send('Destination parent not found')
      }

      // Check edit access on destination
      const hasDestEdit = await StoragePermissionService.hasAccess(app.db, destParent.folder.id, request.user.id, 'edit')
      if (!hasDestEdit) return reply.status(403).send('No edit access to destination')

      // Copy: read source file data and upload to destination
      const fileData = await app.storage.get(resolved.file.path)
      const overwrite = (request.headers['overwrite'] as string)?.toUpperCase() !== 'F'

      // Check if destination exists
      const existingDest = await WebDavService.resolvePath(app.db, destPath)
      if (existingDest.type === 'file' && existingDest.file) {
        if (!overwrite) return reply.status(412).send('Destination exists')
        await StorageFileService.delete(app.db, app.storage, existingDest.file.id)
      }

      await StorageFileService.upload(app.db, app.storage, {
        folderId: destParent.folder.id,
        data: fileData,
        filename: destName || resolved.file.filename,
        contentType: resolved.file.contentType,
        uploadedBy: request.user.id,
      })

      return reply.status(existingDest.type === 'file' ? 204 : 201).send('')
    },
  })

  // --- MOVE ---
  app.route({
    method: 'MOVE' as any,
    url: '/*',
    preHandler: auth,
    handler: async (request, reply) => {
      const resourcePath = getResourcePath(request)
      const destinationHeader = request.headers['destination'] as string
      if (!destinationHeader) return reply.status(400).send('Destination header required')

      const destUrl = new URL(destinationHeader, `http://${request.headers.host}`)
      let destPath = destUrl.pathname
      if (destPath.startsWith(WEBDAV_PREFIX)) destPath = destPath.slice(WEBDAV_PREFIX.length)
      if (destPath.length > 1 && destPath.endsWith('/')) destPath = destPath.slice(0, -1)
      destPath = destPath || '/'

      const resolved = await WebDavService.resolvePath(app.db, resourcePath)

      if (resolved.type === 'file' && resolved.file && resolved.folder) {
        // Check edit access on source folder
        const hasSrcEdit = await StoragePermissionService.hasAccess(app.db, resolved.folder.id, request.user.id, 'edit')
        if (!hasSrcEdit) return reply.status(403).send('No edit access to source')

        // Resolve destination
        const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)
        const destParent = await WebDavService.resolvePath(app.db, parentPath)
        if (destParent.type !== 'folder' || !destParent.folder) {
          return reply.status(409).send('Destination parent not found')
        }
        const hasDestEdit = await StoragePermissionService.hasAccess(app.db, destParent.folder.id, request.user.id, 'edit')
        if (!hasDestEdit) return reply.status(403).send('No edit access to destination')

        // Move: copy data then delete source
        const overwrite = (request.headers['overwrite'] as string)?.toUpperCase() !== 'F'
        const existingDest = await WebDavService.resolvePath(app.db, destPath)
        if (existingDest.type === 'file' && existingDest.file) {
          if (!overwrite) return reply.status(412).send('Destination exists')
          await StorageFileService.delete(app.db, app.storage, existingDest.file.id)
        }

        // Read source, upload to dest, delete source
        const fileData = await app.storage.get(resolved.file.path)
        await StorageFileService.upload(app.db, app.storage, {
          folderId: destParent.folder.id,
          data: fileData,
          filename: destName || resolved.file.filename,
          contentType: resolved.file.contentType,
          uploadedBy: request.user.id,
        })
        await StorageFileService.delete(app.db, app.storage, resolved.file.id)

        return reply.status(existingDest.type === 'file' ? 204 : 201).send('')
      }

      if (resolved.type === 'folder' && resolved.folder) {
        // Folder move — update parentId and recalculate path
        const srcAccess = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
        if (srcAccess !== 'admin') return reply.status(403).send('Admin access required to move folder')

        const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)

        // If just renaming (same parent), update name
        const destParent = await WebDavService.resolvePath(app.db, parentPath)
        if (destParent.type === 'folder' || destParent.type === 'root') {
          const newParentId = destParent.type === 'folder' ? destParent.folder?.id : undefined
          if (newParentId) {
            const hasDestEdit = await StoragePermissionService.hasAccess(app.db, newParentId, request.user.id, 'edit')
            if (!hasDestEdit) return reply.status(403).send('No edit access to destination')
          }

          await StorageFolderService.update(app.db, resolved.folder.id, { name: destName })
          // TODO: If parent changed, also update parentId + materialized path
          return reply.status(201).send('')
        }

        return reply.status(409).send('Destination parent not found')
      }

      return reply.status(404).send('Not Found')
    },
  })

  // --- Lock helpers ---
  const MAX_LOCKS_PER_USER = 100
  const LOCK_TIMEOUT = 300 // 5 minutes

  function cleanExpiredLocks() {
    const now = Date.now()
    for (const [path, lock] of locks) {
      if (lock.expires < now) locks.delete(path)
    }
  }

  function checkLock(resourcePath: string, userId: string): { locked: boolean; byOther: boolean } {
    const lock = locks.get(resourcePath)
    if (!lock || lock.expires < Date.now()) return { locked: false, byOther: false }
    return { locked: true, byOther: lock.owner !== userId }
  }

  // --- LOCK ---
  app.route({
    method: 'LOCK' as any,
    url: '/*',
    preHandler: auth,
    handler: async (request, reply) => {
      const resourcePath = getResourcePath(request)
      cleanExpiredLocks()

      // Check if locked by another user
      const existing = locks.get(resourcePath)
      if (existing && existing.expires > Date.now() && existing.owner !== request.user.id) {
        return reply.status(423).send('Resource is locked by another user')
      }

      // Cap locks per user
      let userLockCount = 0
      for (const lock of locks.values()) {
        if (lock.owner === request.user.id && lock.expires > Date.now()) userLockCount++
      }
      if (userLockCount >= MAX_LOCKS_PER_USER) {
        return reply.status(400).send('Too many active locks')
      }

      const token = `opaquelocktoken:${randomUUID()}`
      locks.set(resourcePath, { token, owner: request.user.id, expires: Date.now() + LOCK_TIMEOUT * 1000 })

      const xml = buildLockResponse(token, request.user.id, LOCK_TIMEOUT)
      return reply
        .header('Content-Type', 'application/xml; charset=utf-8')
        .header('Lock-Token', `<${token}>`)
        .status(200)
        .send(xml)
    },
  })

  app.route({
    method: 'LOCK' as any,
    url: '/',
    preHandler: auth,
    handler: async (request, reply) => {
      cleanExpiredLocks()
      const token = `opaquelocktoken:${randomUUID()}`
      locks.set('/', { token, owner: request.user.id, expires: Date.now() + LOCK_TIMEOUT * 1000 })
      const xml = buildLockResponse(token, request.user.id, LOCK_TIMEOUT)
      return reply
        .header('Content-Type', 'application/xml; charset=utf-8')
        .header('Lock-Token', `<${token}>`)
        .status(200)
        .send(xml)
    },
  })

  // --- UNLOCK ---
  app.route({
    method: 'UNLOCK' as any,
    url: '/*',
    preHandler: auth,
    handler: async (request, reply) => {
      const resourcePath = getResourcePath(request)
      const lock = locks.get(resourcePath)
      // Only the lock owner can unlock
      if (lock && lock.owner !== request.user.id) {
        return reply.status(403).send('Not the lock owner')
      }
      locks.delete(resourcePath)
      return reply.status(204).send('')
    },
  })

  app.route({
    method: 'UNLOCK' as any,
    url: '/',
    preHandler: auth,
    handler: async (request, reply) => {
      const lock = locks.get('/')
      if (lock && lock.owner !== request.user.id) {
        return reply.status(403).send('Not the lock owner')
      }
      locks.delete('/')
      return reply.status(204).send('')
    },
  })

  // --- PROPPATCH (stub — accept but do nothing) ---
  app.route({
    method: 'PROPPATCH' as any,
    url: '/*',
    preHandler: auth,
    handler: async (request, reply) => {
      const resourcePath = getResourcePath(request)
      const resolved = await WebDavService.resolvePath(app.db, resourcePath)
      if (resolved.type === null) return reply.status(404).send('Not Found')

      // Return a minimal success response
      const xml = buildMultistatus([{
        href: `${WEBDAV_PREFIX}${resourcePath}`,
        isCollection: resolved.type === 'folder' || resolved.type === 'root',
        displayName: resolved.type === 'file' ? resolved.file!.filename : resolved.type === 'folder' ? resolved.folder!.name : 'Files',
      }])

      return reply
        .header('Content-Type', 'application/xml; charset=utf-8')
        .status(207)
        .send(xml)
    },
  })
}

function guessContentType(filename: string): string {
  const ext = filename.split('.').pop()?.toLowerCase()
  const map: Record<string, string> = {
    pdf: 'application/pdf',
    jpg: 'image/jpeg', jpeg: 'image/jpeg',
    png: 'image/png', webp: 'image/webp', gif: 'image/gif',
    doc: 'application/msword',
    docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    xls: 'application/vnd.ms-excel',
    xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    csv: 'text/csv', txt: 'text/plain',
    mp4: 'video/mp4', mp3: 'audio/mpeg',
    zip: 'application/zip',
    json: 'application/json',
    xml: 'application/xml',
    html: 'text/html', htm: 'text/html',
  }
  return map[ext ?? ''] ?? 'application/octet-stream'
}
