import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { Prisma, type PrismaClient } from '@prisma/client'
import { router, adminProcedure, protectedProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import { validateRoundConfig, defaultRoundConfig } from '@/types/competition-configs'
import { generateShortlist } from '../services/ai-shortlist'
import { createBulkNotifications } from '../services/in-app-notification'
import {
  getAdvancementNotificationTemplate,
  getRejectionNotificationTemplate,
  sendInvitationEmail,
  getBaseUrl,
} from '@/lib/email'
import { sendBatchNotifications } from '../services/notification-sender'
import type { NotificationItem } from '../services/notification-sender'
import { generateInviteToken, getInviteExpiryHours, getInviteExpiryMs } from '@/server/utils/invite'
import {
  openWindow,
  closeWindow,
  lockWindow,
  checkDeadlinePolicy,
  validateSubmission,
  getVisibleWindows,
} from '../services/submission-manager'

const roundTypeEnum = z.enum([
  'INTAKE',
  'FILTERING',
  'EVALUATION',
  'SUBMISSION',
  'MENTORING',
  'LIVE_FINAL',
  'DELIBERATION',
])

export const roundRouter = router({
  /**
   * Create a new round within a competition
   */
  create: adminProcedure
    .input(
      z.object({
        competitionId: z.string(),
        name: z.string().min(1).max(255),
        slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
        roundType: roundTypeEnum,
        sortOrder: z.number().int().nonnegative(),
        configJson: z.record(z.unknown()).optional(),
        windowOpenAt: z.date().nullable().optional(),
        windowCloseAt: z.date().nullable().optional(),
        juryGroupId: z.string().nullable().optional(),
        submissionWindowId: z.string().nullable().optional(),
        purposeKey: z.string().nullable().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Verify competition exists
      await ctx.prisma.competition.findUniqueOrThrow({
        where: { id: input.competitionId },
      })

      // Validate configJson against the Zod schema for this roundType
      const config = input.configJson
        ? validateRoundConfig(input.roundType, input.configJson)
        : defaultRoundConfig(input.roundType)

      const round = await ctx.prisma.round.create({
        data: {
          competitionId: input.competitionId,
          name: input.name,
          slug: input.slug,
          roundType: input.roundType,
          sortOrder: input.sortOrder,
          configJson: config as unknown as Prisma.InputJsonValue,
          windowOpenAt: input.windowOpenAt ?? undefined,
          windowCloseAt: input.windowCloseAt ?? undefined,
          juryGroupId: input.juryGroupId ?? undefined,
          submissionWindowId: input.submissionWindowId ?? undefined,
          purposeKey: input.purposeKey ?? undefined,
        },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'Round',
        entityId: round.id,
        detailsJson: {
          name: input.name,
          roundType: input.roundType,
          competitionId: input.competitionId,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return round
    }),

  /**
   * Get round by ID with all relations
   */
  getById: protectedProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findUnique({
        where: { id: input.id },
        include: {
          juryGroup: {
            include: {
              members: {
                include: {
                  user: { select: { id: true, name: true, email: true } },
                },
              },
            },
          },
          submissionWindow: {
            include: { fileRequirements: true },
          },
          visibleSubmissionWindows: {
            include: { submissionWindow: true },
          },
          _count: {
            select: { projectRoundStates: true },
          },
        },
      })

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

      return round
    }),

  /**
   * Update round settings/config
   */
  update: adminProcedure
    .input(
      z.object({
        id: z.string(),
        name: z.string().min(1).max(255).optional(),
        slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional(),
        status: z.enum(['ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED']).optional(),
        configJson: z.record(z.unknown()).optional(),
        windowOpenAt: z.date().nullable().optional(),
        windowCloseAt: z.date().nullable().optional(),
        juryGroupId: z.string().nullable().optional(),
        submissionWindowId: z.string().nullable().optional(),
        purposeKey: z.string().nullable().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, configJson, ...data } = input

      const existing = await ctx.prisma.round.findUniqueOrThrow({ where: { id } })

      // If configJson provided, validate it against the round type
      let validatedConfig: Prisma.InputJsonValue | undefined
      if (configJson) {
        const parsed = validateRoundConfig(existing.roundType, configJson)
        validatedConfig = parsed as unknown as Prisma.InputJsonValue
      }

      const round = await ctx.prisma.round.update({
        where: { id },
        data: {
          ...data,
          ...(validatedConfig !== undefined ? { configJson: validatedConfig } : {}),
        },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPDATE',
        entityType: 'Round',
        entityId: id,
        detailsJson: {
          changes: input,
          previous: {
            name: existing.name,
            status: existing.status,
          },
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return round
    }),

  /**
   * Reorder rounds within a competition
   */
  updateOrder: adminProcedure
    .input(
      z.object({
        competitionId: z.string(),
        roundIds: z.array(z.string()),
      })
    )
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.$transaction(
        input.roundIds.map((roundId, index) =>
          ctx.prisma.round.update({
            where: { id: roundId },
            data: { sortOrder: index },
          })
        )
      )
    }),

  /**
   * Count projects in a MENTORING round eligible for mentor auto-fill
   * (i.e., no mentorAssignment, scoped by configJson.eligibility).
   * Used by the Auto-fill remaining toolbar on the round Projects tab.
   */
  getProjectsNeedingMentor: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { roundType: true, configJson: true },
      })
      if (round.roundType !== 'MENTORING') {
        return { count: 0, eligibleTotal: 0, mentorPoolSize: 0 }
      }
      const config = (round.configJson ?? {}) as Record<string, unknown>
      const eligibility = (config.eligibility as string) ?? 'requested_only'
      if (eligibility === 'admin_selected') {
        return { count: 0, eligibleTotal: 0, mentorPoolSize: 0 }
      }

      const eligibilityWhere =
        eligibility === 'requested_only' ? { wantsMentorship: true } : {}

      // Mirror autoAssignBulkForRound's filter exactly so the toolbar count
      // matches what the auto-fill button will actually process.
      const autoFillWhere = {
        mentorAssignments: { none: { droppedAt: null } },
        finalistConfirmation: { status: 'CONFIRMED' as const },
        ...eligibilityWhere,
      }
      const [count, eligibleTotal, mentorPoolSize] = await Promise.all([
        ctx.prisma.projectRoundState.count({
          where: { roundId: input.roundId, project: autoFillWhere },
        }),
        ctx.prisma.projectRoundState.count({
          where: {
            roundId: input.roundId,
            project: {
              finalistConfirmation: { status: 'CONFIRMED' as const },
              ...eligibilityWhere,
            },
          },
        }),
        ctx.prisma.user.count({
          where: { roles: { has: 'MENTOR' }, status: { not: 'SUSPENDED' } },
        }),
      ])
      return { count, eligibleTotal, mentorPoolSize }
    }),

  /**
   * For a MENTORING round, find the immediately-prior round in the same
   * competition and report how many of its PASSED projects are not yet
   * present in this round. Drives the "Import from prior round" CTA so
   * admins don't have to manually pick projects via the From-Round modal.
   */
  getMentoringImportCandidates: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { roundType: true, competitionId: true, sortOrder: true },
      })
      if (round.roundType !== 'MENTORING') {
        return { priorRound: null, pendingCount: 0 }
      }
      const prior = await ctx.prisma.round.findFirst({
        where: {
          competitionId: round.competitionId,
          sortOrder: { lt: round.sortOrder },
        },
        orderBy: { sortOrder: 'desc' },
        select: { id: true, name: true, status: true },
      })
      if (!prior) return { priorRound: null, pendingCount: 0 }
      if (prior.status !== 'ROUND_ACTIVE' && prior.status !== 'ROUND_CLOSED') {
        return {
          priorRound: { id: prior.id, name: prior.name, status: prior.status },
          pendingCount: 0,
        }
      }
      const existingInTarget = await ctx.prisma.projectRoundState.findMany({
        where: { roundId: input.roundId },
        select: { projectId: true },
      })
      const existingIds = new Set(existingInTarget.map((s) => s.projectId))
      const passedInPrior = await ctx.prisma.projectRoundState.findMany({
        where: { roundId: prior.id, state: 'PASSED' },
        select: { projectId: true },
      })
      const pendingCount = passedInPrior.filter(
        (s) => !existingIds.has(s.projectId),
      ).length
      return {
        priorRound: { id: prior.id, name: prior.name, status: prior.status },
        pendingCount,
      }
    }),

  /**
   * List projects in a MENTORING round with their (multi-)mentor assignments.
   * Drives the per-team assignment table on the round Projects tab so admins
   * can see who is assigned to whom and add/swap mentors per project.
   */
  listMentoringProjects: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { roundType: true, configJson: true },
      })
      if (round.roundType !== 'MENTORING') return { projects: [] }

      const config = (round.configJson ?? {}) as Record<string, unknown>
      const eligibility = (config.eligibility as string) ?? 'requested_only'

      const states = await ctx.prisma.projectRoundState.findMany({
        where: { roundId: input.roundId },
        select: {
          state: true,
          project: {
            select: {
              id: true,
              title: true,
              teamName: true,
              country: true,
              wantsMentorship: true,
              competitionCategory: true,
              finalistConfirmation: { select: { status: true } },
              mentorAssignments: {
                where: { droppedAt: null },
                select: {
                  id: true,
                  method: true,
                  assignedAt: true,
                  mentor: { select: { id: true, name: true, email: true } },
                },
                orderBy: { assignedAt: 'asc' },
              },
            },
          },
        },
        orderBy: [{ project: { title: 'asc' } }],
      })

      return {
        eligibility,
        projects: states.map((s) => {
          const isEligible =
            eligibility === 'all_in_round' ||
            eligibility === 'admin_selected' ||
            s.project.wantsMentorship
          return {
            id: s.project.id,
            title: s.project.title,
            teamName: s.project.teamName,
            country: s.project.country,
            competitionCategory: s.project.competitionCategory,
            wantsMentorship: s.project.wantsMentorship,
            finalistConfirmationStatus:
              s.project.finalistConfirmation?.status ?? null,
            isEligible,
            state: s.state,
            mentors: s.project.mentorAssignments.map((a) => ({
              assignmentId: a.id,
              method: a.method,
              assignedAt: a.assignedAt,
              id: a.mentor.id,
              name: a.mentor.name,
              email: a.mentor.email,
            })),
          }
        }),
      }
    }),

  /**
   * Delete a round
   */
  delete: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.prisma.round.findUniqueOrThrow({ where: { id: input.id } })

      await ctx.prisma.round.delete({ where: { id: input.id } })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'Round',
        entityId: input.id,
        detailsJson: {
          name: existing.name,
          roundType: existing.roundType,
          competitionId: existing.competitionId,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return existing
    }),

  // =========================================================================
  // Project Advancement (Manual Only)
  // =========================================================================

  /**
   * Advance PASSED projects from one round to the next.
   * This is ALWAYS manual — no auto-advancement after AI filtering.
   * Admin must explicitly trigger this after reviewing results.
   */
  advanceProjects: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        targetRoundId: z.string().optional(),
        projectIds: z.array(z.string()).optional(),
        autoPassPending: z.boolean().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { roundId, targetRoundId, projectIds, autoPassPending } = input

      // Get current round with competition context + status
      const currentRound = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: roundId },
        select: { id: true, name: true, competitionId: true, sortOrder: true, status: true, configJson: true },
      })

      // Validate: current round must be ROUND_ACTIVE or ROUND_CLOSED
      if (currentRound.status !== 'ROUND_ACTIVE' && currentRound.status !== 'ROUND_CLOSED') {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: `Cannot advance from round with status ${currentRound.status}. Round must be ROUND_ACTIVE or ROUND_CLOSED.`,
        })
      }

      // Determine target round
      let targetRound: { id: string; name: string; competitionId: string; sortOrder: number; configJson: unknown }
      if (targetRoundId) {
        targetRound = await ctx.prisma.round.findUniqueOrThrow({
          where: { id: targetRoundId },
          select: { id: true, name: true, competitionId: true, sortOrder: true, configJson: true },
        })

        // Validate: target must be in same competition
        if (targetRound.competitionId !== currentRound.competitionId) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Target round must belong to the same competition as the source round.',
          })
        }

        // Validate: target must be after current round
        if (targetRound.sortOrder <= currentRound.sortOrder) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Target round must come after the current round (higher sortOrder).',
          })
        }
      } else {
        // Find next round in same competition by sortOrder
        const nextRound = await ctx.prisma.round.findFirst({
          where: {
            competitionId: currentRound.competitionId,
            sortOrder: { gt: currentRound.sortOrder },
          },
          orderBy: { sortOrder: 'asc' },
          select: { id: true, name: true, competitionId: true, sortOrder: true, configJson: true },
        })
        if (!nextRound) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'No subsequent round exists in this competition. Create the next round first.',
          })
        }
        targetRound = nextRound
      }

      // Validate projectIds exist in current round if provided
      if (projectIds && projectIds.length > 0) {
        const existingStates = await ctx.prisma.projectRoundState.findMany({
          where: { roundId, projectId: { in: projectIds } },
          select: { projectId: true },
        })
        const existingIds = new Set(existingStates.map((s) => s.projectId))
        const missing = projectIds.filter((id) => !existingIds.has(id))
        if (missing.length > 0) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: `Projects not found in current round: ${missing.join(', ')}`,
          })
        }
      }

      // Transaction: auto-pass + create entries in target round + mark current as COMPLETED
      let autoPassedCount = 0
      let idsToAdvance: string[]

      await ctx.prisma.$transaction(async (tx) => {
        // Auto-pass all PENDING projects first (for intake/bulk workflows) — inside tx
        if (autoPassPending) {
          const result = await tx.projectRoundState.updateMany({
            where: { roundId, state: 'PENDING' },
            data: { state: 'PASSED' },
          })
          autoPassedCount = result.count
        }

        // Determine which projects to advance
        if (projectIds && projectIds.length > 0) {
          idsToAdvance = projectIds
        } else {
          // Default: all PASSED projects in current round
          const passedStates = await tx.projectRoundState.findMany({
            where: { roundId, state: 'PASSED' },
            select: { projectId: true },
          })
          idsToAdvance = passedStates.map((s) => s.projectId)
        }

        if (idsToAdvance.length === 0) return

        // Create ProjectRoundState in target round
        await tx.projectRoundState.createMany({
          data: idsToAdvance.map((projectId) => ({
            projectId,
            roundId: targetRound.id,
          })),
          skipDuplicates: true,
        })

        // Mark current round states as COMPLETED
        await tx.projectRoundState.updateMany({
          where: {
            roundId,
            projectId: { in: idsToAdvance },
            state: 'PASSED',
          },
          data: { state: 'COMPLETED' },
        })

        // Update project status to ASSIGNED
        await tx.project.updateMany({
          where: { id: { in: idsToAdvance } },
          data: { status: 'ASSIGNED' },
        })

        // Status history
        await tx.projectStatusHistory.createMany({
          data: idsToAdvance.map((projectId) => ({
            projectId,
            status: 'ASSIGNED',
            changedBy: ctx.user?.id,
          })),
        })
      })

      // If nothing to advance (set inside tx), return early
      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
      if (!idsToAdvance! || idsToAdvance!.length === 0) {
        return { advancedCount: 0, autoPassedCount, targetRoundId: targetRound.id, targetRoundName: targetRound.name }
      }

      // Audit
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'ADVANCE_PROJECTS',
        entityType: 'Round',
        entityId: roundId,
        detailsJson: {
          fromRound: currentRound.name,
          toRound: targetRound.name,
          targetRoundId: targetRound.id,
          projectCount: idsToAdvance!.length,
          autoPassedCount,
          projectIds: idsToAdvance!,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return {
        advancedCount: idsToAdvance!.length,
        autoPassedCount,
        targetRoundId: targetRound.id,
        targetRoundName: targetRound.name,
      }
    }),

  // =========================================================================
  // AI Shortlist Recommendations
  // =========================================================================

  /**
   * Generate AI-powered shortlist recommendations for a round.
   * Runs independently for STARTUP and BUSINESS_CONCEPT categories.
   * Uses per-round config for advancement targets and file parsing.
   */
  generateAIRecommendations: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        rubric: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: {
          id: true,
          name: true,
          competitionId: true,
          configJson: true,
        },
      })

      const config = (round.configJson as Record<string, unknown>) ?? {}
      const startupTopN = (config.startupAdvanceCount as number) || 10
      const conceptTopN = (config.conceptAdvanceCount as number) || 10
      const aiParseFiles = !!config.aiParseFiles

      const result = await generateShortlist(
        {
          roundId: input.roundId,
          competitionId: round.competitionId,
          startupTopN,
          conceptTopN,
          rubric: input.rubric,
          aiParseFiles,
        },
        ctx.prisma,
      )

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'AI_SHORTLIST',
        entityType: 'Round',
        entityId: input.roundId,
        detailsJson: {
          roundName: round.name,
          startupTopN,
          conceptTopN,
          aiParseFiles,
          success: result.success,
          startupCount: result.recommendations.STARTUP.length,
          conceptCount: result.recommendations.BUSINESS_CONCEPT.length,
          tokensUsed: result.tokensUsed,
          errors: result.errors,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return result
    }),

  // =========================================================================
  // Submission Window Management
  // =========================================================================

  /**
   * Create a submission window for a round
   */
  createSubmissionWindow: adminProcedure
    .input(
      z.object({
        competitionId: z.string(),
        name: z.string().min(1).max(255),
        slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
        roundNumber: z.number().int().min(1),
        windowOpenAt: z.date().optional(),
        windowCloseAt: z.date().optional(),
        deadlinePolicy: z.enum(['HARD_DEADLINE', 'FLAG', 'GRACE']).default('HARD_DEADLINE'),
        graceHours: z.number().int().min(0).optional(),
        lockOnClose: z.boolean().default(true),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const window = await ctx.prisma.submissionWindow.create({
        data: {
          competitionId: input.competitionId,
          name: input.name,
          slug: input.slug,
          roundNumber: input.roundNumber,
          windowOpenAt: input.windowOpenAt,
          windowCloseAt: input.windowCloseAt,
          deadlinePolicy: input.deadlinePolicy,
          graceHours: input.graceHours,
          lockOnClose: input.lockOnClose,
        },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'SubmissionWindow',
        entityId: window.id,
        detailsJson: { name: input.name, competitionId: input.competitionId },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return window
    }),

  /**
   * Update an existing submission window
   */
  updateSubmissionWindow: adminProcedure
    .input(
      z.object({
        id: z.string(),
        name: z.string().min(1).max(255).optional(),
        slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional(),
        roundNumber: z.number().int().min(1).optional(),
        windowOpenAt: z.date().nullable().optional(),
        windowCloseAt: z.date().nullable().optional(),
        deadlinePolicy: z.enum(['HARD_DEADLINE', 'FLAG', 'GRACE']).optional(),
        graceHours: z.number().int().min(0).nullable().optional(),
        lockOnClose: z.boolean().optional(),
        sortOrder: z.number().int().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input
      const window = await ctx.prisma.submissionWindow.update({
        where: { id },
        data,
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPDATE',
        entityType: 'SubmissionWindow',
        entityId: id,
        detailsJson: data,
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })
      return window
    }),

  /**
   * Delete a submission window (only if no files uploaded)
   */
  deleteSubmissionWindow: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      // Check if window has uploaded files
      const window = await ctx.prisma.submissionWindow.findUniqueOrThrow({
        where: { id: input.id },
        select: { id: true, name: true, _count: { select: { projectFiles: true } } },
      })
      if (window._count.projectFiles > 0) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: `Cannot delete window "${window.name}" — it has ${window._count.projectFiles} uploaded files. Remove files first.`,
        })
      }
      await ctx.prisma.submissionWindow.delete({ where: { id: input.id } })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'SubmissionWindow',
        entityId: input.id,
        detailsJson: { name: window.name },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })
      return { success: true }
    }),

  /**
   * Open a submission window
   */
  openSubmissionWindow: adminProcedure
    .input(z.object({ windowId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const result = await openWindow(input.windowId, ctx.user.id, ctx.prisma)
      if (!result.success) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: result.errors?.join('; ') ?? 'Failed to open window',
        })
      }
      return result
    }),

  /**
   * Close a submission window
   */
  closeSubmissionWindow: adminProcedure
    .input(z.object({ windowId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const result = await closeWindow(input.windowId, ctx.user.id, ctx.prisma)
      if (!result.success) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: result.errors?.join('; ') ?? 'Failed to close window',
        })
      }
      return result
    }),

  /**
   * Lock a submission window
   */
  lockSubmissionWindow: adminProcedure
    .input(z.object({ windowId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const result = await lockWindow(input.windowId, ctx.user.id, ctx.prisma)
      if (!result.success) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: result.errors?.join('; ') ?? 'Failed to lock window',
        })
      }
      return result
    }),

  /**
   * Check deadline status of a window
   */
  checkDeadline: protectedProcedure
    .input(z.object({ windowId: z.string() }))
    .query(async ({ ctx, input }) => {
      return checkDeadlinePolicy(input.windowId, ctx.prisma)
    }),

  /**
   * Validate files against window requirements
   */
  validateSubmission: protectedProcedure
    .input(
      z.object({
        projectId: z.string(),
        windowId: z.string(),
        files: z.array(
          z.object({
            mimeType: z.string(),
            size: z.number(),
            requirementId: z.string().optional(),
          })
        ),
      })
    )
    .mutation(async ({ ctx, input }) => {
      return validateSubmission(input.projectId, input.windowId, input.files, ctx.prisma)
    }),

  /**
   * Get visible submission windows for a round
   */
  getVisibleWindows: protectedProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      return getVisibleWindows(input.roundId, ctx.prisma)
    }),

  // =========================================================================
  // File Requirements Management
  // =========================================================================

  /**
   * Create a file requirement for a submission window
   */
  createFileRequirement: adminProcedure
    .input(
      z.object({
        submissionWindowId: z.string(),
        slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
        label: z.string().min(1).max(255),
        description: z.string().max(2000).optional(),
        mimeTypes: z.array(z.string()).default([]),
        maxSizeMb: z.number().int().min(0).optional(),
        required: z.boolean().default(false),
        sortOrder: z.number().int().default(0),
      })
    )
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.submissionFileRequirement.create({
        data: input,
      })
    }),

  /**
   * Update a file requirement
   */
  updateFileRequirement: adminProcedure
    .input(
      z.object({
        id: z.string(),
        label: z.string().min(1).max(255).optional(),
        description: z.string().max(2000).optional().nullable(),
        mimeTypes: z.array(z.string()).optional(),
        maxSizeMb: z.number().min(0).optional().nullable(),
        required: z.boolean().optional(),
        sortOrder: z.number().int().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input
      return ctx.prisma.submissionFileRequirement.update({
        where: { id },
        data,
      })
    }),

  /**
   * Delete a file requirement
   */
  deleteFileRequirement: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.prisma.submissionFileRequirement.delete({
        where: { id: input.id },
      })
    }),

  /**
   * Get submission windows for applicants in a competition
   */
  getApplicantWindows: protectedProcedure
    .input(z.object({ competitionId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.submissionWindow.findMany({
        where: { competitionId: input.competitionId },
        include: {
          fileRequirements: { orderBy: { sortOrder: 'asc' } },
        },
        orderBy: { sortOrder: 'asc' },
      })
    }),

  /**
   * Get the most recent SUBMISSION round config for a program.
   * Used on the project edit page to show required document slots.
   */
  getSubmissionRoundForProgram: adminProcedure
    .input(z.object({ programId: z.string() }))
    .query(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findFirst({
        where: {
          roundType: 'SUBMISSION',
          competition: { programId: input.programId },
        },
        select: {
          id: true,
          name: true,
          configJson: true,
        },
        orderBy: { sortOrder: 'desc' },
      })
      return round ?? null
    }),

  // =========================================================================
  // Notification Procedures
  // =========================================================================

  previewAdvancementEmail: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        targetRoundId: z.string().optional(),
        customMessage: z.string().optional(),
        fullCustomBody: z.boolean().default(false),
      })
    )
    .query(async ({ ctx, input }) => {
      const { roundId, targetRoundId, customMessage, fullCustomBody } = input

      const currentRound = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: roundId },
        select: { name: true, competition: { select: { rounds: { select: { id: true, name: true, sortOrder: true }, orderBy: { sortOrder: 'asc' } } } } },
      })

      // Determine target round name
      const rounds = currentRound.competition.rounds
      const currentIdx = rounds.findIndex((r) => r.id === roundId)
      const targetRound = targetRoundId
        ? rounds.find((r) => r.id === targetRoundId)
        : rounds[currentIdx + 1]
      const toRoundName = targetRound?.name ?? 'Next Round'

      // Count recipients: team members of PASSED or COMPLETED projects in this round
      const projectStates = await ctx.prisma.projectRoundState.findMany({
        where: { roundId, state: { in: ['PASSED', 'COMPLETED'] } },
        select: { projectId: true },
      })
      const projectIds = projectStates.map((ps) => ps.projectId)

      let recipientCount = 0
      if (projectIds.length > 0) {
        const teamMembers = await ctx.prisma.teamMember.findMany({
          where: { projectId: { in: projectIds } },
          select: { user: { select: { email: true } } },
        })
        const emails = new Set(teamMembers.map((tm) => tm.user.email).filter(Boolean))

        // Also count submittedByEmail for projects without team member emails
        const projects = await ctx.prisma.project.findMany({
          where: { id: { in: projectIds } },
          select: { submittedByEmail: true, teamMembers: { select: { user: { select: { email: true } } } } },
        })
        for (const p of projects) {
          const hasTeamEmail = p.teamMembers.some((tm) => tm.user.email)
          if (!hasTeamEmail && p.submittedByEmail) {
            emails.add(p.submittedByEmail)
          }
        }
        recipientCount = emails.size
      }

      // Build preview HTML
      const template = getAdvancementNotificationTemplate(
        'Team Member',
        'Your Project',
        currentRound.name,
        toRoundName,
        customMessage || undefined,
        undefined,
        fullCustomBody,
      )

      return { html: template.html, subject: template.subject, recipientCount }
    }),

  sendAdvancementNotifications: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        targetRoundId: z.string().optional(),
        customMessage: z.string().optional(),
        fullCustomBody: z.boolean().default(false),
        projectIds: z.array(z.string()).optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { roundId, targetRoundId, customMessage, fullCustomBody } = input

      const currentRound = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: roundId },
        select: { name: true, competition: { select: { rounds: { select: { id: true, name: true, sortOrder: true }, orderBy: { sortOrder: 'asc' } } } } },
      })

      const rounds = currentRound.competition.rounds
      const currentIdx = rounds.findIndex((r) => r.id === roundId)
      const targetRound = targetRoundId
        ? rounds.find((r) => r.id === targetRoundId)
        : rounds[currentIdx + 1]
      const toRoundName = targetRound?.name ?? 'Next Round'

      // Get target projects
      let projectIds = input.projectIds
      if (!projectIds) {
        const projectStates = await ctx.prisma.projectRoundState.findMany({
          where: { roundId, state: { in: ['PASSED', 'COMPLETED'] } },
          select: { projectId: true },
        })
        projectIds = projectStates.map((ps) => ps.projectId)
      }

      if (projectIds.length === 0) {
        return { sent: 0, failed: 0 }
      }

      // Fetch projects with team members
      const projects = await ctx.prisma.project.findMany({
        where: { id: { in: projectIds } },
        select: {
          id: true,
          title: true,
          submittedByEmail: true,
          teamMembers: {
            select: { user: { select: { id: true, email: true, name: true } } },
          },
        },
      })

      const allUserIds = new Set<string>()
      const items: NotificationItem[] = []

      for (const project of projects) {
        const recipients = new Map<string, { name: string | null; userId: string }>()
        for (const tm of project.teamMembers) {
          if (tm.user.email) {
            recipients.set(tm.user.email, { name: tm.user.name, userId: tm.user.id })
            allUserIds.add(tm.user.id)
          }
        }
        if (recipients.size === 0 && project.submittedByEmail) {
          recipients.set(project.submittedByEmail, { name: null, userId: '' })
        }

        for (const [email, { name, userId }] of recipients) {
          items.push({
            email,
            name: name || '',
            type: 'ADVANCEMENT_NOTIFICATION',
            context: {
              title: 'Your project has advanced!',
              message: '',
              linkUrl: '/applicant',
              metadata: {
                projectName: project.title,
                fromRoundName: currentRound.name,
                toRoundName,
                customMessage: customMessage || undefined,
                fullCustomBody,
              },
            },
            projectId: project.id,
            userId: userId || undefined,
            roundId,
          })
        }
      }

      const result = await sendBatchNotifications(items)

      // Create in-app notifications
      if (allUserIds.size > 0) {
        void createBulkNotifications({
          userIds: [...allUserIds],
          type: 'project_advanced',
          title: 'Your project has advanced!',
          message: `Your project has advanced from "${currentRound.name}" to "${toRoundName}".`,
          linkUrl: '/applicant',
          linkLabel: 'View Dashboard',
          icon: 'Trophy',
          priority: 'high',
        })
      }

      // Audit
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user?.id,
        action: 'SEND_ADVANCEMENT_NOTIFICATIONS',
        entityType: 'Round',
        entityId: roundId,
        detailsJson: { sent: result.sent, failed: result.failed, projectCount: projectIds.length, customMessage: !!customMessage },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { sent: result.sent, failed: result.failed }
    }),

  previewRejectionEmail: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        customMessage: z.string().optional(),
        fullCustomBody: z.boolean().default(false),
      })
    )
    .query(async ({ ctx, input }) => {
      const { roundId, customMessage, fullCustomBody } = input

      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: roundId },
        select: { name: true, roundType: true },
      })

      // For FILTERING rounds, also count projects filtered out via FilteringResult
      let projectIds: string[]
      if (round.roundType === 'FILTERING') {
        const fromPRS = await ctx.prisma.projectRoundState.findMany({
          where: { roundId, state: 'REJECTED' },
          select: { projectId: true },
        })
        const fromFR = await ctx.prisma.filteringResult.findMany({
          where: { roundId, finalOutcome: 'FILTERED_OUT' },
          select: { projectId: true },
        })
        projectIds = [...new Set([...fromPRS, ...fromFR].map((p) => p.projectId))]
      } else {
        const projectStates = await ctx.prisma.projectRoundState.findMany({
          where: { roundId, state: 'REJECTED' },
          select: { projectId: true },
        })
        projectIds = projectStates.map((ps) => ps.projectId)
      }

      let recipientCount = 0
      if (projectIds.length > 0) {
        const projects = await ctx.prisma.project.findMany({
          where: { id: { in: projectIds } },
          select: { submittedByEmail: true, teamMembers: { select: { user: { select: { email: true } } } } },
        })
        const emails = new Set<string>()
        for (const p of projects) {
          const hasTeamEmail = p.teamMembers.some((tm) => tm.user.email)
          if (hasTeamEmail) {
            for (const tm of p.teamMembers) {
              if (tm.user.email) emails.add(tm.user.email)
            }
          } else if (p.submittedByEmail) {
            emails.add(p.submittedByEmail)
          }
        }
        recipientCount = emails.size
      }

      const template = getRejectionNotificationTemplate(
        'Team Member',
        'Your Project',
        round.name,
        customMessage || undefined,
        fullCustomBody,
      )

      return { html: template.html, subject: template.subject, recipientCount }
    }),

  sendRejectionNotifications: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        customMessage: z.string().optional(),
        fullCustomBody: z.boolean().default(false),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { roundId, customMessage, fullCustomBody } = input

      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: roundId },
        select: { name: true, roundType: true },
      })

      // For FILTERING rounds, also include projects filtered out via FilteringResult
      let projectIds: string[]
      if (round.roundType === 'FILTERING') {
        const fromPRS = await ctx.prisma.projectRoundState.findMany({
          where: { roundId, state: 'REJECTED' },
          select: { projectId: true },
        })
        const fromFR = await ctx.prisma.filteringResult.findMany({
          where: { roundId, finalOutcome: 'FILTERED_OUT' },
          select: { projectId: true },
        })
        projectIds = [...new Set([...fromPRS, ...fromFR].map((p) => p.projectId))]
      } else {
        const projectStates = await ctx.prisma.projectRoundState.findMany({
          where: { roundId, state: 'REJECTED' },
          select: { projectId: true },
        })
        projectIds = projectStates.map((ps) => ps.projectId)
      }

      if (projectIds.length === 0) {
        return { sent: 0, failed: 0 }
      }

      const projects = await ctx.prisma.project.findMany({
        where: { id: { in: projectIds } },
        select: {
          id: true,
          title: true,
          submittedByEmail: true,
          teamMembers: {
            select: { user: { select: { id: true, email: true, name: true } } },
          },
        },
      })

      const allUserIds = new Set<string>()
      const items: NotificationItem[] = []

      for (const project of projects) {
        const recipients = new Map<string, { name: string | null; userId: string }>()
        for (const tm of project.teamMembers) {
          if (tm.user.email) {
            recipients.set(tm.user.email, { name: tm.user.name, userId: tm.user.id })
            allUserIds.add(tm.user.id)
          }
        }
        if (recipients.size === 0 && project.submittedByEmail) {
          recipients.set(project.submittedByEmail, { name: null, userId: '' })
        }

        for (const [email, { name, userId }] of recipients) {
          items.push({
            email,
            name: name || '',
            type: 'REJECTION_NOTIFICATION',
            context: {
              title: 'Update on your application',
              message: '',
              linkUrl: '/applicant',
              metadata: {
                projectName: project.title,
                roundName: round.name,
                customMessage: customMessage || undefined,
                fullCustomBody,
              },
            },
            projectId: project.id,
            userId: userId || undefined,
            roundId,
          })
        }
      }

      const result = await sendBatchNotifications(items)

      // In-app notifications
      if (allUserIds.size > 0) {
        void createBulkNotifications({
          userIds: [...allUserIds],
          type: 'NOT_SELECTED',
          title: 'Update on your application',
          message: `Your project was not selected to advance from "${round.name}".`,
          linkUrl: '/applicant',
          linkLabel: 'View Dashboard',
          icon: 'Info',
        })
      }

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user?.id,
        action: 'SEND_REJECTION_NOTIFICATIONS',
        entityType: 'Round',
        entityId: roundId,
        detailsJson: { sent: result.sent, failed: result.failed, projectCount: projectIds.length, customMessage: !!customMessage },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { sent: result.sent, failed: result.failed }
    }),

  getBulkInvitePreview: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const { roundId } = input

      // Get all projects in this round
      const projectStates = await ctx.prisma.projectRoundState.findMany({
        where: { roundId },
        select: { projectId: true },
      })
      const projectIds = projectStates.map((ps) => ps.projectId)

      if (projectIds.length === 0) {
        return { uninvitedCount: 0, totalTeamMembers: 0, alreadyInvitedCount: 0 }
      }

      // Get all team members for these projects
      const teamMembers = await ctx.prisma.teamMember.findMany({
        where: { projectId: { in: projectIds } },
        select: { user: { select: { id: true, status: true } } },
      })

      // Deduplicate by user ID
      const userMap = new Map<string, string>()
      for (const tm of teamMembers) {
        userMap.set(tm.user.id, tm.user.status)
      }

      let uninvitedCount = 0
      let alreadyInvitedCount = 0
      for (const [, status] of userMap) {
        if (status === 'ACTIVE' || status === 'INVITED') {
          alreadyInvitedCount++
        } else {
          uninvitedCount++
        }
      }

      return {
        uninvitedCount,
        totalTeamMembers: userMap.size,
        alreadyInvitedCount,
      }
    }),

  bulkInviteTeamMembers: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const { roundId } = input

      // Get all projects in this round
      const projectStates = await ctx.prisma.projectRoundState.findMany({
        where: { roundId },
        select: { projectId: true },
      })
      const projectIds = projectStates.map((ps) => ps.projectId)

      if (projectIds.length === 0) {
        return { invited: 0, skipped: 0, failed: 0 }
      }

      // Get all team members with user details
      const teamMembers = await ctx.prisma.teamMember.findMany({
        where: { projectId: { in: projectIds } },
        select: {
          user: { select: { id: true, email: true, name: true, status: true, role: true } },
        },
      })

      // Deduplicate by user ID
      const users = new Map<string, { id: string; email: string; name: string | null; status: string; role: string }>()
      for (const tm of teamMembers) {
        if (tm.user.email && !users.has(tm.user.id)) {
          users.set(tm.user.id, tm.user)
        }
      }

      const baseUrl = getBaseUrl()
      const expiryHours = await getInviteExpiryHours(ctx.prisma as unknown as import('@prisma/client').PrismaClient)
      const expiryMs = expiryHours * 60 * 60 * 1000

      let skipped = 0
      let failed = 0

      // Phase 1: Batch all DB writes — generate tokens and update users
      const toInvite: Array<{ id: string; email: string; name: string | null; role: string; token: string }> = []
      for (const [, user] of users) {
        if (user.status === 'ACTIVE' || user.status === 'INVITED') {
          skipped++
          continue
        }
        toInvite.push({ ...user, token: generateInviteToken() })
      }

      if (toInvite.length > 0) {
        await ctx.prisma.$transaction(
          toInvite.map((u) =>
            ctx.prisma.user.update({
              where: { id: u.id },
              data: {
                status: 'INVITED',
                inviteToken: u.token,
                inviteTokenExpiresAt: new Date(Date.now() + expiryMs),
              },
            })
          )
        )
      }

      // Phase 2: Send emails with concurrency pool of 10
      const CONCURRENCY = 10
      let invited = 0

      for (let i = 0; i < toInvite.length; i += CONCURRENCY) {
        const batch = toInvite.slice(i, i + CONCURRENCY)
        const results = await Promise.allSettled(
          batch.map((u) => {
            const inviteUrl = `${baseUrl}/accept-invite?token=${u.token}`
            return sendInvitationEmail(u.email, u.name, inviteUrl, u.role, expiryHours)
          })
        )
        for (const result of results) {
          if (result.status === 'fulfilled') {
            invited++
          } else {
            console.error('[bulkInviteTeamMembers] Email send failed:', result.reason)
            failed++
          }
        }
      }

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user?.id,
        action: 'BULK_INVITE_TEAM_MEMBERS',
        entityType: 'Round',
        entityId: roundId,
        detailsJson: { invited, skipped, failed, totalUsers: users.size },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { invited, skipped, failed }
    }),
})
