import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { router, protectedProcedure, adminProcedure } from '../trpc'
import { getPresignedUrl, generateObjectKey, deleteObject, BUCKET_NAME } from '@/lib/minio'
import { logAudit } from '../utils/audit'

export const fileRouter = router({
  /**
   * Get pre-signed download URL
   * Checks that the user is authorized to access the file's project
   */
  getDownloadUrl: protectedProcedure
    .input(
      z.object({
        bucket: z.string(),
        objectKey: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)

      if (!isAdmin) {
        // Find the file record to get the project and round info
        const file = await ctx.prisma.projectFile.findFirst({
          where: { bucket: input.bucket, objectKey: input.objectKey },
          select: {
            projectId: true,
            roundId: true,
            round: { select: { programId: true, sortOrder: true } },
          },
        })

        if (!file) {
          throw new TRPCError({
            code: 'NOT_FOUND',
            message: 'File not found',
          })
        }

        // Check if user is assigned as jury, mentor, or team member for this project
        const [juryAssignment, mentorAssignment, teamMembership] = await Promise.all([
          ctx.prisma.assignment.findFirst({
            where: { userId: ctx.user.id, projectId: file.projectId },
            select: { id: true, roundId: true },
          }),
          ctx.prisma.mentorAssignment.findFirst({
            where: { mentorId: ctx.user.id, projectId: file.projectId },
            select: { id: true },
          }),
          ctx.prisma.project.findFirst({
            where: {
              id: file.projectId,
              OR: [
                { submittedByUserId: ctx.user.id },
                { teamMembers: { some: { userId: ctx.user.id } } },
              ],
            },
            select: { id: true },
          }),
        ])

        if (!juryAssignment && !mentorAssignment && !teamMembership) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to this file',
          })
        }

        // For jury members, verify round-scoped access:
        // File must belong to the jury's assigned round or a prior round in the same program
        if (juryAssignment && !mentorAssignment && !teamMembership && file.roundId && file.round) {
          const assignedRound = await ctx.prisma.round.findUnique({
            where: { id: juryAssignment.roundId },
            select: { programId: true, sortOrder: true },
          })

          if (assignedRound) {
            const sameProgram = assignedRound.programId === file.round.programId
            const priorOrSameRound = file.round.sortOrder <= assignedRound.sortOrder

            if (!sameProgram || !priorOrSameRound) {
              throw new TRPCError({
                code: 'FORBIDDEN',
                message: 'You do not have access to this file',
              })
            }
          }
        }
      }

      const url = await getPresignedUrl(input.bucket, input.objectKey, 'GET', 900) // 15 min

      // Log file access
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'FILE_DOWNLOADED',
        entityType: 'ProjectFile',
        detailsJson: { bucket: input.bucket, objectKey: input.objectKey },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { url }
    }),

  /**
   * Get pre-signed upload URL (admin only)
   */
  getUploadUrl: adminProcedure
    .input(
      z.object({
        projectId: z.string(),
        fileName: z.string(),
        fileType: z.enum(['EXEC_SUMMARY', 'PRESENTATION', 'VIDEO', 'OTHER']),
        mimeType: z.string(),
        size: z.number().int().positive(),
        roundId: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Block dangerous file extensions
      const dangerousExtensions = ['.exe', '.sh', '.bat', '.cmd', '.ps1', '.php', '.jsp', '.cgi', '.dll', '.msi']
      const ext = input.fileName.toLowerCase().slice(input.fileName.lastIndexOf('.'))
      if (dangerousExtensions.includes(ext)) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: `File type "${ext}" is not allowed`,
        })
      }

      // Calculate isLate flag if roundId is provided
      let isLate = false
      if (input.roundId) {
        const round = await ctx.prisma.round.findUnique({
          where: { id: input.roundId },
          select: { votingEndAt: true },
        })

        if (round?.votingEndAt) {
          isLate = new Date() > round.votingEndAt
        }
      }

      const bucket = BUCKET_NAME
      const objectKey = generateObjectKey(input.projectId, input.fileName)

      const uploadUrl = await getPresignedUrl(bucket, objectKey, 'PUT', 3600) // 1 hour

      // Create file record
      const file = await ctx.prisma.projectFile.create({
        data: {
          projectId: input.projectId,
          fileType: input.fileType,
          fileName: input.fileName,
          mimeType: input.mimeType,
          size: input.size,
          bucket,
          objectKey,
          roundId: input.roundId,
          isLate,
        },
      })

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPLOAD_FILE',
        entityType: 'ProjectFile',
        entityId: file.id,
        detailsJson: {
          projectId: input.projectId,
          fileName: input.fileName,
          fileType: input.fileType,
          roundId: input.roundId,
          isLate,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return {
        uploadUrl,
        file,
      }
    }),

  /**
   * Confirm file upload completed
   */
  confirmUpload: adminProcedure
    .input(z.object({ fileId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      // In the future, we could verify the file exists in MinIO
      // For now, just return the file
      return ctx.prisma.projectFile.findUniqueOrThrow({
        where: { id: input.fileId },
      })
    }),

  /**
   * Delete file (admin only)
   */
  delete: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const file = await ctx.prisma.projectFile.delete({
        where: { id: input.id },
      })

      // Delete actual storage object (best-effort, don't fail the operation)
      try {
        if (file.bucket && file.objectKey) {
          await deleteObject(file.bucket, file.objectKey)
        }
      } catch (error) {
        console.error(`[File] Failed to delete storage object ${file.objectKey}:`, error)
      }

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE_FILE',
        entityType: 'ProjectFile',
        entityId: input.id,
        detailsJson: {
          fileName: file.fileName,
          bucket: file.bucket,
          objectKey: file.objectKey,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return file
    }),

  /**
   * List files for a project
   * Checks that the user is authorized to view the project's files
   */
  listByProject: protectedProcedure
    .input(z.object({
      projectId: z.string(),
      roundId: z.string().optional(),
    }))
    .query(async ({ ctx, input }) => {
      const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)

      if (!isAdmin) {
        const [juryAssignment, mentorAssignment, teamMembership] = await Promise.all([
          ctx.prisma.assignment.findFirst({
            where: { userId: ctx.user.id, projectId: input.projectId },
            select: { id: true },
          }),
          ctx.prisma.mentorAssignment.findFirst({
            where: { mentorId: ctx.user.id, projectId: input.projectId },
            select: { id: true },
          }),
          ctx.prisma.project.findFirst({
            where: {
              id: input.projectId,
              OR: [
                { submittedByUserId: ctx.user.id },
                { teamMembers: { some: { userId: ctx.user.id } } },
              ],
            },
            select: { id: true },
          }),
        ])

        if (!juryAssignment && !mentorAssignment && !teamMembership) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to this project\'s files',
          })
        }
      }

      const where: Record<string, unknown> = { projectId: input.projectId }
      if (input.roundId) {
        where.roundId = input.roundId
      }

      return ctx.prisma.projectFile.findMany({
        where,
        include: {
          round: { select: { id: true, name: true, sortOrder: true } },
        },
        orderBy: [{ fileType: 'asc' }, { createdAt: 'asc' }],
      })
    }),

  /**
   * List files for a project grouped by round
   * Returns files for the specified round + all prior rounds in the same program
   */
  listByProjectForRound: protectedProcedure
    .input(z.object({
      projectId: z.string(),
      roundId: z.string(),
    }))
    .query(async ({ ctx, input }) => {
      const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)

      if (!isAdmin) {
        const [juryAssignment, mentorAssignment, teamMembership] = await Promise.all([
          ctx.prisma.assignment.findFirst({
            where: { userId: ctx.user.id, projectId: input.projectId },
            select: { id: true, roundId: true },
          }),
          ctx.prisma.mentorAssignment.findFirst({
            where: { mentorId: ctx.user.id, projectId: input.projectId },
            select: { id: true },
          }),
          ctx.prisma.project.findFirst({
            where: {
              id: input.projectId,
              OR: [
                { submittedByUserId: ctx.user.id },
                { teamMembers: { some: { userId: ctx.user.id } } },
              ],
            },
            select: { id: true },
          }),
        ])

        if (!juryAssignment && !mentorAssignment && !teamMembership) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to this project\'s files',
          })
        }
      }

      // Get the target round with its program and sortOrder
      const targetRound = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { programId: true, sortOrder: true },
      })

      // Get all rounds in the same program with sortOrder <= target
      const eligibleRounds = await ctx.prisma.round.findMany({
        where: {
          programId: targetRound.programId,
          sortOrder: { lte: targetRound.sortOrder },
        },
        select: { id: true, name: true, sortOrder: true },
        orderBy: { sortOrder: 'asc' },
      })

      const eligibleRoundIds = eligibleRounds.map((r) => r.id)

      // Get files for these rounds (or files with no roundId)
      const files = await ctx.prisma.projectFile.findMany({
        where: {
          projectId: input.projectId,
          OR: [
            { roundId: { in: eligibleRoundIds } },
            { roundId: null },
          ],
        },
        include: {
          round: { select: { id: true, name: true, sortOrder: true } },
        },
        orderBy: [{ createdAt: 'asc' }],
      })

      // Group by round
      const grouped: Array<{
        roundId: string | null
        roundName: string
        sortOrder: number
        files: typeof files
      }> = []

      // Add "General" group for files with no round
      const generalFiles = files.filter((f) => !f.roundId)
      if (generalFiles.length > 0) {
        grouped.push({
          roundId: null,
          roundName: 'General',
          sortOrder: -1,
          files: generalFiles,
        })
      }

      // Add groups for each round
      for (const round of eligibleRounds) {
        const roundFiles = files.filter((f) => f.roundId === round.id)
        if (roundFiles.length > 0) {
          grouped.push({
            roundId: round.id,
            roundName: round.name,
            sortOrder: round.sortOrder,
            files: roundFiles,
          })
        }
      }

      return grouped
    }),

  /**
   * Replace a file with a new version
   */
  replaceFile: protectedProcedure
    .input(
      z.object({
        projectId: z.string(),
        oldFileId: z.string(),
        fileName: z.string(),
        fileType: z.enum(['EXEC_SUMMARY', 'PRESENTATION', 'VIDEO', 'OTHER']),
        mimeType: z.string(),
        size: z.number().int().positive(),
        bucket: z.string(),
        objectKey: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)

      if (!isAdmin) {
        // Check user has access to the project (assigned or team member)
        const [assignment, mentorAssignment, teamMembership] = await Promise.all([
          ctx.prisma.assignment.findFirst({
            where: { userId: ctx.user.id, projectId: input.projectId },
            select: { id: true },
          }),
          ctx.prisma.mentorAssignment.findFirst({
            where: { mentorId: ctx.user.id, projectId: input.projectId },
            select: { id: true },
          }),
          ctx.prisma.project.findFirst({
            where: {
              id: input.projectId,
              OR: [
                { submittedByUserId: ctx.user.id },
                { teamMembers: { some: { userId: ctx.user.id } } },
              ],
            },
            select: { id: true },
          }),
        ])

        if (!assignment && !mentorAssignment && !teamMembership) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to replace files for this project',
          })
        }
      }

      // Get the old file to read its version
      const oldFile = await ctx.prisma.projectFile.findUniqueOrThrow({
        where: { id: input.oldFileId },
        select: { id: true, version: true, projectId: true },
      })

      if (oldFile.projectId !== input.projectId) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'File does not belong to the specified project',
        })
      }

      // Create new file and update old file in a transaction
      const result = await ctx.prisma.$transaction(async (tx) => {
        const newFile = await tx.projectFile.create({
          data: {
            projectId: input.projectId,
            fileName: input.fileName,
            fileType: input.fileType,
            mimeType: input.mimeType,
            size: input.size,
            bucket: input.bucket,
            objectKey: input.objectKey,
            version: oldFile.version + 1,
          },
        })

        // Link old file to new file
        await tx.projectFile.update({
          where: { id: input.oldFileId },
          data: { replacedById: newFile.id },
        })

        await logAudit({
          prisma: tx,
          userId: ctx.user.id,
          action: 'REPLACE_FILE',
          entityType: 'ProjectFile',
          entityId: newFile.id,
          detailsJson: {
            projectId: input.projectId,
            oldFileId: input.oldFileId,
            oldVersion: oldFile.version,
            newVersion: newFile.version,
            fileName: input.fileName,
          },
          ipAddress: ctx.ip,
          userAgent: ctx.userAgent,
        })

        return newFile
      })

      return result
    }),

  /**
   * Get version history for a file
   */
  getVersionHistory: protectedProcedure
    .input(z.object({ fileId: z.string() }))
    .query(async ({ ctx, input }) => {
      // Find the requested file
      const file = await ctx.prisma.projectFile.findUniqueOrThrow({
        where: { id: input.fileId },
        select: {
          id: true,
          projectId: true,
          fileName: true,
          fileType: true,
          mimeType: true,
          size: true,
          bucket: true,
          objectKey: true,
          version: true,
          replacedById: true,
          createdAt: true,
        },
      })

      // Walk backwards: find all prior versions by following replacedById chains
      // First, collect ALL files for this project with the same fileType to find the chain
      const allRelatedFiles = await ctx.prisma.projectFile.findMany({
        where: { projectId: file.projectId },
        select: {
          id: true,
          fileName: true,
          fileType: true,
          mimeType: true,
          size: true,
          bucket: true,
          objectKey: true,
          version: true,
          replacedById: true,
          createdAt: true,
        },
        orderBy: { version: 'asc' },
      })

      // Build a chain map: fileId -> file that replaced it
      const replacedByMap = new Map(
        allRelatedFiles.filter((f) => f.replacedById).map((f) => [f.replacedById!, f.id])
      )

      // Walk from the current file backwards through replacedById to find all versions in chain
      const versions: typeof allRelatedFiles = []

      // Find the root of this version chain (walk backwards)
      let currentId: string | undefined = input.fileId
      const visited = new Set<string>()
      while (currentId && !visited.has(currentId)) {
        visited.add(currentId)
        const prevId = replacedByMap.get(currentId)
        if (prevId) {
          currentId = prevId
        } else {
          break // reached root
        }
      }

      // Now walk forward from root
      let walkId: string | undefined = currentId
      const fileMap = new Map(allRelatedFiles.map((f) => [f.id, f]))
      const forwardVisited = new Set<string>()
      while (walkId && !forwardVisited.has(walkId)) {
        forwardVisited.add(walkId)
        const f = fileMap.get(walkId)
        if (f) {
          versions.push(f)
          walkId = f.replacedById ?? undefined
        } else {
          break
        }
      }

      return versions
    }),

  /**
   * Get bulk download URLs for project files
   */
  getBulkDownloadUrls: protectedProcedure
    .input(
      z.object({
        projectId: z.string(),
        fileIds: z.array(z.string()).optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)

      if (!isAdmin) {
        const [assignment, mentorAssignment, teamMembership] = await Promise.all([
          ctx.prisma.assignment.findFirst({
            where: { userId: ctx.user.id, projectId: input.projectId },
            select: { id: true },
          }),
          ctx.prisma.mentorAssignment.findFirst({
            where: { mentorId: ctx.user.id, projectId: input.projectId },
            select: { id: true },
          }),
          ctx.prisma.project.findFirst({
            where: {
              id: input.projectId,
              OR: [
                { submittedByUserId: ctx.user.id },
                { teamMembers: { some: { userId: ctx.user.id } } },
              ],
            },
            select: { id: true },
          }),
        ])

        if (!assignment && !mentorAssignment && !teamMembership) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to this project\'s files',
          })
        }
      }

      // Get files
      const where: Record<string, unknown> = { projectId: input.projectId }
      if (input.fileIds && input.fileIds.length > 0) {
        where.id = { in: input.fileIds }
      }

      const files = await ctx.prisma.projectFile.findMany({
        where,
        select: {
          id: true,
          fileName: true,
          bucket: true,
          objectKey: true,
        },
      })

      // Generate signed URLs for each file
      const results = await Promise.all(
        files.map(async (file) => {
          const downloadUrl = await getPresignedUrl(file.bucket, file.objectKey, 'GET', 900)
          return {
            fileId: file.id,
            fileName: file.fileName,
            downloadUrl,
          }
        })
      )

      return results
    }),
})
