import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { router, protectedProcedure, adminProcedure, userHasRole } from '../../trpc'
import { getUserAvatarUrl } from '../../utils/avatar-url'
import { createNotification, NotificationTypes } from '../../services/in-app-notification'
import { logAudit } from '@/server/utils/audit'
import { buildBatchNotifications } from './shared'

export const assignmentCrudRouter = router({
  listByStage: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.assignment.findMany({
        where: { roundId: input.roundId },
        include: {
          user: { select: { id: true, name: true, email: true, expertiseTags: true } },
          project: { select: { id: true, title: true, tags: true } },
          evaluation: {
            select: {
              status: true,
              submittedAt: true,
              criterionScoresJson: true,
              form: { select: { criteriaJson: true } },
            },
          },
          conflictOfInterest: { select: { hasConflict: true, conflictType: true, reviewAction: true } },
        },
        orderBy: { createdAt: 'desc' },
      })
    }),

  /**
   * List assignments for a project (admin only)
   */
  listByProject: adminProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      const assignments = await ctx.prisma.assignment.findMany({
        where: { projectId: input.projectId },
        include: {
          user: { select: { id: true, name: true, email: true, expertiseTags: true, profileImageKey: true, profileImageProvider: true } },
          evaluation: { select: { status: true, submittedAt: true, globalScore: true, binaryDecision: true } },
        },
        orderBy: { createdAt: 'desc' },
      })

      // Attach avatar URLs
      return Promise.all(
        assignments.map(async (a) => ({
          ...a,
          user: {
            ...a.user,
            avatarUrl: await getUserAvatarUrl(a.user.profileImageKey, a.user.profileImageProvider),
          },
        }))
      )
    }),

  /**
   * Get my assignments (for jury members)
   */
  myAssignments: protectedProcedure
    .input(
      z.object({
        roundId: z.string().optional(),
        status: z.enum(['all', 'pending', 'completed']).default('all'),
      })
    )
    .query(async ({ ctx, input }) => {
      const where: Record<string, unknown> = {
        userId: ctx.user.id,
      }

      if (input.roundId) {
        where.roundId = input.roundId
      }

      if (input.status === 'pending') {
        where.isCompleted = false
      } else if (input.status === 'completed') {
        where.isCompleted = true
      }

      return ctx.prisma.assignment.findMany({
        where,
        include: {
          project: {
            include: { files: true },
          },
          round: true,
          evaluation: true,
        },
        orderBy: [{ isCompleted: 'asc' }, { createdAt: 'asc' }],
      })
    }),

  /**
   * Get assignment by ID
   */
  get: protectedProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const assignment = await ctx.prisma.assignment.findUniqueOrThrow({
        where: { id: input.id },
        include: {
          user: { select: { id: true, name: true, email: true } },
          project: { include: { files: true } },
          round: { include: { evaluationForms: { where: { isActive: true } } } },
          evaluation: true,
        },
      })

      // Verify access
      if (
        userHasRole(ctx.user, 'JURY_MEMBER') &&
        !userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN') &&
        assignment.userId !== ctx.user.id
      ) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'You do not have access to this assignment',
        })
      }

      return assignment
    }),

  /**
   * Create a single assignment (admin only)
   */
  create: adminProcedure
    .input(
      z.object({
        userId: z.string(),
        projectId: z.string(),
        roundId: z.string(),
        isRequired: z.boolean().default(true),
        forceOverride: z.boolean().default(false),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.prisma.assignment.findUnique({
        where: {
          userId_projectId_roundId: {
            userId: input.userId,
            projectId: input.projectId,
            roundId: input.roundId,
          },
        },
      })

      if (existing) {
        throw new TRPCError({
          code: 'CONFLICT',
          message: 'This assignment already exists',
        })
      }

      const [stage, user] = await Promise.all([
        ctx.prisma.round.findUniqueOrThrow({
          where: { id: input.roundId },
          select: { configJson: true },
        }),
        ctx.prisma.user.findUniqueOrThrow({
          where: { id: input.userId },
          select: { maxAssignments: true, name: true },
        }),
      ])

      const config = (stage.configJson ?? {}) as Record<string, unknown>
      const maxAssignmentsPerJuror =
        (config.maxLoadPerJuror as number) ??
        (config.maxAssignmentsPerJuror as number) ??
        20
      const effectiveMax = user.maxAssignments ?? maxAssignmentsPerJuror

      const currentCount = await ctx.prisma.assignment.count({
        where: { userId: input.userId, roundId: input.roundId },
      })

      // Check if at or over limit
      if (currentCount >= effectiveMax) {
        if (!input.forceOverride) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: `${user.name || 'Judge'} has reached their maximum limit of ${effectiveMax} projects. Use manual override to proceed.`,
          })
        }
        // Log the override in audit
        console.log(`[Assignment] Manual override: Assigning ${user.name} beyond limit (${currentCount}/${effectiveMax})`)
      }

      const { forceOverride: _override, ...assignmentData } = input
      const assignment = await ctx.prisma.assignment.create({
        data: {
          ...assignmentData,
          method: 'MANUAL',
          createdBy: ctx.user.id,
        },
      })

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'Assignment',
        entityId: assignment.id,
        detailsJson: input,
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      const [project, stageInfo] = await Promise.all([
        ctx.prisma.project.findUnique({
          where: { id: input.projectId },
          select: { title: true },
        }),
        ctx.prisma.round.findUnique({
          where: { id: input.roundId },
          select: { name: true, windowCloseAt: true },
        }),
      ])

      if (project && stageInfo) {
        const deadline = stageInfo.windowCloseAt
          ? new Date(stageInfo.windowCloseAt).toLocaleDateString('en-US', {
              weekday: 'long',
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })
          : undefined

        await createNotification({
          userId: input.userId,
          type: NotificationTypes.ASSIGNED_TO_PROJECT,
          title: 'New Project Assignment',
          message: `You have been assigned to evaluate "${project.title}" for ${stageInfo.name}.`,
          linkUrl: `/jury/competitions`,
          linkLabel: 'View Assignment',
          metadata: {
            projectName: project.title,
            roundName: stageInfo.name,
            deadline,
            assignmentId: assignment.id,
          },
        })
      }

      return assignment
    }),

  /**
   * Bulk create assignments (admin only)
   */
  bulkCreate: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        assignments: z.array(
          z.object({
            userId: z.string(),
            projectId: z.string(),
          })
        ),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Fetch per-juror maxAssignments and current counts for capacity checking
      const uniqueUserIds = [...new Set(input.assignments.map((a) => a.userId))]
      const users = await ctx.prisma.user.findMany({
        where: { id: { in: uniqueUserIds } },
        select: {
          id: true,
          name: true,
          maxAssignments: true,
          _count: {
            select: {
              assignments: { where: { roundId: input.roundId } },
            },
          },
        },
      })
      const userMap = new Map(users.map((u) => [u.id, u]))

      // Get stage default max
      const stage = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { configJson: true, name: true, windowCloseAt: true },
      })
      const config = (stage.configJson ?? {}) as Record<string, unknown>
      const stageMaxPerJuror =
        (config.maxLoadPerJuror as number) ??
        (config.maxAssignmentsPerJuror as number) ??
        20

      // Track running counts to handle multiple assignments to the same juror in one batch
      const runningCounts = new Map<string, number>()
      for (const u of users) {
        runningCounts.set(u.id, u._count.assignments)
      }

      // Filter out assignments that would exceed a juror's limit
      let skippedDueToCapacity = 0
      const allowedAssignments = input.assignments.filter((a) => {
        const user = userMap.get(a.userId)
        if (!user) return true // unknown user, let createMany handle it

        const effectiveMax = user.maxAssignments ?? stageMaxPerJuror
        const currentCount = runningCounts.get(a.userId) ?? 0

        if (currentCount >= effectiveMax) {
          skippedDueToCapacity++
          return false
        }

        // Increment running count for subsequent assignments to same user
        runningCounts.set(a.userId, currentCount + 1)
        return true
      })

      const result = await ctx.prisma.assignment.createMany({
        data: allowedAssignments.map((a) => ({
          ...a,
          roundId: input.roundId,
          method: 'BULK',
          createdBy: ctx.user.id,
        })),
        skipDuplicates: true,
      })

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'BULK_CREATE',
        entityType: 'Assignment',
        detailsJson: {
          count: result.count,
          requested: input.assignments.length,
          skippedDueToCapacity,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      // Send notifications to assigned jury members (grouped by user)
      if (result.count > 0 && allowedAssignments.length > 0) {
        // Group assignments by user to get counts
        const userAssignmentCounts = allowedAssignments.reduce(
          (acc, a) => {
            acc[a.userId] = (acc[a.userId] || 0) + 1
            return acc
          },
          {} as Record<string, number>
        )

        const deadline = stage?.windowCloseAt
          ? new Date(stage.windowCloseAt).toLocaleDateString('en-US', {
              weekday: 'long',
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })
          : undefined

        await buildBatchNotifications(userAssignmentCounts, stage?.name, deadline)
      }

      return {
        created: result.count,
        requested: input.assignments.length,
        skipped: input.assignments.length - result.count,
        skippedDueToCapacity,
      }
    }),

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

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'Assignment',
        entityId: input.id,
        detailsJson: {
          userId: assignment.userId,
          projectId: assignment.projectId,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return assignment
    }),

  /**
   * Get assignment statistics for a round
   */
  getStats: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const stage = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { configJson: true },
      })
      const config = (stage.configJson ?? {}) as Record<string, unknown>
      const requiredReviews = (config.requiredReviewsPerProject as number) ?? 3

      const projectRoundStates = await ctx.prisma.projectRoundState.findMany({
        where: { roundId: input.roundId },
        select: { projectId: true },
      })
      const projectIds = projectRoundStates.map((pss) => pss.projectId)

      const [
        totalAssignments,
        completedAssignments,
        assignmentsByUser,
        projectCoverage,
      ] = await Promise.all([
        ctx.prisma.assignment.count({ where: { roundId: input.roundId } }),
        ctx.prisma.assignment.count({
          where: { roundId: input.roundId, isCompleted: true },
        }),
        ctx.prisma.assignment.groupBy({
          by: ['userId'],
          where: { roundId: input.roundId },
          _count: true,
        }),
        ctx.prisma.project.findMany({
          where: { id: { in: projectIds } },
          select: {
            id: true,
            title: true,
            _count: { select: { assignments: { where: { roundId: input.roundId } } } },
          },
        }),
      ])

      const projectsWithFullCoverage = projectCoverage.filter(
        (p) => p._count.assignments >= requiredReviews
      ).length

      return {
        totalAssignments,
        completedAssignments,
        completionPercentage:
          totalAssignments > 0
            ? Math.round((completedAssignments / totalAssignments) * 100)
            : 0,
        juryMembersAssigned: assignmentsByUser.length,
        projectsWithFullCoverage,
        totalProjects: projectCoverage.length,
        coveragePercentage:
          projectCoverage.length > 0
            ? Math.round(
                (projectsWithFullCoverage / projectCoverage.length) * 100
              )
            : 0,
      }
    }),
})
