import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { router, protectedProcedure, adminProcedure, juryProcedure, userHasRole, withAIRateLimit } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import { notifyAdmins, NotificationTypes } from '../services/in-app-notification'
import { reassignAfterCOI } from '../services/juror-reassignment'
import { sendManualReminders } from '../services/evaluation-reminders'
import { generateSummary } from '@/server/services/ai-evaluation-summary'
import { quickRank as aiQuickRank } from '../services/ai-ranking'
import type { EvaluationConfig } from '@/types/competition-configs'
import type { PrismaClient } from '@prisma/client'
import { findActiveForm } from '@/server/utils/evaluation-form-lookup'
import { triggerInProgressOnActivity, checkEvaluationCompletionAndTransition } from '../services/round-engine'

/**
 * Auto-trigger AI ranking if all required assignments for the round are complete.
 * MUST be called fire-and-forget (void). Never awaited from submission mutation.
 * Implements RANK-09 cooldown guard (5-minute window).
 */
async function triggerAutoRankIfComplete(
  roundId: string,
  prisma: PrismaClient,
  userId: string,
): Promise<void> {
  try {
    // 1. Check if round is fully evaluated
    const [total, completed] = await Promise.all([
      prisma.assignment.count({ where: { roundId, isRequired: true } }),
      prisma.assignment.count({ where: { roundId, isRequired: true, isCompleted: true } }),
    ])
    if (total === 0 || total !== completed) return

    // 2. Check round config for auto-ranking settings
    const round = await prisma.round.findUnique({
      where: { id: roundId },
      select: { id: true, name: true, configJson: true, competition: { select: { id: true, name: true } } },
    })
    if (!round) return

    const config = (round.configJson as EvaluationConfig | null) ?? ({} as EvaluationConfig)
    const criteriaText = config?.rankingCriteria ?? null
    const autoRankEnabled = config?.autoRankOnComplete ?? false

    if (!autoRankEnabled || !criteriaText) {
      // Auto-ranking not configured for this round — skip silently
      return
    }

    // 3. Cooldown check: skip if AUTO snapshot created in last 5 minutes
    const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000)
    const recentSnapshot = await prisma.rankingSnapshot.findFirst({
      where: { roundId, triggerType: 'AUTO', createdAt: { gte: fiveMinutesAgo } },
      select: { id: true },
    })
    if (recentSnapshot) {
      // Duplicate auto-trigger within cooldown window — skip
      return
    }

    // 4. Execute ranking (takes 10-30s, do not await from submission)
    const result = await aiQuickRank(criteriaText, roundId, prisma, userId)

    // 5. Save snapshot
    await prisma.rankingSnapshot.create({
      data: {
        roundId,
        triggeredById: null, // auto-triggered, no specific user
        triggerType: 'AUTO',
        criteriaText,
        parsedRulesJson: result.parsedRules as unknown as import('@prisma/client').Prisma.InputJsonValue,
        startupRankingJson: result.startup.rankedProjects as unknown as import('@prisma/client').Prisma.InputJsonValue,
        conceptRankingJson: result.concept.rankedProjects as unknown as import('@prisma/client').Prisma.InputJsonValue,
        mode: 'QUICK',
        status: 'COMPLETED',
      },
    })

    // 6. Notify admins of success
    const startupCount = result.startup.rankedProjects.length
    const conceptCount = result.concept.rankedProjects.length
    await notifyAdmins({
      type: NotificationTypes.AI_RANKING_COMPLETE,
      title: 'AI Ranking Complete',
      message: `Rankings generated for "${round.name}" — ${startupCount} STARTUP, ${conceptCount} BUSINESS_CONCEPT projects ranked.`,
      linkUrl: `/admin/competitions/${round.competition.id}/rounds/${roundId}?tab=ranking`,
      linkLabel: 'View Rankings',
      priority: 'normal',
    })
  } catch (error) {
    // Auto-trigger must never crash the calling mutation
    try {
      await notifyAdmins({
        type: NotificationTypes.AI_RANKING_FAILED,
        title: 'AI Ranking Failed',
        message: `Auto-ranking failed for round (ID: ${roundId}). Please trigger manually.`,
        priority: 'high',
      })
    } catch (err) {
      console.error('Failed to send AI ranking failure notification to admins:', err)
      // Even notification failure must not propagate
    }
    console.error('[auto-rank] triggerAutoRankIfComplete failed:', error)
  }
}

