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

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(),
        forDownload: z.boolean().optional(),
        fileName: z.string().optional(),
        /** Why the URL is being requested — drives audit log granularity. */
        purpose: z.enum(['preview', 'open', 'download']).optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'].includes(ctx.user.role)

      if (!isAdminOrObserver) {
        const file = await ctx.prisma.projectFile.findFirst({
          where: { bucket: input.bucket, objectKey: input.objectKey },
          select: {
            projectId: true,
          },
        })

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

        const [juryAssignment, mentorAssignment, teamMembership, awardJurorAccess] = 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 },
          }),
          // Award jurors can access files for projects eligible in their awards
          ctx.prisma.awardJuror.findFirst({
            where: {
              userId: ctx.user.id,
              award: {
                status: { in: ['VOTING_OPEN', 'NOMINATIONS_OPEN'] },
                eligibilities: {
                  some: { projectId: file.projectId, eligible: true },
                },
              },
            },
            select: { id: true },
          }),
        ])

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

        if (juryAssignment && !mentorAssignment && !teamMembership && !awardJurorAccess) {
          const assignedRound = await ctx.prisma.round.findUnique({
            where: { id: juryAssignment.roundId },
            select: { competitionId: true, sortOrder: true },
          })

          if (assignedRound) {
            const priorOrCurrentRounds = await ctx.prisma.round.findMany({
              where: {
                competitionId: assignedRound.competitionId,
                sortOrder: { lte: assignedRound.sortOrder },
              },
              select: { id: true },
            })

            const roundIds = priorOrCurrentRounds.map((r) => r.id)

            const hasFileRequirement = await ctx.prisma.fileRequirement.findFirst({
              where: {
                roundId: { in: roundIds },
                files: { some: { bucket: input.bucket, objectKey: input.objectKey } },
              },
              select: { id: true },
            })

            if (!hasFileRequirement) {
              const fileInProject = await ctx.prisma.projectFile.findFirst({
                where: {
                  bucket: input.bucket,
                  objectKey: input.objectKey,
                  requirementId: null,
                },
                select: { id: true },
              })

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

      let url: string
      try {
        url = await getPresignedUrl(input.bucket, input.objectKey, 'GET', 900,
          input.forDownload ? { downloadFileName: input.fileName || input.objectKey.split('/').pop() || 'download' } : undefined
        ) // 15 min
      } catch (err) {
        console.error('[file] getPresignedUrl failed:', input.objectKey, err instanceof Error ? err.message : err)
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: `File not available in storage: ${err instanceof Error ? err.message : 'unknown error'}`,
        })
      }

      // Log file access with granular action based on purpose
      const purpose = input.purpose ?? (input.forDownload ? 'download' : undefined)
      if (purpose) {
        const actionMap = {
          preview: 'FILE_VIEWED',
          open: 'FILE_OPENED',
          download: 'FILE_DOWNLOADED',
        } as const
        await logAudit({
          prisma: ctx.prisma,
          userId: ctx.user.id,
          action: actionMap[purpose],
          entityType: 'ProjectFile',
          detailsJson: {
            bucket: input.bucket,
            objectKey: input.objectKey,
            fileName: input.fileName,
            purpose,
          },
          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`,
        })
      }

      // Fetch project title and optional round name for storage path
      const [project, roundInfo] = await Promise.all([
        ctx.prisma.project.findUniqueOrThrow({
          where: { id: input.projectId },
          select: { title: true },
        }),
        input.roundId
          ? ctx.prisma.round.findUnique({
              where: { id: input.roundId },
              select: { name: true, windowCloseAt: true },
            })
          : null,
      ])

      let isLate = false
      if (roundInfo?.windowCloseAt) {
        isLate = new Date() > roundInfo.windowCloseAt
      }

      const bucket = BUCKET_NAME
      const objectKey = generateObjectKey(project.title, input.fileName, roundInfo?.name)

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

      // Create file record
      const file = await ctx.prisma.projectFile.create({
        data: {
          projectId: input.projectId,
          roundId: input.roundId ?? null,
          fileType: input.fileType,
          fileName: input.fileName,
          mimeType: input.mimeType,
          size: input.size,
          bucket,
          objectKey,
          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,
      })

      // Auto-analyze document (fire-and-forget, delayed for presigned upload)
      import('../services/document-analyzer').then(({ analyzeFileDelayed }) =>
        analyzeFileDelayed(file.id).catch((err) =>
          console.warn('[DocAnalyzer] Post-upload analysis failed:', err))
      ).catch(() => {})

      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 isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'].includes(ctx.user.role)

      if (!isAdminOrObserver) {
        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.OR = [
          { requirement: { roundId: input.roundId } },
          { roundId: input.roundId, requirementId: null },
        ]
      }

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

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

      if (!isAdminOrObserver) {
        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',
          })
        }
      }

      const targetRound = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { competitionId: true, sortOrder: true },
      })

      const eligibleRounds = await ctx.prisma.round.findMany({
        where: {
          competitionId: targetRound.competitionId,
          sortOrder: { lte: targetRound.sortOrder },
        },
        select: { id: true, name: true, sortOrder: true },
        orderBy: { sortOrder: 'asc' },
      })

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

      const files = await ctx.prisma.projectFile.findMany({
        where: {
          projectId: input.projectId,
          OR: [
            { requirement: { roundId: { in: eligibleRoundIds } } },
            { roundId: { in: eligibleRoundIds }, requirementId: null },
            { roundId: null, requirementId: null },
          ],
        },
        include: {
          requirement: {
            select: {
              id: true,
              name: true,
              description: true,
              isRequired: true,
              roundId: true,
              round: { select: { id: true, name: true, sortOrder: true } },
            },
          },
        },
        orderBy: [{ createdAt: 'asc' }],
      })

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

      // Files with no round association at all go to General
      const generalFiles = files.filter((f) => !f.requirementId && !f.roundId)
      if (generalFiles.length > 0) {
        grouped.push({
          roundId: null,
          roundName: 'General',
          sortOrder: -1,
          files: generalFiles,
        })
      }

      for (const round of eligibleRounds) {
        const roundFiles = files.filter(
          (f) => f.requirement?.roundId === round.id || (!f.requirementId && 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 }) => {
      // Replace is a write operation on the team's submission. Only admins
      // and the team itself (submitter or team member) may replace files —
      // jurors and mentors are read-only on project files even though they
      // can see them. Observers/Award masters are also read-only.
      const isAdmin = ctx.user.role === 'SUPER_ADMIN' || ctx.user.role === 'PROGRAM_ADMIN'

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

        if (!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',
        })
      }

      // Verify the client-supplied objectKey actually belongs to this project's
      // sanitized prefix. Without this check, a team member (or admin) could
      // pass another project's bucket+objectKey here and bind it as a "new
      // version" — then anyone with project access could read the foreign
      // object via file.getDownloadUrl, which authorizes by bucket+objectKey.
      const project = await ctx.prisma.project.findUniqueOrThrow({
        where: { id: input.projectId },
        select: { title: true },
      })
      if (input.bucket !== BUCKET_NAME) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Invalid file location',
        })
      }
      if (!objectKeyBelongsToProject(input.objectKey, project.title)) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Invalid file location for this 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 },
        })

        return newFile
      })

      // Audit outside transaction so failures don't roll back the file replacement
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'REPLACE_FILE',
        entityType: 'ProjectFile',
        entityId: result.id,
        detailsJson: {
          projectId: input.projectId,
          oldFileId: input.oldFileId,
          oldVersion: oldFile.version,
          newVersion: result.version,
          fileName: input.fileName,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      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 isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'].includes(ctx.user.role)

      // For non-admin/observer callers, mirror the per-round scope used by
      // file.getDownloadUrl: a juror assigned to round N may only pull URLs
      // for files in rounds with sortOrder <= N. Without this, a juror
      // assigned to EVALUATION can bulk-download LIVE_FINAL/DELIBERATION
      // confidential files via this endpoint.
      let priorRoundIds: string[] | null = null

      if (!isAdminOrObserver) {
        const [assignment, 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 (!assignment && !mentorAssignment && !teamMembership) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to this project\'s files',
          })
        }

        // Apply per-round filter only when the caller's access is jury-only.
        // Mentors / team members / award jurors get unrestricted file access
        // on the project (matches getDownloadUrl semantics).
        if (assignment && !mentorAssignment && !teamMembership) {
          const assignedRound = await ctx.prisma.round.findUnique({
            where: { id: assignment.roundId },
            select: { competitionId: true, sortOrder: true },
          })
          if (assignedRound) {
            const priorOrCurrent = await ctx.prisma.round.findMany({
              where: {
                competitionId: assignedRound.competitionId,
                sortOrder: { lte: assignedRound.sortOrder },
              },
              select: { id: true },
            })
            priorRoundIds = priorOrCurrent.map((r) => r.id)
          }
        }
      }

      // Get files
      const where: Record<string, unknown> = { projectId: input.projectId }
      if (input.fileIds && input.fileIds.length > 0) {
        where.id = { in: input.fileIds }
      }
      if (priorRoundIds !== null) {
        // Allow a file when:
        //  - it has a requirement whose round is in the allowed set, OR
        //  - it has no requirement and its roundId is in the allowed set, OR
        //  - it has neither requirement nor roundId (legacy general files)
        where.OR = [
          { requirement: { roundId: { in: priorRoundIds } } },
          { requirementId: null, roundId: { in: priorRoundIds } },
          { requirementId: null, roundId: null },
        ]
      }

      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,
            { downloadFileName: file.fileName }
          )
          return {
            fileId: file.id,
            fileName: file.fileName,
            downloadUrl,
          }
        })
      )

      return results
    }),

  // NOTE: getProjectRequirements procedure removed - depends on deleted Pipeline/Track/Stage models
  // Will need to be reimplemented with new Competition/Round architecture

  // =========================================================================
  // FILE REQUIREMENTS
  // =========================================================================

  /**
   * Materialize legacy configJson file requirements into FileRequirement rows.
   * No-op if the stage already has DB-backed requirements.
   */
  materializeRequirementsFromConfig: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const stage = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: {
          id: true,
          roundType: true,
          configJson: true,
        },
      })

      if (stage.roundType !== 'INTAKE') {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Requirements can only be materialized for INTAKE stages',
        })
      }

      const existingCount = await ctx.prisma.fileRequirement.count({
        where: { roundId: input.roundId },
      })
      if (existingCount > 0) {
        return { created: 0, skipped: true, reason: 'already_materialized' as const }
      }

      const config = (stage.configJson as Record<string, unknown> | null) ?? {}
      const configRequirements = Array.isArray(config.fileRequirements)
        ? (config.fileRequirements as Array<Record<string, unknown>>)
        : []

      if (configRequirements.length === 0) {
        return { created: 0, skipped: true, reason: 'no_config_requirements' as const }
      }

      const mapLegacyMimeType = (type: unknown): string[] => {
        switch (String(type ?? '').toUpperCase()) {
          case 'PDF':
            return ['application/pdf']
          case 'VIDEO':
            return ['video/*']
          case 'IMAGE':
            return ['image/*']
          case 'DOC':
          case 'DOCX':
            return ['application/vnd.openxmlformats-officedocument.wordprocessingml.document']
          case 'PPT':
          case 'PPTX':
            return ['application/vnd.openxmlformats-officedocument.presentationml.presentation']
          default:
            return []
        }
      }

      let created = 0
      await ctx.prisma.$transaction(async (tx) => {
        for (let i = 0; i < configRequirements.length; i++) {
          const raw = configRequirements[i]
          const name = typeof raw.name === 'string' ? raw.name.trim() : ''
          if (!name) continue

          const acceptedMimeTypes = Array.isArray(raw.acceptedMimeTypes)
            ? raw.acceptedMimeTypes.filter((v): v is string => typeof v === 'string')
            : mapLegacyMimeType(raw.type)

          await tx.fileRequirement.create({
            data: {
              roundId: input.roundId,
              name,
              description:
                typeof raw.description === 'string' && raw.description.trim().length > 0
                  ? raw.description.trim()
                  : undefined,
              acceptedMimeTypes,
              maxSizeMB:
                typeof raw.maxSizeMB === 'number' && Number.isFinite(raw.maxSizeMB)
                  ? Math.trunc(raw.maxSizeMB)
                  : undefined,
              isRequired:
                (raw.isRequired as boolean | undefined) ??
                ((raw.required as boolean | undefined) ?? false),
              sortOrder: i,
            },
          })
          created++
        }
      })

      return { created, skipped: false as const }
    }),

  /**
   * List file requirements for a stage (available to any authenticated user)
   */
  listRequirements: protectedProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.fileRequirement.findMany({
        where: { roundId: input.roundId },
        orderBy: { sortOrder: 'asc' },
      })
    }),

  /**
   * List file requirements for multiple rounds in a single query.
   * Avoids dynamic hook violations when fetching requirements per-round.
   */
  listRequirementsByRounds: protectedProcedure
    .input(z.object({ roundIds: z.array(z.string()).max(50) }))
    .query(async ({ ctx, input }) => {
      if (input.roundIds.length === 0) return []
      return ctx.prisma.fileRequirement.findMany({
        where: { roundId: { in: input.roundIds } },
        orderBy: { sortOrder: 'asc' },
      })
    }),

  /**
   * Create a file requirement for a stage (admin only)
   */
  createRequirement: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        name: z.string().min(1).max(200),
        description: z.string().max(1000).optional(),
        acceptedMimeTypes: z.array(z.string()).default([]),
        maxSizeMB: z.number().int().min(1).max(5000).optional(),
        isRequired: z.boolean().default(true),
        sortOrder: z.number().int().default(0),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const requirement = await ctx.prisma.fileRequirement.create({
        data: input,
      })

      try {
        await logAudit({
          prisma: ctx.prisma,
          userId: ctx.user.id,
          action: 'CREATE',
          entityType: 'FileRequirement',
          entityId: requirement.id,
          detailsJson: { name: input.name, roundId: input.roundId },
        })
      } catch (err) {
        console.error('[File] Audit log failed:', err)
      }

      return requirement
    }),

  /**
   * Update a file requirement (admin only)
   */
  updateRequirement: adminProcedure
    .input(
      z.object({
        id: z.string(),
        name: z.string().min(1).max(200).optional(),
        description: z.string().max(1000).optional().nullable(),
        acceptedMimeTypes: z.array(z.string()).optional(),
        maxSizeMB: z.number().int().min(1).max(5000).optional().nullable(),
        isRequired: z.boolean().optional(),
        sortOrder: z.number().int().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input
      const requirement = await ctx.prisma.fileRequirement.update({
        where: { id },
        data,
      })

      try {
        await logAudit({
          prisma: ctx.prisma,
          userId: ctx.user.id,
          action: 'UPDATE',
          entityType: 'FileRequirement',
          entityId: id,
          detailsJson: data,
        })
      } catch (err) {
        console.error('[File] Audit log failed:', err)
      }

      return requirement
    }),

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

      try {
        await logAudit({
          prisma: ctx.prisma,
          userId: ctx.user.id,
          action: 'DELETE',
          entityType: 'FileRequirement',
          entityId: input.id,
        })
      } catch (err) {
        console.error('[File] Audit log failed:', err)
      }

      return { success: true }
    }),

  /**
   * Reorder file requirements (admin only)
   */
  reorderRequirements: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        orderedIds: z.array(z.string()),
      })
    )
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.$transaction(
        input.orderedIds.map((id, index) =>
          ctx.prisma.fileRequirement.update({
            where: { id },
            data: { sortOrder: index },
          })
        )
      )

      return { success: true }
    }),

  // =========================================================================
  // BULK UPLOAD
  // =========================================================================

  /**
   * List projects with their upload status for a given submission window.
   * Powers the bulk upload admin page.
   */
  listProjectsWithUploadStatus: adminProcedure
    .input(
      z.object({
        submissionWindowId: z.string(),
        search: z.string().optional(),
        status: z.enum(['all', 'missing', 'complete']).default('all'),
        page: z.number().int().min(1).default(1),
        pageSize: z.number().int().min(1).max(100).default(50),
      })
    )
    .query(async ({ ctx, input }) => {
      // Get the submission window with its requirements and competition
      const window = await ctx.prisma.submissionWindow.findUniqueOrThrow({
        where: { id: input.submissionWindowId },
        include: {
          competition: { select: { id: true, programId: true, name: true } },
          fileRequirements: { orderBy: { sortOrder: 'asc' } },
        },
      })

      const requirements = window.fileRequirements

      // Build project filter
      const projectWhere: Record<string, unknown> = {
        programId: window.competition.programId,
      }
      if (input.search) {
        projectWhere.OR = [
          { title: { contains: input.search, mode: 'insensitive' } },
          { teamName: { contains: input.search, mode: 'insensitive' } },
          { teamMembers: { some: { user: { name: { contains: input.search, mode: 'insensitive' } } } } },
          { teamMembers: { some: { user: { email: { contains: input.search, mode: 'insensitive' } } } } },
        ]
      }

      // Get total count first (before status filtering, which happens in-memory)
      const allProjects = await ctx.prisma.project.findMany({
        where: projectWhere,
        select: {
          id: true,
          title: true,
          teamName: true,
          submittedByUserId: true,
          submittedBy: { select: { id: true, name: true, email: true } },
          files: {
            where: { submissionWindowId: input.submissionWindowId },
            select: {
              id: true,
              fileName: true,
              mimeType: true,
              size: true,
              createdAt: true,
              submissionFileRequirementId: true,
            },
          },
        },
        orderBy: { title: 'asc' },
      })

      // Map projects with their requirement status
      const mapped = allProjects.map((project) => {
        const reqStatus = requirements.map((req) => {
          const file = project.files.find(
            (f) => f.submissionFileRequirementId === req.id
          )
          return {
            requirementId: req.id,
            label: req.label,
            mimeTypes: req.mimeTypes,
            required: req.required,
            file: file ?? null,
          }
        })

        const totalRequired = reqStatus.filter((r) => r.required).length
        const filledRequired = reqStatus.filter(
          (r) => r.required && r.file
        ).length

        return {
          project: {
            id: project.id,
            title: project.title,
            teamName: project.teamName,
            submittedBy: project.submittedBy,
          },
          requirements: reqStatus,
          isComplete: totalRequired > 0 ? filledRequired >= totalRequired : reqStatus.every((r) => r.file),
          filledCount: reqStatus.filter((r) => r.file).length,
          totalCount: reqStatus.length,
        }
      })

      // Apply status filter
      const filtered =
        input.status === 'missing'
          ? mapped.filter((p) => !p.isComplete)
          : input.status === 'complete'
            ? mapped.filter((p) => p.isComplete)
            : mapped

      // Paginate
      const total = filtered.length
      const totalPages = Math.ceil(total / input.pageSize)
      const page = Math.min(input.page, Math.max(totalPages, 1))
      const projects = filtered.slice(
        (page - 1) * input.pageSize,
        page * input.pageSize
      )

      // Summary stats
      const completeCount = mapped.filter((p) => p.isComplete).length

      return {
        projects,
        requirements,
        total,
        page,
        totalPages,
        completeCount,
        totalProjects: mapped.length,
        competition: window.competition,
        windowName: window.name,
      }
    }),

  /**
   * Admin upload for a specific submission file requirement.
   * Creates pre-signed PUT URL + ProjectFile record.
   */
  adminUploadForRequirement: adminProcedure
    .input(
      z.object({
        projectId: z.string(),
        fileName: z.string(),
        mimeType: z.string(),
        size: z.number().int().positive(),
        submissionWindowId: z.string(),
        submissionFileRequirementId: z.string(),
      })
    )
    .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`,
        })
      }

      // Validate requirement exists and belongs to the window
      const requirement = await ctx.prisma.submissionFileRequirement.findFirst({
        where: {
          id: input.submissionFileRequirementId,
          submissionWindowId: input.submissionWindowId,
        },
      })
      if (!requirement) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Requirement not found for this submission window',
        })
      }

      // Validate MIME type if requirement specifies allowed types
      if (requirement.mimeTypes.length > 0) {
        const isAllowed = requirement.mimeTypes.some((allowed) => {
          if (allowed.endsWith('/*')) {
            return input.mimeType.startsWith(allowed.replace('/*', '/'))
          }
          return input.mimeType === allowed
        })
        if (!isAllowed) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: `File type "${input.mimeType}" is not allowed for this requirement. Accepted: ${requirement.mimeTypes.join(', ')}`,
          })
        }
      }

      // Infer fileType from mimeType
      let fileType: 'EXEC_SUMMARY' | 'PRESENTATION' | 'VIDEO' | 'OTHER' = 'OTHER'
      if (input.mimeType.startsWith('video/')) fileType = 'VIDEO'
      else if (input.mimeType === 'application/pdf') fileType = 'EXEC_SUMMARY'
      else if (input.mimeType.includes('presentation') || input.mimeType.includes('powerpoint'))
        fileType = 'PRESENTATION'

      // Fetch project title and window name for storage path
      const [project, submissionWindow] = await Promise.all([
        ctx.prisma.project.findUniqueOrThrow({
          where: { id: input.projectId },
          select: { title: true },
        }),
        ctx.prisma.submissionWindow.findUniqueOrThrow({
          where: { id: input.submissionWindowId },
          select: { name: true },
        }),
      ])

      const bucket = BUCKET_NAME
      const objectKey = generateObjectKey(project.title, input.fileName, submissionWindow.name)
      const uploadUrl = await getPresignedUrl(bucket, objectKey, 'PUT', 3600)

      // Remove any existing file for this project+requirement combo (replace)
      await ctx.prisma.projectFile.deleteMany({
        where: {
          projectId: input.projectId,
          submissionWindowId: input.submissionWindowId,
          submissionFileRequirementId: input.submissionFileRequirementId,
        },
      })

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

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPLOAD_FILE',
        entityType: 'ProjectFile',
        entityId: file.id,
        detailsJson: {
          projectId: input.projectId,
          fileName: input.fileName,
          submissionWindowId: input.submissionWindowId,
          submissionFileRequirementId: input.submissionFileRequirementId,
          bulkUpload: true,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      // Auto-analyze document (fire-and-forget, delayed for presigned upload)
      import('../services/document-analyzer').then(({ analyzeFileDelayed }) =>
        analyzeFileDelayed(file.id).catch((err) =>
          console.warn('[DocAnalyzer] Post-upload analysis failed:', err))
      ).catch(() => {})

      return { uploadUrl, file }
    }),

  /**
   * List submission windows (for the bulk upload window selector)
   */
  listSubmissionWindows: adminProcedure
    .query(async ({ ctx }) => {
      return ctx.prisma.submissionWindow.findMany({
        include: {
          competition: {
            select: { id: true, name: true, program: { select: { name: true, year: true } } },
          },
          fileRequirements: {
            select: { id: true },
          },
        },
        orderBy: [{ competition: { program: { year: 'desc' } } }, { sortOrder: 'asc' }],
      })
    }),

  /**
   * List rounds with their file requirement counts (for bulk upload round selector)
   */
  listRoundsForBulkUpload: adminProcedure
    .query(async ({ ctx }) => {
      return ctx.prisma.round.findMany({
        where: {
          fileRequirements: { some: {} },
        },
        select: {
          id: true,
          name: true,
          roundType: true,
          sortOrder: true,
          competition: {
            select: { id: true, name: true, program: { select: { name: true, year: true } } },
          },
          fileRequirements: {
            select: { id: true },
          },
        },
        orderBy: [
          { competition: { program: { year: 'desc' } } },
          { sortOrder: 'asc' },
        ],
      })
    }),

  /**
   * List projects with upload status against a round's FileRequirements (for bulk upload)
   */
  listProjectsByRoundRequirements: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        search: z.string().optional(),
        status: z.enum(['all', 'missing', 'complete']).default('all'),
        page: z.number().int().min(1).default(1),
        pageSize: z.number().int().min(1).max(100).default(50),
      })
    )
    .query(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        include: {
          competition: { select: { id: true, programId: true, name: true } },
          fileRequirements: { orderBy: { sortOrder: 'asc' } },
        },
      })

      // Normalize requirements to a common shape
      const requirements = round.fileRequirements.map((req) => ({
        id: req.id,
        label: req.name,
        mimeTypes: req.acceptedMimeTypes,
        required: req.isRequired,
        maxSizeMb: req.maxSizeMB,
        description: req.description,
      }))

      // Build project filter
      const projectWhere: Record<string, unknown> = {
        programId: round.competition.programId,
      }
      if (input.search) {
        projectWhere.OR = [
          { title: { contains: input.search, mode: 'insensitive' } },
          { teamName: { contains: input.search, mode: 'insensitive' } },
          { teamMembers: { some: { user: { name: { contains: input.search, mode: 'insensitive' } } } } },
          { teamMembers: { some: { user: { email: { contains: input.search, mode: 'insensitive' } } } } },
        ]
      }

      const allProjects = await ctx.prisma.project.findMany({
        where: projectWhere,
        select: {
          id: true,
          title: true,
          teamName: true,
          submittedByUserId: true,
          submittedBy: { select: { id: true, name: true, email: true } },
          files: {
            where: { roundId: input.roundId, requirementId: { not: null } },
            select: {
              id: true,
              fileName: true,
              mimeType: true,
              size: true,
              createdAt: true,
              requirementId: true,
              bucket: true,
              objectKey: true,
            },
          },
        },
        orderBy: { title: 'asc' },
      })

      // Map projects with their requirement status
      const mapped = allProjects.map((project) => {
        const reqStatus = requirements.map((req) => {
          const file = project.files.find(
            (f) => f.requirementId === req.id
          )
          return {
            requirementId: req.id,
            label: req.label,
            mimeTypes: req.mimeTypes,
            required: req.required,
            file: file ?? null,
          }
        })

        const totalRequired = reqStatus.filter((r) => r.required).length
        const filledRequired = reqStatus.filter(
          (r) => r.required && r.file
        ).length

        return {
          project: {
            id: project.id,
            title: project.title,
            teamName: project.teamName,
            submittedBy: project.submittedBy,
          },
          requirements: reqStatus,
          isComplete: totalRequired > 0 ? filledRequired >= totalRequired : reqStatus.every((r) => r.file),
          filledCount: reqStatus.filter((r) => r.file).length,
          totalCount: reqStatus.length,
        }
      })

      // Apply status filter
      const filtered =
        input.status === 'missing'
          ? mapped.filter((p) => !p.isComplete)
          : input.status === 'complete'
            ? mapped.filter((p) => p.isComplete)
            : mapped

      // Paginate
      const total = filtered.length
      const totalPages = Math.ceil(total / input.pageSize)
      const page = Math.min(input.page, Math.max(totalPages, 1))
      const projects = filtered.slice(
        (page - 1) * input.pageSize,
        page * input.pageSize
      )

      const completeCount = mapped.filter((p) => p.isComplete).length

      return {
        projects,
        requirements,
        total,
        page,
        totalPages,
        completeCount,
        totalProjects: mapped.length,
        competition: round.competition,
      }
    }),

  /**
   * Upload a file for a round's FileRequirement (admin bulk upload)
   */
  adminUploadForRoundRequirement: adminProcedure
    .input(
      z.object({
        projectId: z.string(),
        fileName: z.string(),
        mimeType: z.string(),
        size: z.number().int().positive(),
        roundId: z.string(),
        requirementId: z.string(),
      })
    )
    .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`,
        })
      }

      // Validate requirement exists and belongs to the round
      const requirement = await ctx.prisma.fileRequirement.findFirst({
        where: {
          id: input.requirementId,
          roundId: input.roundId,
        },
      })
      if (!requirement) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Requirement not found for this round',
        })
      }

      // Validate MIME type if requirement specifies allowed types
      if (requirement.acceptedMimeTypes.length > 0) {
        const isAllowed = requirement.acceptedMimeTypes.some((allowed) => {
          if (allowed.endsWith('/*')) {
            return input.mimeType.startsWith(allowed.replace('/*', '/'))
          }
          return input.mimeType === allowed
        })
        if (!isAllowed) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: `File type "${input.mimeType}" is not allowed for this requirement. Accepted: ${requirement.acceptedMimeTypes.join(', ')}`,
          })
        }
      }

      // Infer fileType from mimeType
      let fileType: 'EXEC_SUMMARY' | 'PRESENTATION' | 'VIDEO' | 'OTHER' = 'OTHER'
      if (input.mimeType.startsWith('video/')) fileType = 'VIDEO'
      else if (input.mimeType === 'application/pdf') fileType = 'EXEC_SUMMARY'
      else if (input.mimeType.includes('presentation') || input.mimeType.includes('powerpoint'))
        fileType = 'PRESENTATION'

      // Fetch project title and round name for storage path
      const [project, round] = await Promise.all([
        ctx.prisma.project.findUniqueOrThrow({
          where: { id: input.projectId },
          select: { title: true },
        }),
        ctx.prisma.round.findUniqueOrThrow({
          where: { id: input.roundId },
          select: { name: true },
        }),
      ])

      const bucket = BUCKET_NAME
      const objectKey = generateObjectKey(project.title, input.fileName, round.name)
      const uploadUrl = await getPresignedUrl(bucket, objectKey, 'PUT', 3600)

      // Remove any existing file for this project+requirement combo (replace)
      await ctx.prisma.projectFile.deleteMany({
        where: {
          projectId: input.projectId,
          roundId: input.roundId,
          requirementId: input.requirementId,
        },
      })

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

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPLOAD_FILE',
        entityType: 'ProjectFile',
        entityId: file.id,
        detailsJson: {
          projectId: input.projectId,
          fileName: input.fileName,
          roundId: input.roundId,
          requirementId: input.requirementId,
          bulkUpload: true,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      // Auto-transition: check if all required documents are now uploaded
      await checkRequirementsAndTransition(
        input.projectId,
        input.roundId,
        ctx.user.id,
        ctx.prisma,
      )

      // Auto-analyze document (fire-and-forget, delayed for presigned upload)
      import('../services/document-analyzer').then(({ analyzeFileDelayed }) =>
        analyzeFileDelayed(file.id).catch((err) =>
          console.warn('[DocAnalyzer] Post-upload analysis failed:', err))
      ).catch(() => {})

      return { uploadUrl, file }
    }),

  /**
   * Verify that files actually exist in storage (MinIO/S3).
   * Returns a map of objectKey → exists boolean.
   */
  verifyFilesExist: adminProcedure
    .input(
      z.object({
        files: z.array(
          z.object({
            bucket: z.string(),
            objectKey: z.string(),
          })
        ).max(200),
      })
    )
    .query(async ({ input }) => {
      const { getMinioClient } = await import('@/lib/minio')
      const client = getMinioClient()

      const results: Record<string, boolean> = {}
      await Promise.all(
        input.files.map(async ({ bucket, objectKey }) => {
          try {
            await client.statObject(bucket, objectKey)
            results[objectKey] = true
          } catch (err) {
            console.error('Failed to stat MinIO object during existence check:', err)
            results[objectKey] = false
          }
        })
      )
      return results
    }),

  /**
   * Analyze all files for a specific project (page count, language, text preview).
   * Retroactive: re-analyzes even previously analyzed files.
   */
  analyzeProjectFiles: adminProcedure
    .input(z.object({ projectId: z.string() }))
    .mutation(async ({ input }) => {
      const { analyzeProjectFiles } = await import('../services/document-analyzer')
      return analyzeProjectFiles(input.projectId)
    }),

  /**
   * Batch analyze all unanalyzed files across the platform.
   * For retroactive analysis of files uploaded before this feature.
   */
  analyzeAllFiles: adminProcedure
    .mutation(async () => {
      const { analyzeAllUnanalyzed } = await import('../services/document-analyzer')
      return analyzeAllUnanalyzed()
    }),

  /**
   * Get document language summary for a specific round.
   * Returns per-project file language data, flagging non-English documents.
   */
  roundLanguageSummary: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const files = await ctx.prisma.projectFile.findMany({
        where: { roundId: input.roundId },
        select: {
          id: true,
          fileName: true,
          fileType: true,
          mimeType: true,
          pageCount: true,
          detectedLang: true,
          langConfidence: true,
          analyzedAt: true,
          project: { select: { id: true, title: true } },
        },
        orderBy: { createdAt: 'desc' },
      })

      const totalFiles = files.length
      const analyzedFiles = files.filter((f) => f.analyzedAt !== null)
      const nonEnglish = analyzedFiles.filter(
        (f) => f.detectedLang && f.detectedLang !== 'eng' && f.detectedLang !== 'und' && (f.langConfidence ?? 0) >= 0.4
      )
      const unanalyzed = files.filter((f) => f.analyzedAt === null)

      // Group non-English files by project
      const projectMap = new Map<string, {
        projectId: string
        projectTitle: string
        files: typeof nonEnglish
      }>()
      for (const file of nonEnglish) {
        const existing = projectMap.get(file.project.id)
        if (existing) {
          existing.files.push(file)
        } else {
          projectMap.set(file.project.id, {
            projectId: file.project.id,
            projectTitle: file.project.title,
            files: [file],
          })
        }
      }

      return {
        totalFiles,
        analyzedCount: analyzedFiles.length,
        unanalyzedCount: unanalyzed.length,
        nonEnglishCount: nonEnglish.length,
        flaggedProjects: Array.from(projectMap.values()),
      }
    }),
})