export const evaluationRouter = router({
  /**
   * Get evaluation for an assignment
   */
  get: protectedProcedure
    .input(z.object({ assignmentId: z.string() }))
    .query(async ({ ctx, input }) => {
      // Verify ownership or admin
      const assignment = await ctx.prisma.assignment.findUniqueOrThrow({
        where: { id: input.assignmentId },
      })

      if (
        userHasRole(ctx.user, 'JURY_MEMBER') &&
        !userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN') &&
        assignment.userId !== ctx.user.id
      ) {
        throw new TRPCError({ code: 'FORBIDDEN' })
      }

      return ctx.prisma.evaluation.findUnique({
        where: { assignmentId: input.assignmentId },
        include: {
          form: true,
        },
      })
    }),

  /**
   * Start an evaluation (creates draft)
   */
  start: protectedProcedure
    .input(
      z.object({
        assignmentId: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Verify assignment ownership
      const assignment = await ctx.prisma.assignment.findUniqueOrThrow({
        where: { id: input.assignmentId },
      })

      if (assignment.userId !== ctx.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' })
      }

      // Get active form for this stage
      const form = await ctx.prisma.evaluationForm.findFirst({
        where: { roundId: assignment.roundId, isActive: true },
      })
      if (!form) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'No active evaluation form for this stage',
        })
      }

      // Check if evaluation exists
      const existing = await ctx.prisma.evaluation.findUnique({
        where: { assignmentId: input.assignmentId },
      })

      if (existing) return existing

      return ctx.prisma.evaluation.create({
        data: {
          assignmentId: input.assignmentId,
          formId: form.id,
          status: 'DRAFT',
        },
      })
    }),

  /**
   * Autosave evaluation (debounced on client)
   */
  autosave: protectedProcedure
    .input(
      z.object({
        id: z.string(),
        criterionScoresJson: z.record(z.union([z.number(), z.string(), z.boolean()])).optional(),
        globalScore: z.number().int().min(1).max(10).optional().nullable(),
        binaryDecision: z.boolean().optional().nullable(),
        feedbackText: z.string().optional().nullable(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input

      // Verify ownership and status
      const evaluation = await ctx.prisma.evaluation.findUniqueOrThrow({
        where: { id },
        include: { assignment: true },
      })

      if (evaluation.assignment.userId !== ctx.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' })
      }

      if (
        evaluation.status === 'SUBMITTED' ||
        evaluation.status === 'LOCKED'
      ) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Cannot edit submitted evaluation',
        })
      }

      return ctx.prisma.evaluation.update({
        where: { id },
        data: {
          ...data,
          status: 'DRAFT',
        },
      })
    }),

  /**
   * Submit evaluation (final)
   */
  submit: protectedProcedure
    .input(
      z.object({
        id: z.string(),
        criterionScoresJson: z.record(z.union([z.number(), z.string(), z.boolean()])),
        globalScore: z.number().int().min(1).max(10).optional(),
        binaryDecision: z.boolean().optional(),
        feedbackText: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input

      // Verify ownership
      const evaluation = await ctx.prisma.evaluation.findUniqueOrThrow({
        where: { id },
        include: {
          assignment: true,
          form: { select: { criteriaJson: true } },
        },
      })

      if (evaluation.assignment.userId !== ctx.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' })
      }

      // Server-side COI check
      const coi = await ctx.prisma.conflictOfInterest.findFirst({
        where: { assignmentId: evaluation.assignmentId, hasConflict: true },
      })
      if (coi) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Cannot submit evaluation — conflict of interest declared',
        })
      }

      // Check voting window via round
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: evaluation.assignment.roundId },
      })
      const now = new Date()

      if (round.status !== 'ROUND_ACTIVE') {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Round is not active',
        })
      }

      // Check for grace period
      const gracePeriod = await ctx.prisma.gracePeriod.findFirst({
        where: {
          roundId: round.id,
          userId: ctx.user.id,
          OR: [
            { projectId: null },
            { projectId: evaluation.assignment.projectId },
          ],
          extendedUntil: { gte: now },
        },
      })

      const effectiveEndDate = gracePeriod?.extendedUntil ?? round.windowCloseAt

      if (round.windowOpenAt && now < round.windowOpenAt) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Voting has not started yet',
        })
      }

      if (effectiveEndDate && now > effectiveEndDate) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Voting window has closed',
        })
      }

      // Load round config for validation
      const config = (round.configJson as Record<string, unknown>) || {}
      const scoringMode = (config.scoringMode as string) || 'criteria'

      // Fix 3: Dynamic feedback validation based on config
      const requireFeedback = config.requireFeedback !== false
      if (requireFeedback) {
        const feedbackMinLength = (config.feedbackMinLength as number) || 10
        if (!data.feedbackText || data.feedbackText.length < feedbackMinLength) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: `Feedback must be at least ${feedbackMinLength} characters`,
          })
        }
      }

      // Fix 4: Normalize binaryDecision and globalScore based on scoringMode
      if (scoringMode !== 'binary') {
        data.binaryDecision = undefined
      }
      if (scoringMode === 'binary') {
        data.globalScore = undefined
      }

      // Fix 5: requireAllCriteriaScored validation
      // Use the form the juror was assigned (evaluation.form), NOT the current active form.
      // If the admin re-saves the form, criterion IDs change — jurors who started before
      // the re-save would have scores keyed to old IDs that don't match the new form.
      if (config.requireAllCriteriaScored && scoringMode === 'criteria') {
        const evalForm = evaluation.form
        if (evalForm?.criteriaJson) {
          const criteria = evalForm.criteriaJson as Array<{ id: string; label?: string; type?: string; required?: boolean }>
          const scorableCriteria = criteria.filter(
            (c) => c.type !== 'section_header' && c.type !== 'text' && c.required !== false
          )
          const scores = data.criterionScoresJson as Record<string, unknown> | undefined
          const missingCriteria = scorableCriteria.filter((c) => {
            if (!scores) return true
            const val = scores[c.id]
            // Boolean criteria store true/false, numeric criteria store numbers
            if (c.type === 'boolean' || c.type === 'advance') return typeof val !== 'boolean'
            return typeof val !== 'number'
          })
          if (missingCriteria.length > 0) {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `Missing scores for criteria: ${missingCriteria.map((c) => c.label || c.id).join(', ')}`,
            })
          }
        }
      }

      // Submit evaluation and mark assignment as completed atomically
      const saveData = {
        criterionScoresJson: data.criterionScoresJson,
        globalScore: data.globalScore ?? null,
        binaryDecision: data.binaryDecision ?? null,
        feedbackText: data.feedbackText ?? null,
      }
      const [updated] = await ctx.prisma.$transaction([
        ctx.prisma.evaluation.update({
          where: { id },
          data: {
            ...saveData,
            status: 'SUBMITTED',
            submittedAt: now,
          },
        }),
        ctx.prisma.assignment.update({
          where: { id: evaluation.assignmentId },
          data: { isCompleted: true },
        }),
      ])

      // Auto-trigger ranking if all assignments complete (fire-and-forget, never awaited)
      triggerAutoRankIfComplete(evaluation.assignment.roundId, ctx.prisma, ctx.user.id).catch((err) => {
        console.error('[Evaluation] triggerAutoRankIfComplete failed:', err)
      })

      // Auto-transition: mark project IN_PROGRESS and check if all evaluations are done
      const projectId = evaluation.assignment.projectId
      const roundIdForTransition = evaluation.assignment.roundId
      await triggerInProgressOnActivity(projectId, roundIdForTransition, ctx.user.id, ctx.prisma)
      await checkEvaluationCompletionAndTransition(projectId, roundIdForTransition, ctx.user.id, ctx.prisma)

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'EVALUATION_SUBMITTED',
        entityType: 'Evaluation',
        entityId: id,
        detailsJson: {
          projectId: evaluation.assignment.projectId,
          roundId: evaluation.assignment.roundId,
          globalScore: data.globalScore,
          binaryDecision: data.binaryDecision,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return updated
    }),

  /**
   * Reset (erase) an evaluation so a juror can start over (admin only)
   * Deletes the evaluation record and resets the assignment's isCompleted flag.
   */
  resetEvaluation: adminProcedure
    .input(
      z.object({
        assignmentId: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const assignment = await ctx.prisma.assignment.findUniqueOrThrow({
        where: { id: input.assignmentId },
        include: {
          evaluation: true,
          user: { select: { id: true, name: true, email: true } },
          project: { select: { id: true, title: true } },
        },
      })

      if (!assignment.evaluation) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'No evaluation found for this assignment',
        })
      }

      // Delete the evaluation and reset assignment completion in a transaction
      await ctx.prisma.$transaction([
        ctx.prisma.evaluation.delete({
          where: { id: assignment.evaluation.id },
        }),
        ctx.prisma.assignment.update({
          where: { id: input.assignmentId },
          data: { isCompleted: false },
        }),
      ])

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'EVALUATION_RESET',
        entityType: 'Evaluation',
        entityId: assignment.evaluation.id,
        detailsJson: {
          assignmentId: input.assignmentId,
          jurorId: assignment.user.id,
          jurorName: assignment.user.name || assignment.user.email,
          projectId: assignment.project.id,
          projectTitle: assignment.project.title,
          previousStatus: assignment.evaluation.status,
          previousGlobalScore: assignment.evaluation.globalScore,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { success: true }
    }),

  /**
   * Get aggregated stats for a project (admin only)
   */
  getProjectStats: adminProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      const evaluations = await ctx.prisma.evaluation.findMany({
        where: {
          status: 'SUBMITTED',
          assignment: { projectId: input.projectId },
        },
      })

      if (evaluations.length === 0) {
        return null
      }

      const globalScores = evaluations
        .map((e) => e.globalScore)
        .filter((s): s is number => s !== null)

      const yesVotes = evaluations.filter(
        (e) => e.binaryDecision === true
      ).length

      return {
        totalEvaluations: evaluations.length,
        averageGlobalScore:
          globalScores.length > 0
            ? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
            : null,
        minScore: globalScores.length > 0 ? Math.min(...globalScores) : null,
        maxScore: globalScores.length > 0 ? Math.max(...globalScores) : null,
        yesVotes,
        noVotes: evaluations.length - yesVotes,
        yesPercentage: (yesVotes / evaluations.length) * 100,
      }
    }),

  /**
   * Get all evaluations for a stage (admin only)
   */
  listByStage: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        status: z.enum(['NOT_STARTED', 'DRAFT', 'SUBMITTED', 'LOCKED']).optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      return ctx.prisma.evaluation.findMany({
        where: {
          assignment: { roundId: input.roundId },
          ...(input.status && { status: input.status }),
        },
        include: {
          assignment: {
            include: {
              user: { select: { id: true, name: true, email: true } },
              project: { select: { id: true, title: true } },
            },
          },
        },
        orderBy: { updatedAt: 'desc' },
      })
    }),

  /**
   * Get my past evaluations (read-only for jury)
   */
  myPastEvaluations: protectedProcedure
    .input(z.object({ roundId: z.string().optional() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.evaluation.findMany({
        where: {
          assignment: {
            userId: ctx.user.id,
            ...(input.roundId && { roundId: input.roundId }),
          },
          status: 'SUBMITTED',
        },
        include: {
          assignment: {
            include: {
              project: { select: { id: true, title: true } },
              round: { select: { id: true, name: true } },
            },
          },
        },
        orderBy: { submittedAt: 'desc' },
      })
    }),

  // =========================================================================
  // Conflict of Interest (COI) Endpoints
  // =========================================================================

  /**
   * Declare a conflict of interest for an assignment
   */
  declareCOI: protectedProcedure
    .input(
      z.object({
        assignmentId: z.string(),
        hasConflict: z.boolean(),
        conflictType: z.string().optional(),
        description: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Look up the assignment to get projectId, roundId, userId
      const assignment = await ctx.prisma.assignment.findUniqueOrThrow({
        where: { id: input.assignmentId },
        include: {
          project: { select: { title: true } },
          round: { select: { id: true, name: true } },
        },
      })

      // Verify ownership
      if (assignment.userId !== ctx.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' })
      }

      // Upsert COI record
      const coi = await ctx.prisma.conflictOfInterest.upsert({
        where: { assignmentId: input.assignmentId },
        create: {
          assignmentId: input.assignmentId,
          userId: ctx.user.id,
          projectId: assignment.projectId,
          hasConflict: input.hasConflict,
          conflictType: input.hasConflict ? input.conflictType : null,
          description: input.hasConflict ? input.description : null,
        },
        update: {
          hasConflict: input.hasConflict,
          conflictType: input.hasConflict ? input.conflictType : null,
          description: input.hasConflict ? input.description : null,
          declaredAt: new Date(),
        },
      })

      // Notify admins if conflict declared
      if (input.hasConflict) {
        await notifyAdmins({
          type: NotificationTypes.JURY_INACTIVE,
          title: 'Conflict of Interest Declared',
          message: `${ctx.user.name || ctx.user.email} declared a conflict of interest (${input.conflictType || 'unspecified'}) for project "${assignment.project.title}" in ${assignment.round.name}.`,
          linkUrl: `/admin/stages/${assignment.roundId}/coi`,
          linkLabel: 'Review COI',
          priority: 'high',
          metadata: {
            assignmentId: input.assignmentId,
            userId: ctx.user.id,
            projectId: assignment.projectId,
            roundId: assignment.roundId,
            conflictType: input.conflictType,
          },
        })
      }

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: input.hasConflict ? 'COI_DECLARED' : 'COI_NO_CONFLICT',
        entityType: 'ConflictOfInterest',
        entityId: coi.id,
        detailsJson: {
          assignmentId: input.assignmentId,
          projectId: assignment.projectId,
          roundId: assignment.roundId,
          hasConflict: input.hasConflict,
          conflictType: input.conflictType,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      // Auto-reassign the project to another eligible juror
      let reassignment: { newJurorId: string; newJurorName: string } | null = null
      if (input.hasConflict) {
        try {
          reassignment = await reassignAfterCOI({
            assignmentId: input.assignmentId,
            auditUserId: ctx.user.id,
            auditIp: ctx.ip,
            auditUserAgent: ctx.userAgent,
          })
        } catch (err) {
          // Don't fail the COI declaration if reassignment fails
          console.error('[COI] Auto-reassignment failed:', err)
        }
      }

      return { ...coi, reassignment }
    }),

  /**
   * Get COI status for an assignment.
   * Caller must own the assignment (or be admin). The COI description can
   * contain confidential personal/financial relationships.
   */
  getCOIStatus: protectedProcedure
    .input(z.object({ assignmentId: z.string() }))
    .query(async ({ ctx, input }) => {
      const assignment = await ctx.prisma.assignment.findUnique({
        where: { id: input.assignmentId },
        select: { userId: true },
      })
      if (!assignment) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' })
      }
      const isAdmin = userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN')
      if (!isAdmin && assignment.userId !== ctx.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' })
      }
      return ctx.prisma.conflictOfInterest.findUnique({
        where: { assignmentId: input.assignmentId },
      })
    }),

  /**
   * List COI declarations for a stage (admin only)
   */
  listCOIByStage: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        hasConflictOnly: z.boolean().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      return ctx.prisma.conflictOfInterest.findMany({
        where: {
          assignment: { roundId: input.roundId },
          ...(input.hasConflictOnly && { hasConflict: true }),
        },
        include: {
          user: { select: { id: true, name: true, email: true } },
          assignment: {
            include: {
              project: { select: { id: true, title: true } },
            },
          },
          reviewedBy: { select: { id: true, name: true, email: true } },
        },
        orderBy: { declaredAt: 'desc' },
      })
    }),

  /**
   * Review a COI declaration (admin only)
   */
  reviewCOI: adminProcedure
    .input(
      z.object({
        id: z.string(),
        reviewAction: z.enum(['cleared', 'reassigned', 'noted']),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const coi = await ctx.prisma.conflictOfInterest.update({
        where: { id: input.id },
        data: {
          reviewedById: ctx.user.id,
          reviewedAt: new Date(),
          reviewAction: input.reviewAction,
        },
      })

      // If admin chose "reassigned", trigger actual reassignment
      let reassignment: { newJurorId: string; newJurorName: string } | null = null
      if (input.reviewAction === 'reassigned') {
        reassignment = await reassignAfterCOI({
          assignmentId: coi.assignmentId,
          auditUserId: ctx.user.id,
          auditIp: ctx.ip,
          auditUserAgent: ctx.userAgent,
        })
      }

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'COI_REVIEWED',
        entityType: 'ConflictOfInterest',
        entityId: input.id,
        detailsJson: {
          reviewAction: input.reviewAction,
          assignmentId: coi.assignmentId,
          userId: coi.userId,
          projectId: coi.projectId,
          reassignedTo: reassignment?.newJurorId,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { ...coi, reassignment }
    }),

  // =========================================================================
  // Reminder Triggers
  // =========================================================================

  /**
   * Manually trigger reminder check for a specific stage (admin only)
   */
  triggerReminders: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const result = await sendManualReminders(input.roundId)

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'REMINDERS_TRIGGERED',
        entityType: 'Stage',
        entityId: input.roundId,
        detailsJson: {
          sent: result.sent,
          errors: result.errors,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return result
    }),

  // =========================================================================
  // AI Evaluation Summary Endpoints
  // =========================================================================

  /**
   * Generate an AI-powered evaluation summary for a project (admin only)
   */
  generateSummary: adminProcedure
    .use(withAIRateLimit)
    .input(
      z.object({
        projectId: z.string(),
        roundId: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      return generateSummary({
        projectId: input.projectId,
        roundId: input.roundId,
        userId: ctx.user.id,
        prisma: ctx.prisma,
      })
    }),

  /**
   * Get an existing evaluation summary for a project (admin only)
   */
  getSummary: adminProcedure
    .input(
      z.object({
        projectId: z.string(),
        roundId: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      return ctx.prisma.evaluationSummary.findUnique({
        where: {
          projectId_roundId: {
            projectId: input.projectId,
            roundId: input.roundId,
          },
        },
      })
    }),

  /**
   * Generate summaries for all projects in a stage with submitted evaluations (admin only)
   */
  generateBulkSummaries: adminProcedure
    .use(withAIRateLimit)
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      // Find all projects with at least 1 submitted evaluation in this stage
      const assignments = await ctx.prisma.assignment.findMany({
        where: {
          roundId: input.roundId,
          evaluation: {
            status: 'SUBMITTED',
          },
        },
        select: { projectId: true },
        distinct: ['projectId'],
      })

      const projectIds = assignments.map((a) => a.projectId)

      let generated = 0
      const errors: Array<{ projectId: string; error: string }> = []

      // Generate summaries sequentially to avoid rate limits
      for (const projectId of projectIds) {
        try {
          await generateSummary({
            projectId,
            roundId: input.roundId,
            userId: ctx.user.id,
            prisma: ctx.prisma,
          })
          generated++
        } catch (error) {
          errors.push({
            projectId,
            error: error instanceof Error ? error.message : 'Unknown error',
          })
        }
      }

      return {
        total: projectIds.length,
        generated,
        errors,
      }
    }),

  // =========================================================================
  // Side-by-Side Comparison (F4)
  // =========================================================================

  /**
   * Get multiple projects with evaluations for side-by-side comparison
   */
  getMultipleForComparison: juryProcedure
    .input(
      z.object({
        projectIds: z.array(z.string()).min(2).max(3),
        roundId: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      // Verify all projects are assigned to current user in this stage
      const assignments = await ctx.prisma.assignment.findMany({
        where: {
          userId: ctx.user.id,
          roundId: input.roundId,
          projectId: { in: input.projectIds },
        },
        include: {
          project: {
            select: {
              id: true,
              title: true,
              teamName: true,
              description: true,
              country: true,
              tags: true,
              files: {
                select: {
                  id: true,
                  fileName: true,
                  fileType: true,
                  size: true,
                },
              },
            },
          },
          evaluation: true,
        },
      })

      if (assignments.length !== input.projectIds.length) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'You are not assigned to all requested projects in this stage',
        })
      }

      // Fetch the active evaluation form for this stage to get criteria labels
      const evaluationForm = await ctx.prisma.evaluationForm.findFirst({
        where: { roundId: input.roundId, isActive: true },
        select: { criteriaJson: true, scalesJson: true },
      })

      return {
        items: assignments.map((a) => ({
          project: a.project,
          evaluation: a.evaluation,
          assignmentId: a.id,
        })),
        criteria: evaluationForm?.criteriaJson as Array<{
          id: string; label: string; description?: string; scale?: string; weight?: number; type?: string
        }> | null,
        scales: evaluationForm?.scalesJson as Record<string, { min: number; max: number }> | null,
      }
    }),

  // =========================================================================
  // Peer Review & Discussion (F13)
  // =========================================================================

  /**
   * Get anonymized peer evaluation summary for a project
   */
  getPeerSummary: juryProcedure
    .input(
      z.object({
        projectId: z.string(),
        roundId: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      // Verify user has submitted their own evaluation first
      const userAssignment = await ctx.prisma.assignment.findFirst({
        where: {
          userId: ctx.user.id,
          projectId: input.projectId,
          roundId: input.roundId,
        },
        include: { evaluation: true },
      })

      if (!userAssignment || userAssignment.evaluation?.status !== 'SUBMITTED') {
        throw new TRPCError({
          code: 'PRECONDITION_FAILED',
          message: 'You must submit your own evaluation before viewing peer summaries',
        })
      }

      // Check stage settings for peer review
      const stage = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
      })

      const settings = (stage.configJson as Record<string, unknown>) || {}
      if (!settings.peerReviewEnabled) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Peer review is not enabled for this stage',
        })
      }

      // Get all submitted evaluations for this project
      const evaluations = await ctx.prisma.evaluation.findMany({
        where: {
          status: 'SUBMITTED',
          assignment: {
            projectId: input.projectId,
            roundId: input.roundId,
          },
        },
        include: {
          assignment: {
            include: {
              user: { select: { id: true, name: true } },
            },
          },
        },
      })

      if (evaluations.length === 0) {
        return { aggregated: null, individualScores: [], totalEvaluations: 0 }
      }

      // Calculate average and stddev per criterion
      const criterionData: Record<string, number[]> = {}
      evaluations.forEach((e) => {
        const scores = e.criterionScoresJson as Record<string, number> | null
        if (scores) {
          Object.entries(scores).forEach(([key, val]) => {
            if (typeof val === 'number') {
              if (!criterionData[key]) criterionData[key] = []
              criterionData[key].push(val)
            }
          })
        }
      })

      const aggregated: Record<string, { average: number; stddev: number; count: number; distribution: Record<number, number> }> = {}
      Object.entries(criterionData).forEach(([key, scores]) => {
        const avg = scores.reduce((a, b) => a + b, 0) / scores.length
        const variance = scores.reduce((sum, s) => sum + Math.pow(s - avg, 2), 0) / scores.length
        const stddev = Math.sqrt(variance)

        const distribution: Record<number, number> = {}
        scores.forEach((s) => {
          const bucket = Math.round(s)
          distribution[bucket] = (distribution[bucket] || 0) + 1
        })

        aggregated[key] = { average: avg, stddev, count: scores.length, distribution }
      })

      // Anonymize individual scores based on round settings
      const anonymizationLevel = (settings.anonymizationLevel as string) || 'fully_anonymous'

      const individualScores = evaluations.map((e) => {
        let jurorLabel: string
        if (anonymizationLevel === 'named') {
          jurorLabel = e.assignment.user.name || 'Juror'
        } else if (anonymizationLevel === 'show_initials') {
          const name = e.assignment.user.name || ''
          jurorLabel = name
            .split(' ')
            .map((n) => n[0])
            .join('')
            .toUpperCase() || 'J'
        } else {
          jurorLabel = `Juror ${evaluations.indexOf(e) + 1}`
        }

        return {
          jurorLabel,
          globalScore: e.globalScore,
          binaryDecision: e.binaryDecision,
          criterionScoresJson: e.criterionScoresJson,
        }
      })

      return {
        aggregated,
        individualScores,
        totalEvaluations: evaluations.length,
      }
    }),

  /**
   * Get or create a discussion for a project evaluation.
   * Caller must have an Assignment for this project+round (or be admin).
   */
  getDiscussion: juryProcedure
    .input(
      z.object({
        projectId: z.string(),
        roundId: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      // Authorization: admins always allowed; jurors must have an Assignment.
      if (!userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN')) {
        const assignment = await ctx.prisma.assignment.findFirst({
          where: {
            userId: ctx.user.id,
            projectId: input.projectId,
            roundId: input.roundId,
          },
          select: { id: true },
        })
        if (!assignment) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You are not assigned to this project',
          })
        }
      }

      // Get or create discussion
      let discussion = await ctx.prisma.evaluationDiscussion.findUnique({
        where: {
          projectId_roundId: {
            projectId: input.projectId,
            roundId: input.roundId,
          },
        },
        include: {
          comments: {
            include: {
              user: { select: { id: true, name: true } },
            },
            orderBy: { createdAt: 'asc' },
          },
        },
      })

      if (!discussion) {
        discussion = await ctx.prisma.evaluationDiscussion.create({
          data: {
            projectId: input.projectId,
            roundId: input.roundId,
          },
          include: {
            comments: {
              include: {
                user: { select: { id: true, name: true } },
              },
              orderBy: { createdAt: 'asc' },
            },
          },
        })
      }

      // Anonymize comments based on round settings
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
      })
      const settings = (round.configJson as Record<string, unknown>) || {}
      const anonymizationLevel = (settings.anonymizationLevel as string) || 'fully_anonymous'

      const anonymizedComments = discussion.comments.map((c: { id: string; userId: string; user: { name: string | null }; content: string; createdAt: Date }, idx: number) => {
        let authorLabel: string
        if (anonymizationLevel === 'named' || c.userId === ctx.user.id) {
          authorLabel = c.user.name || 'Juror'
        } else if (anonymizationLevel === 'show_initials') {
          const name = c.user.name || ''
          authorLabel = name
            .split(' ')
            .map((n: string) => n[0])
            .join('')
            .toUpperCase() || 'J'
        } else {
          authorLabel = `Juror ${idx + 1}`
        }

        return {
          id: c.id,
          authorLabel,
          isOwn: c.userId === ctx.user.id,
          content: c.content,
          createdAt: c.createdAt,
        }
      })

      return {
        id: discussion.id,
        status: discussion.status,
        createdAt: discussion.createdAt,
        closedAt: discussion.closedAt,
        comments: anonymizedComments,
      }
    }),

  /**
   * Add a comment to a project evaluation discussion.
   * Caller must have an Assignment for this project+round (or be admin).
   */
  addComment: juryProcedure
    .input(
      z.object({
        projectId: z.string(),
        roundId: z.string(),
        content: z.string().min(1).max(2000),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Authorization: admins always allowed; jurors must have an Assignment.
      if (!userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN')) {
        const assignment = await ctx.prisma.assignment.findFirst({
          where: {
            userId: ctx.user.id,
            projectId: input.projectId,
            roundId: input.roundId,
          },
          select: { id: true },
        })
        if (!assignment) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You are not assigned to this project',
          })
        }
      }

      // Check max comment length from round settings
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
      })
      const settings = (round.configJson as Record<string, unknown>) || {}
      const maxLength = (settings.maxCommentLength as number) || 2000
      if (input.content.length > maxLength) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: `Comment exceeds maximum length of ${maxLength} characters`,
        })
      }

      // Get or create discussion
      let discussion = await ctx.prisma.evaluationDiscussion.findUnique({
        where: {
          projectId_roundId: {
            projectId: input.projectId,
            roundId: input.roundId,
          },
        },
      })

      if (!discussion) {
        discussion = await ctx.prisma.evaluationDiscussion.create({
          data: {
            projectId: input.projectId,
            roundId: input.roundId,
          },
        })
      }

      if (discussion.status === 'closed') {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'This discussion has been closed',
        })
      }

      const comment = await ctx.prisma.discussionComment.create({
        data: {
          discussionId: discussion.id,
          userId: ctx.user.id,
          content: input.content,
        },
      })

      // Audit log
      try {
        await logAudit({
          prisma: ctx.prisma,
          userId: ctx.user.id,
          action: 'DISCUSSION_COMMENT_ADDED',
          entityType: 'DiscussionComment',
          entityId: comment.id,
          detailsJson: {
            discussionId: discussion.id,
            projectId: input.projectId,
            roundId: input.roundId,
          },
          ipAddress: ctx.ip,
          userAgent: ctx.userAgent,
        })
      } catch (err) {
        console.error('Failed to write audit log for discussion comment creation:', err)
        // Never throw on audit failure
      }

      return comment
    }),

  /**
   * Close a discussion (admin only)
   */
  closeDiscussion: adminProcedure
    .input(z.object({ discussionId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const discussion = await ctx.prisma.evaluationDiscussion.update({
        where: { id: input.discussionId },
        data: {
          status: 'closed',
          closedAt: new Date(),
          closedById: ctx.user.id,
        },
      })

      // Audit log
      try {
        await logAudit({
          prisma: ctx.prisma,
          userId: ctx.user.id,
          action: 'DISCUSSION_CLOSED',
          entityType: 'EvaluationDiscussion',
          entityId: input.discussionId,
          detailsJson: {
            projectId: discussion.projectId,
            roundId: discussion.roundId,
          },
          ipAddress: ctx.ip,
          userAgent: ctx.userAgent,
        })
      } catch (err) {
        console.error('Failed to write audit log for discussion close:', err)
        // Never throw on audit failure
      }

      return discussion
    }),

  // =========================================================================
  // Evaluation Form CRUD (Admin)
  // =========================================================================

  /**
   * Get active evaluation form for a round (admin view with full details)
   */
  getForm: adminProcedure
    .input(z.object({ roundId: z.string(), category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).nullish() }))
    .query(async ({ ctx, input }) => {
      const form = await findActiveForm(ctx.prisma, input.roundId, input.category)

      if (!form) return null

      return {
        id: form.id,
        roundId: form.roundId,
        version: form.version,
        isActive: form.isActive,
        criteriaJson: form.criteriaJson as Array<{
          id: string
          label: string
          description?: string
          weight?: number
          minScore?: number
          maxScore?: number
        }>,
        scalesJson: form.scalesJson as Record<string, unknown> | null,
        createdAt: form.createdAt,
        updatedAt: form.updatedAt,
      }
    }),

  /**
   * Create or update the evaluation form for a round.
   * Deactivates any existing active form and creates a new versioned one.
   */
  upsertForm: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).nullish(),
        criteria: z.array(
          z.object({
            id: z.string(),
            label: z.string().min(1).max(255),
            description: z.string().max(2000).optional(),
            type: z.enum(['numeric', 'text', 'boolean', 'advance', 'section_header']).optional(),
            // Numeric fields
            weight: z.number().min(0).max(100).optional(),
            minScore: z.number().int().min(0).optional(),
            maxScore: z.number().int().min(1).optional(),
            scale: z.number().int().min(1).max(10).optional(),
            required: z.boolean().optional(),
            // Text fields
            maxLength: z.number().int().min(1).max(10000).optional(),
            placeholder: z.string().max(500).optional(),
            // Boolean fields
            trueLabel: z.string().max(100).optional(),
            falseLabel: z.string().max(100).optional(),
            // Conditional visibility
            condition: z.object({
              criterionId: z.string(),
              operator: z.enum(['equals', 'greaterThan', 'lessThan']),
              value: z.union([z.number(), z.string(), z.boolean()]),
            }).optional(),
            // Section grouping
            sectionId: z.string().optional(),
          })
        ).min(1),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { roundId, category, criteria } = input

      // Enforce max one advance criterion per form
      const advanceCount = criteria.filter((c) => c.type === 'advance').length
      if (advanceCount > 1) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Only one advance criterion is allowed per evaluation form',
        })
      }

      // Verify round exists
      await ctx.prisma.round.findUniqueOrThrow({ where: { id: roundId } })

      // Get current max version for this round + category
      const latestForm = await ctx.prisma.evaluationForm.findFirst({
        where: { roundId, category: category ?? null },
        orderBy: { version: 'desc' },
        select: { version: true },
      })
      const nextVersion = (latestForm?.version ?? 0) + 1

      // Build criteriaJson preserving all fields
      const criteriaJson = criteria.map((c) => {
        const type = c.type || 'numeric'
        const base = {
          id: c.id,
          label: c.label,
          description: c.description || '',
          type,
          required: c.required ?? (type !== 'section_header'),
        }

        if (type === 'numeric') {
          const scaleVal = c.scale ?? 10
          return {
            ...base,
            weight: c.weight ?? 1,
            scale: `${c.minScore ?? 1}-${c.maxScore ?? scaleVal}`,
          }
        }
        if (type === 'text') {
          return {
            ...base,
            maxLength: c.maxLength ?? 1000,
            placeholder: c.placeholder || '',
          }
        }
        if (type === 'boolean') {
          return {
            ...base,
            trueLabel: c.trueLabel || 'Yes',
            falseLabel: c.falseLabel || 'No',
          }
        }
        if (type === 'advance') {
          return {
            ...base,
            required: true, // always required, override any input
            trueLabel: c.trueLabel || 'Yes',
            falseLabel: c.falseLabel || 'No',
          }
        }
        // section_header
        return base
      })

      // Auto-generate scalesJson from numeric criteria
      const numericCriteria = criteriaJson.filter((c) => c.type === 'numeric')
      const scaleSet = new Set(numericCriteria.map((c) => (c as { scale: string }).scale))
      const scalesJson: Record<string, { min: number; max: number }> = {}
      for (const scale of scaleSet) {
        const [min, max] = scale.split('-').map(Number)
        scalesJson[scale] = { min, max }
      }

      // Transaction: deactivate old → create new (scoped to category)
      const form = await ctx.prisma.$transaction(async (tx) => {
        await tx.evaluationForm.updateMany({
          where: { roundId, isActive: true, category: category ?? null },
          data: { isActive: false },
        })

        return tx.evaluationForm.create({
          data: {
            roundId,
            version: nextVersion,
            criteriaJson,
            scalesJson,
            isActive: true,
            category: category ?? null,
          },
        })
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPSERT_EVALUATION_FORM',
        entityType: 'EvaluationForm',
        entityId: form.id,
        detailsJson: {
          roundId,
          version: nextVersion,
          criteriaCount: criteria.length,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return form
    }),

  // =========================================================================
  // Phase 4: Stage-scoped evaluation procedures
  // =========================================================================

  /**
   * Start a stage-scoped evaluation (create or return existing draft)
   */
  startStage: protectedProcedure
    .input(
      z.object({
        assignmentId: z.string(),
        roundId: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Verify assignment ownership and roundId match
      const assignment = await ctx.prisma.assignment.findUniqueOrThrow({
        where: { id: input.assignmentId },
      })

      if (assignment.userId !== ctx.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' })
      }

      if (assignment.roundId !== input.roundId) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Assignment does not belong to this stage',
        })
      }

      // Check round window
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
      })

      const now = new Date()
      if (round.status !== 'ROUND_ACTIVE') {
        throw new TRPCError({
          code: 'PRECONDITION_FAILED',
          message: 'Round is not active',
        })
      }

      // Check grace period
      const gracePeriod = await ctx.prisma.gracePeriod.findFirst({
        where: {
          roundId: input.roundId,
          userId: ctx.user.id,
          OR: [
            { projectId: null },
            { projectId: assignment.projectId },
          ],
          extendedUntil: { gte: now },
        },
      })

      const effectiveClose = gracePeriod?.extendedUntil ?? round.windowCloseAt
      if (round.windowOpenAt && now < round.windowOpenAt) {
        throw new TRPCError({
          code: 'PRECONDITION_FAILED',
          message: 'Evaluation window has not opened yet',
        })
      }
      if (effectiveClose && now > effectiveClose) {
        throw new TRPCError({
          code: 'PRECONDITION_FAILED',
          message: 'Evaluation window has closed',
        })
      }

      // Check for existing evaluation
      const existing = await ctx.prisma.evaluation.findUnique({
        where: { assignmentId: input.assignmentId },
      })
      if (existing) return existing

      // Fetch project's competition category for category-aware form lookup
      const project = await ctx.prisma.project.findUniqueOrThrow({
        where: { id: assignment.projectId },
        select: { competitionCategory: true },
      })

      // Get active evaluation form for this stage (category-aware)
      const form = await findActiveForm(ctx.prisma, input.roundId, project.competitionCategory)
      if (!form) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'No active evaluation form for this stage',
        })
      }

      return ctx.prisma.evaluation.create({
        data: {
          assignmentId: input.assignmentId,
          formId: form.id,
          status: 'DRAFT',
        },
      })
    }),

  /**
   * Get the active evaluation form for a stage
   */
  getStageForm: protectedProcedure
    .input(z.object({ roundId: z.string(), category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).nullish() }))
    .query(async ({ ctx, input }) => {
      const form = await findActiveForm(ctx.prisma, input.roundId, input.category)

      if (!form) {
        return null
      }

      return {
        id: form.id,
        criteriaJson: form.criteriaJson as Array<{
          id: string
          label: string
          description?: string
          scale?: string
          weight?: number
          type?: string
          required?: boolean
        }>,
        scalesJson: form.scalesJson as Record<string, { min: number; max: number; labels?: Record<string, string> }> | null,
        version: form.version,
      }
    }),

  /**
   * Check the evaluation window status for a stage
   */
  checkStageWindow: protectedProcedure
    .input(
      z.object({
        roundId: z.string(),
        userId: z.string().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const stage = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: {
          id: true,
          status: true,
          windowOpenAt: true,
          windowCloseAt: true,
        },
      })

      const userId = input.userId ?? ctx.user.id
      const now = new Date()

      // Check for grace period
      const gracePeriod = await ctx.prisma.gracePeriod.findFirst({
        where: {
          roundId: input.roundId,
          userId,
          extendedUntil: { gte: now },
        },
        orderBy: { extendedUntil: 'desc' },
      })

      const effectiveClose = gracePeriod?.extendedUntil ?? stage.windowCloseAt

      const isOpen =
        stage.status === 'ROUND_ACTIVE' &&
        (!stage.windowOpenAt || now >= stage.windowOpenAt) &&
        (!effectiveClose || now <= effectiveClose)

      let reason = ''
      if (!isOpen) {
        if (stage.status !== 'ROUND_ACTIVE') {
          reason = 'Stage is not active'
        } else if (stage.windowOpenAt && now < stage.windowOpenAt) {
          reason = 'Window has not opened yet'
        } else {
          reason = 'Window has closed'
        }
      }

      return {
        isOpen,
        opensAt: stage.windowOpenAt,
        closesAt: stage.windowCloseAt,
        hasGracePeriod: !!gracePeriod,
        graceExpiresAt: gracePeriod?.extendedUntil ?? null,
        reason,
      }
    }),

  /**
   * List evaluations for the current user in a specific stage
   */
  listStageEvaluations: protectedProcedure
    .input(
      z.object({
        roundId: z.string(),
        projectId: z.string().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const where: Record<string, unknown> = {
        assignment: {
          userId: ctx.user.id,
          roundId: input.roundId,
          ...(input.projectId ? { projectId: input.projectId } : {}),
        },
      }

      return ctx.prisma.evaluation.findMany({
        where,
        include: {
          assignment: {
            include: {
              project: { select: { id: true, title: true, teamName: true } },
            },
          },
          form: {
            select: { criteriaJson: true, scalesJson: true },
          },
        },
        orderBy: { updatedAt: 'desc' },
      })
    }),

  /**
   * Get the current juror's progress for a round
   */
  getMyProgress: juryProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const { roundId } = input
      const userId = ctx.user.id

      // Get all assignments for this juror in this round
      const assignments = await ctx.prisma.assignment.findMany({
        where: { roundId, userId },
        include: {
          project: { select: { id: true, title: true } },
          evaluation: {
            include: { form: { select: { criteriaJson: true } } },
          },
        },
      })

      const total = assignments.length
      let completed = 0
      let advanceYes = 0
      let advanceNo = 0

      const submissions: Array<{
        projectId: string
        projectName: string
        submittedAt: Date | null
        advanceDecision: boolean | null
        criterionScores: Array<{ label: string; value: number }>
        numericAverage: number | null
      }> = []

      for (const a of assignments) {
        const ev = a.evaluation
        if (!ev || ev.status !== 'SUBMITTED') continue
        completed++

        const criteria = (ev.form?.criteriaJson ?? []) as Array<{
          id: string; label: string; type?: string; weight?: number
        }>
        const scores = (ev.criterionScoresJson ?? {}) as Record<string, unknown>

        // Find the advance criterion
        const advanceCriterion = criteria.find((c) => c.type === 'advance')
        let advanceDecision: boolean | null = null
        if (advanceCriterion) {
          const val = scores[advanceCriterion.id]
          if (typeof val === 'boolean') {
            advanceDecision = val
            if (val) advanceYes++
            else advanceNo++
          }
        }

        // Collect numeric criterion scores
        const numericScores: Array<{ label: string; value: number }> = []
        for (const c of criteria) {
          if (c.type === 'numeric' || (!c.type && c.weight !== undefined)) {
            const val = scores[c.id]
            if (typeof val === 'number') {
              numericScores.push({ label: c.label, value: val })
            }
          }
        }

        const numericAverage = numericScores.length > 0
          ? Math.round((numericScores.reduce((sum, s) => sum + s.value, 0) / numericScores.length) * 10) / 10
          : null

        submissions.push({
          projectId: a.project.id,
          projectName: a.project.title,
          submittedAt: ev.submittedAt,
          advanceDecision,
          criterionScores: numericScores,
          numericAverage,
        })
      }

      // Sort by most recent first
      submissions.sort((a, b) => {
        if (!a.submittedAt) return 1
        if (!b.submittedAt) return -1
        return b.submittedAt.getTime() - a.submittedAt.getTime()
      })

      return {
        total,
        completed,
        advanceCounts: { yes: advanceYes, no: advanceNo },
        submissions,
      }
    }),

  /**
   * Admin: edit the feedbackText on a submitted evaluation.
   */
  adminEditEvaluation: adminProcedure
    .input(
      z.object({
        evaluationId: z.string(),
        feedbackText: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const evaluation = await ctx.prisma.evaluation.findUnique({
        where: { id: input.evaluationId },
        select: { id: true, feedbackText: true },
      })

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

      const updated = await ctx.prisma.evaluation.update({
        where: { id: input.evaluationId },
        data: { feedbackText: input.feedbackText },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'ADMIN_EDIT_EVALUATION_FEEDBACK',
        entityType: 'Evaluation',
        entityId: input.evaluationId,
        detailsJson: {
          adminUserId: ctx.user.id,
          before: (evaluation.feedbackText ?? '').slice(0, 200),
          after: input.feedbackText.slice(0, 200),
        },
      })

      return updated
    }),

  /**
   * Admin: get all evaluations submitted by a specific juror.
   */
  getJurorEvaluations: adminProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
      const assignments = await ctx.prisma.assignment.findMany({
        where: { userId: input.userId },
        include: {
          project: { select: { id: true, title: true, competitionCategory: true } },
          round: { select: { id: true, name: true, roundType: true, sortOrder: true } },
          evaluation: {
            select: {
              id: true,
              globalScore: true,
              binaryDecision: true,
              feedbackText: true,
              status: true,
              submittedAt: true,
              criterionScoresJson: true,
            },
          },
        },
        orderBy: [
          { round: { sortOrder: 'asc' } },
          { project: { title: 'asc' } },
        ],
      })

      return assignments
        .filter((a) => a.evaluation !== null)
        .map((a) => ({
          assignmentId: a.id,
          roundId: a.roundId,
          roundName: a.round.name,
          roundType: a.round.roundType,
          projectId: a.project.id,
          projectTitle: a.project.title,
          evaluation: a.evaluation!,
        }))
    }),

  // =========================================================================
  // Admin Proxy Evaluation — fill in evaluations on behalf of a juror who
  // could not submit themselves (e.g. access issues). Bypasses voting window
  // and ownership checks. Every proxy submission is audit-logged with both
  // the admin userId and the juror userId whose assignment was completed.
  // =========================================================================

  /**
   * Admin: start (or reuse) a draft evaluation for any juror's assignment.
   */
  adminStart: adminProcedure
    .input(z.object({ assignmentId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const assignment = await ctx.prisma.assignment.findUniqueOrThrow({
        where: { id: input.assignmentId },
        include: { project: { select: { competitionCategory: true } } },
      })

      const form = await findActiveForm(
        ctx.prisma,
        assignment.roundId,
        assignment.project.competitionCategory,
      )
      if (!form) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'No active evaluation form for this stage',
        })
      }

      const existing = await ctx.prisma.evaluation.findUnique({
        where: { assignmentId: input.assignmentId },
      })
      if (existing) return existing

      return ctx.prisma.evaluation.create({
        data: {
          assignmentId: input.assignmentId,
          formId: form.id,
          status: 'DRAFT',
        },
      })
    }),

  /**
   * Admin: autosave a draft evaluation. No ownership check; refuses if the
   * evaluation has already been SUBMITTED or LOCKED.
   */
  adminAutosave: adminProcedure
    .input(
      z.object({
        id: z.string(),
        criterionScoresJson: z.record(z.union([z.number(), z.string(), z.boolean()])).optional(),
        globalScore: z.number().int().min(1).max(10).optional().nullable(),
        binaryDecision: z.boolean().optional().nullable(),
        feedbackText: z.string().optional().nullable(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input
      const evaluation = await ctx.prisma.evaluation.findUniqueOrThrow({
        where: { id },
      })
      if (evaluation.status === 'SUBMITTED' || evaluation.status === 'LOCKED') {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Cannot edit submitted evaluation',
        })
      }
      return ctx.prisma.evaluation.update({
        where: { id },
        data: { ...data, status: 'DRAFT' },
      })
    }),

  /**
   * Admin: submit an evaluation on behalf of a juror. Bypasses round-active
   * and voting-window checks. Still enforces COI (juror-declared conflicts
   * must be resolved via reassignment) and feedback/criterion validation.
   */
  adminSubmitOnBehalf: adminProcedure
    .input(
      z.object({
        id: z.string(),
        criterionScoresJson: z.record(z.union([z.number(), z.string(), z.boolean()])),
        globalScore: z.number().int().min(1).max(10).optional(),
        binaryDecision: z.boolean().optional(),
        feedbackText: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input

      const evaluation = await ctx.prisma.evaluation.findUniqueOrThrow({
        where: { id },
        include: {
          assignment: true,
          form: { select: { criteriaJson: true } },
        },
      })

      // COI still blocks: if the juror declared a conflict, the correct path
      // is to reassign, not to proxy-submit.
      const coi = await ctx.prisma.conflictOfInterest.findFirst({
        where: { assignmentId: evaluation.assignmentId, hasConflict: true },
      })
      if (coi) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Cannot submit — juror declared a conflict of interest. Reassign this project first.',
        })
      }

      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: evaluation.assignment.roundId },
      })

      const config = (round.configJson as Record<string, unknown>) || {}
      const scoringMode = (config.scoringMode as string) || 'criteria'

      const requireFeedback = config.requireFeedback !== false
      if (requireFeedback) {
        const feedbackMinLength = (config.feedbackMinLength as number) || 10
        if (!data.feedbackText || data.feedbackText.length < feedbackMinLength) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: `Feedback must be at least ${feedbackMinLength} characters`,
          })
        }
      }

      if (scoringMode !== 'binary') {
        data.binaryDecision = undefined
      }
      if (scoringMode === 'binary') {
        data.globalScore = undefined
      }

      if (config.requireAllCriteriaScored && scoringMode === 'criteria') {
        const evalForm = evaluation.form
        if (evalForm?.criteriaJson) {
          const criteria = evalForm.criteriaJson as Array<{ id: string; label?: string; type?: string; required?: boolean }>
          const scorableCriteria = criteria.filter(
            (c) => c.type !== 'section_header' && c.type !== 'text' && c.required !== false
          )
          const scores = data.criterionScoresJson as Record<string, unknown> | undefined
          const missingCriteria = scorableCriteria.filter((c) => {
            if (!scores) return true
            const val = scores[c.id]
            if (c.type === 'boolean' || c.type === 'advance') return typeof val !== 'boolean'
            return typeof val !== 'number'
          })
          if (missingCriteria.length > 0) {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `Missing scores for criteria: ${missingCriteria.map((c) => c.label || c.id).join(', ')}`,
            })
          }
        }
      }

      const now = new Date()
      const saveData = {
        criterionScoresJson: data.criterionScoresJson,
        globalScore: data.globalScore ?? null,
        binaryDecision: data.binaryDecision ?? null,
        feedbackText: data.feedbackText ?? null,
      }

      const [updated] = await ctx.prisma.$transaction([
        ctx.prisma.evaluation.update({
          where: { id },
          data: {
            ...saveData,
            status: 'SUBMITTED',
            submittedAt: now,
          },
        }),
        ctx.prisma.assignment.update({
          where: { id: evaluation.assignmentId },
          data: { isCompleted: true },
        }),
      ])

      triggerAutoRankIfComplete(evaluation.assignment.roundId, ctx.prisma, ctx.user.id).catch((err) => {
        console.error('[Evaluation] triggerAutoRankIfComplete failed (admin proxy):', err)
      })

      await triggerInProgressOnActivity(
        evaluation.assignment.projectId,
        evaluation.assignment.roundId,
        ctx.user.id,
        ctx.prisma,
      )
      await checkEvaluationCompletionAndTransition(
        evaluation.assignment.projectId,
        evaluation.assignment.roundId,
        ctx.user.id,
        ctx.prisma,
      )

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'ADMIN_PROXY_EVAL_SUBMITTED',
        entityType: 'Evaluation',
        entityId: id,
        detailsJson: {
          adminUserId: ctx.user.id,
          onBehalfOfUserId: evaluation.assignment.userId,
          assignmentId: evaluation.assignmentId,
          projectId: evaluation.assignment.projectId,
          roundId: evaluation.assignment.roundId,
          globalScore: data.globalScore,
          binaryDecision: data.binaryDecision,
          windowCloseAt: round.windowCloseAt,
          roundStatus: round.status,
          bypassedWindow: round.status !== 'ROUND_ACTIVE' || (round.windowCloseAt ? now > round.windowCloseAt : false),
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return updated
    }),

  /**
   * Admin: list all assignments for a specific juror in a round, including
   * project details and current evaluation status. Drives the admin proxy-
   * evaluation flow (list → pick project → fill in).
   */
  getJurorAssignmentsForRound: adminProcedure
    .input(z.object({ roundId: z.string(), userId: z.string() }))
    .query(async ({ ctx, input }) => {
      const [juror, round, assignments] = await Promise.all([
        ctx.prisma.user.findUnique({
          where: { id: input.userId },
          select: { id: true, name: true, email: true },
        }),
        ctx.prisma.round.findUnique({
          where: { id: input.roundId },
          select: { id: true, name: true, roundType: true, status: true, windowCloseAt: true, competitionId: true },
        }),
        ctx.prisma.assignment.findMany({
          where: {
            roundId: input.roundId,
            userId: input.userId,
          },
          include: {
            project: {
              select: {
                id: true,
                title: true,
                competitionCategory: true,
                teamName: true,
              },
            },
            evaluation: {
              select: {
                id: true,
                status: true,
                globalScore: true,
                binaryDecision: true,
                submittedAt: true,
              },
            },
            conflictOfInterest: {
              select: {
                id: true,
                hasConflict: true,
              },
            },
          },
          orderBy: [{ isCompleted: 'asc' }, { project: { title: 'asc' } }],
        }),
      ])

      if (!juror || !round) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Juror or round not found' })
      }

      return { juror, round, assignments }
    }),
})
