import { z } from 'zod'
import { router, observerProcedure, adminProcedure } from '../trpc'
import { normalizeCountryToCode } from '@/lib/countries'
import { getUserAvatarUrl } from '../utils/avatar-url'
import { getProjectLogoUrl } from '../utils/project-logo-url'
import { aggregateVotes } from '../services/deliberation'
import { validateRoundConfig } from '@/types/competition-configs'
import type { LiveFinalConfig } from '@/types/competition-configs'
import {
  computeBalanceContext,
  computeBalancedProjectScores,
  computePerRoundBalanced,
  type ScorePoint,
  type RoundScopedScorePoint,
} from '../services/juror-balance'
import { generateJurorCalibration } from '../services/ai-juror-calibration'

const editionOrRoundInput = z.object({
  roundId: z.string().optional(),
  programId: z.string().optional(),
}).refine(data => data.roundId || data.programId, {
  message: 'Either roundId or programId is required',
})

function projectWhere(input: { roundId?: string; programId?: string }) {
  if (input.roundId) return { projectRoundStates: { some: { roundId: input.roundId } } }
  return { programId: input.programId! }
}

function assignmentWhere(input: { roundId?: string; programId?: string }) {
  if (input.roundId) return { roundId: input.roundId }
  return { round: { competition: { programId: input.programId! } } }
}

function evalWhere(input: { roundId?: string; programId?: string }, extra: Record<string, unknown> = {}) {
  const base = input.roundId
    ? { assignment: { roundId: input.roundId } }
    : { assignment: { round: { competition: { programId: input.programId! } } } }
  return { ...base, ...extra }
}

/** Extract country from "City, Region, Country" location string */
function extractCountry(location: string | null): string {
  if (!location) return 'Unknown'
  const parts = location.split(',').map(s => s.trim())
  return parts[parts.length - 1] || 'Unknown'
}

export const analyticsRouter = router({
  /**
   * Get score distribution (histogram data)
   */
  getScoreDistribution: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      const evaluations = await ctx.prisma.evaluation.findMany({
        where: evalWhere(input, { status: 'SUBMITTED' }),
        select: {
          criterionScoresJson: true,
        },
      })

      // Extract all scores and calculate distribution
      const allScores: number[] = []
      evaluations.forEach((evaluation) => {
        const scores = evaluation.criterionScoresJson as Record<string, number> | null
        if (scores) {
          Object.values(scores).forEach((score) => {
            if (typeof score === 'number') {
              allScores.push(score)
            }
          })
        }
      })

      // Count scores by bucket (1-10)
      const distribution = Array.from({ length: 10 }, (_, i) => ({
        score: i + 1,
        count: allScores.filter((s) => Math.round(s) === i + 1).length,
      }))

      return {
        distribution,
        totalScores: allScores.length,
        averageScore:
          allScores.length > 0
            ? allScores.reduce((a, b) => a + b, 0) / allScores.length
            : 0,
      }
    }),

  /**
   * Get evaluation completion over time (timeline data)
   */
  getEvaluationTimeline: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      const evaluations = await ctx.prisma.evaluation.findMany({
        where: evalWhere(input, { status: 'SUBMITTED' }),
        select: {
          submittedAt: true,
        },
        orderBy: { submittedAt: 'asc' },
      })

      // Group by date
      const byDate: Record<string, number> = {}
      let cumulative = 0

      evaluations.forEach((evaluation) => {
        if (evaluation.submittedAt) {
          const date = evaluation.submittedAt.toISOString().split('T')[0]
          if (!byDate[date]) {
            byDate[date] = 0
          }
          byDate[date]++
        }
      })

      // Convert to cumulative timeline
      const timeline = Object.entries(byDate)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([date, count]) => {
          cumulative += count
          return {
            date,
            daily: count,
            cumulative,
          }
        })

      return timeline
    }),

  /**
   * Get juror workload distribution
   */
  getJurorWorkload: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      const assignments = await ctx.prisma.assignment.findMany({
        where: assignmentWhere(input),
        include: {
          user: { select: { name: true } },
          project: { select: { id: true, title: true } },
          evaluation: {
            select: { id: true, status: true, globalScore: true },
          },
        },
      })

      // Group by user
      const byUser: Record<
        string,
        { name: string; assigned: number; completed: number; projects: { id: string; title: string; evalStatus: string; score: number | null }[] }
      > = {}

      assignments.forEach((assignment) => {
        const userId = assignment.userId
        if (!byUser[userId]) {
          byUser[userId] = {
            name: assignment.user.name || 'Unknown',
            assigned: 0,
            completed: 0,
            projects: [],
          }
        }
        byUser[userId].assigned++
        const evalStatus = assignment.evaluation?.status
        if (evalStatus === 'SUBMITTED') {
          byUser[userId].completed++
        }
        byUser[userId].projects.push({
          id: assignment.project.id,
          title: assignment.project.title,
          evalStatus: evalStatus === 'SUBMITTED' ? 'REVIEWED' : evalStatus === 'DRAFT' ? 'UNDER_REVIEW' : 'NOT_REVIEWED',
          score: evalStatus === 'SUBMITTED' && assignment.evaluation?.globalScore != null
            ? Number(assignment.evaluation.globalScore)
            : null,
        })
      })

      return Object.entries(byUser)
        .map(([id, data]) => ({
          id,
          ...data,
          completionRate:
            data.assigned > 0
              ? Math.round((data.completed / data.assigned) * 100)
              : 0,
        }))
        .sort((a, b) => b.assigned - a.assigned)
    }),

  /**
   * Get project rankings with raw and balanced (juror-normalized) average scores.
   *
   * `averageScore` is the raw mean of per-evaluation criterion averages.
   * `balancedScore` rescales each juror's contributions via z-score (relative
   * to their own mean + stddev across projects they reviewed in this round),
   * then maps back onto the same 1-10 scale using the overall mean + stddev.
   * A harsh juror's scores are pulled up, a lenient juror's pulled down, so
   * rankings aren't skewed by a single outlier grader.
   */
  getProjectRankings: observerProcedure
    .input(editionOrRoundInput.and(z.object({ limit: z.number().optional() })))
    .query(async ({ ctx, input }) => {
      const [projects, evaluations] = await Promise.all([
        ctx.prisma.project.findMany({
          where: projectWhere(input),
          select: {
            id: true,
            title: true,
            teamName: true,
            status: true,
          },
        }),
        ctx.prisma.evaluation.findMany({
          where: evalWhere(input, { status: 'SUBMITTED' }),
          select: {
            criterionScoresJson: true,
            assignment: { select: { userId: true, projectId: true, roundId: true } },
          },
        }),
      ])

      // Extract a single eval-level score (mean of numeric criterion scores) per evaluation.
      const rawPoints: RoundScopedScorePoint[] = []
      for (const e of evaluations) {
        const scores = e.criterionScoresJson as Record<string, unknown> | null
        if (!scores) continue
        const vals = Object.values(scores).filter((s): s is number => typeof s === 'number')
        if (vals.length === 0) continue
        const rawScore = vals.reduce((a, b) => a + b, 0) / vals.length
        rawPoints.push({
          projectId: e.assignment.projectId,
          userId: e.assignment.userId,
          roundId: e.assignment.roundId,
          rawScore,
        })
      }

      // roundId mode: single-round z-context (existing behavior).
      // programId mode: per-round z-contexts aggregated as the mean of per-round
      // balanced averages — never pool z-contexts across rounds because a juror's
      // grading profile differs by round type.
      const balancedByProject: Map<string, { rawAverage: number | null; balancedAverage: number | null; count: number }> = (() => {
        if (input.roundId) {
          const flat: ScorePoint[] = rawPoints.map(({ projectId, userId, rawScore }) => ({ projectId, userId, rawScore }))
          const ctx = computeBalanceContext(flat)
          return computeBalancedProjectScores(flat, ctx)
        }
        return computePerRoundBalanced(rawPoints)
      })()

      const rankings = projects
        .map((project) => {
          const result = balancedByProject.get(project.id)
          return {
            id: project.id,
            title: project.title,
            teamName: project.teamName,
            status: project.status,
            averageScore: result?.rawAverage ?? null,
            balancedScore: result?.balancedAverage ?? null,
            evaluationCount: result?.count ?? 0,
          }
        })
        .sort((a, b) => {
          const aScore = a.balancedScore ?? a.averageScore
          const bScore = b.balancedScore ?? b.averageScore
          if (aScore !== null && bScore !== null) return bScore - aScore
          if (aScore !== null) return -1
          if (bScore !== null) return 1
          return 0
        })

      return input.limit ? rankings.slice(0, input.limit) : rankings
    }),

  /**
   * Get status breakdown (pie chart data)
   */
  getStatusBreakdown: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      if (input.roundId) {
        // Check if this is an evaluation round — show eval-level status breakdown
        const round = await ctx.prisma.round.findUnique({
          where: { id: input.roundId },
          select: { roundType: true },
        })

        if (round?.roundType === 'EVALUATION') {
          // For evaluation rounds, break down by evaluation status per project
          const projects = await ctx.prisma.projectRoundState.findMany({
            where: { roundId: input.roundId },
            select: {
              projectId: true,
              project: {
                select: {
                  assignments: {
                    where: { roundId: input.roundId },
                    select: {
                      evaluation: { select: { status: true } },
                    },
                  },
                },
              },
            },
          })

          let fullyReviewed = 0
          let partiallyReviewed = 0
          let notReviewed = 0

          for (const p of projects) {
            const assignments = p.project.assignments
            if (assignments.length === 0) {
              notReviewed++
              continue
            }
            const submitted = assignments.filter((a) => a.evaluation?.status === 'SUBMITTED').length
            if (submitted === 0) {
              notReviewed++
            } else if (submitted === assignments.length) {
              fullyReviewed++
            } else {
              partiallyReviewed++
            }
          }

          const result = []
          if (fullyReviewed > 0) result.push({ status: 'FULLY_REVIEWED', count: fullyReviewed })
          if (partiallyReviewed > 0) result.push({ status: 'PARTIALLY_REVIEWED', count: partiallyReviewed })
          if (notReviewed > 0) result.push({ status: 'NOT_REVIEWED', count: notReviewed })
          return result
        }

        // Non-evaluation rounds: use ProjectRoundState
        const states = await ctx.prisma.projectRoundState.groupBy({
          by: ['state'],
          where: { roundId: input.roundId },
          _count: true,
        })
        return states.map((s) => ({
          status: s.state,
          count: s._count,
        }))
      }
      // Edition-level: use global project status
      const projects = await ctx.prisma.project.groupBy({
        by: ['status'],
        where: projectWhere(input),
        _count: true,
      })
      return projects.map((p) => ({
        status: p.status,
        count: p._count,
      }))
    }),

  /**
   * Get overview stats for dashboard
   */
  getOverviewStats: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      const [
        projectCount,
        assignmentCount,
        evaluationCount,
        jurorCount,
        statusCounts,
      ] = await Promise.all([
        ctx.prisma.project.count({ where: projectWhere(input) }),
        ctx.prisma.assignment.count({ where: assignmentWhere(input) }),
        ctx.prisma.evaluation.count({
          where: evalWhere(input, { status: 'SUBMITTED' }),
        }),
        ctx.prisma.assignment.groupBy({
          by: ['userId'],
          where: assignmentWhere(input),
        }),
        ctx.prisma.project.groupBy({
          by: ['status'],
          where: projectWhere(input),
          _count: true,
        }),
      ])

      const completionRate =
        assignmentCount > 0
          ? Math.round((evaluationCount / assignmentCount) * 100)
          : 0

      return {
        projectCount,
        assignmentCount,
        evaluationCount,
        jurorCount: jurorCount.length,
        completionRate,
        statusBreakdown: statusCounts.map((s) => ({
          status: s.status,
          count: s._count,
        })),
      }
    }),

  /**
   * Get criteria-level score distribution
   */
  getCriteriaScores: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      const formWhere = input.roundId
        ? { roundId: input.roundId, isActive: true }
        : { round: { competition: { programId: input.programId! } }, isActive: true }

      const evaluationForms = await ctx.prisma.evaluationForm.findMany({
        where: formWhere,
      })

      if (!evaluationForms.length) {
        return []
      }

      // Build label → Set<id> map so program-level queries match all IDs for the same criterion label
      // Skip boolean and section_header criteria — they don't have numeric scores
      const labelToIds = new Map<string, Set<string>>()
      const labelToFirst = new Map<string, { id: string; label: string }>()
      evaluationForms.forEach((form) => {
        const criteria = form.criteriaJson as Array<{ id: string; label: string; type?: string }> | null
        if (criteria) {
          criteria.forEach((c) => {
            if (c.type === 'boolean' || c.type === 'section_header') return
            if (!labelToIds.has(c.label)) {
              labelToIds.set(c.label, new Set())
              labelToFirst.set(c.label, c)
            }
            labelToIds.get(c.label)!.add(c.id)
          })
        }
      })

      const criteriaLabels = Array.from(labelToFirst.values())
      if (criteriaLabels.length === 0) {
        return []
      }

      // Get all evaluations
      const evaluations = await ctx.prisma.evaluation.findMany({
        where: evalWhere(input, { status: 'SUBMITTED' }),
        select: { criterionScoresJson: true },
      })

      // Calculate average score per criterion, checking ALL IDs that share the same label
      const criteriaScores = criteriaLabels.map((criterion) => {
        const scores: number[] = []
        const ids = labelToIds.get(criterion.label) ?? new Set([criterion.id])

        evaluations.forEach((evaluation) => {
          const criterionScoresJson = evaluation.criterionScoresJson as Record<
            string,
            number
          > | null
          if (criterionScoresJson) {
            for (const cid of ids) {
              if (typeof criterionScoresJson[cid] === 'number') {
                scores.push(criterionScoresJson[cid])
                break // Only count one score per evaluation per criterion
              }
            }
          }
        })

        return {
          id: criterion.id,
          name: criterion.label,
          averageScore:
            scores.length > 0
              ? scores.reduce((a, b) => a + b, 0) / scores.length
              : 0,
          count: scores.length,
        }
      })

      return criteriaScores
    }),

  /**
   * Get geographic distribution of projects by country
   */
  getGeographicDistribution: observerProcedure
    .input(
      z.object({
        programId: z.string(),
        roundId: z.string().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const where = input.roundId
        ? { assignments: { some: { roundId: input.roundId } } }
        : { programId: input.programId }

      const distribution = await ctx.prisma.project.groupBy({
        by: ['country'],
        where: { ...where, country: { not: null } },
        _count: { id: true },
      })

      // Resolve country names to ISO codes (DB may store "France" instead of "FR")
      const codeMap = new Map<string, number>()
      for (const d of distribution) {
        const resolved = normalizeCountryToCode(d.country) ?? d.country ?? 'UNKNOWN'
        codeMap.set(resolved, (codeMap.get(resolved) ?? 0) + d._count.id)
      }

      return Array.from(codeMap.entries()).map(([countryCode, count]) => ({
        countryCode,
        count,
      }))
    }),

  // =========================================================================
  // Advanced Analytics (F10)
  // =========================================================================

  /**
   * Compare metrics across multiple rounds
   */
  getCrossRoundComparison: observerProcedure
    .input(z.object({ roundIds: z.array(z.string()).min(2) }))
    .query(async ({ ctx, input }) => {
      const { roundIds } = input

      // Batch: fetch all rounds, assignments, and evaluations in 3 queries
      const [rounds, assignments, evaluations] = await Promise.all([
        ctx.prisma.round.findMany({
          where: { id: { in: roundIds } },
          select: { id: true, name: true },
        }),
        ctx.prisma.assignment.groupBy({
          by: ['roundId'],
          where: { roundId: { in: roundIds } },
          _count: true,
        }),
        ctx.prisma.evaluation.findMany({
          where: {
            assignment: { roundId: { in: roundIds } },
            status: 'SUBMITTED',
          },
          select: { globalScore: true, assignment: { select: { roundId: true } } },
        }),
      ])

      const roundMap = new Map(rounds.map((r) => [r.id, r.name]))
      const assignmentCountMap = new Map(assignments.map((a) => [a.roundId, a._count]))

      // Group evaluations by round
      const evalsByRound = new Map<string, number[]>()
      const projectsByRound = new Map<string, Set<string>>()
      for (const e of evaluations) {
        const rid = e.assignment.roundId
        if (!evalsByRound.has(rid)) evalsByRound.set(rid, [])
        if (e.globalScore !== null) evalsByRound.get(rid)!.push(e.globalScore)
      }

      // Count distinct projects per round via assignments
      const projectAssignments = await ctx.prisma.assignment.findMany({
        where: { roundId: { in: roundIds } },
        select: { roundId: true, projectId: true },
        distinct: ['roundId', 'projectId'],
      })
      for (const pa of projectAssignments) {
        if (!projectsByRound.has(pa.roundId)) projectsByRound.set(pa.roundId, new Set())
        projectsByRound.get(pa.roundId)!.add(pa.projectId)
      }

      return roundIds.map((roundId) => {
        const globalScores = evalsByRound.get(roundId) ?? []
        const assignmentCount = assignmentCountMap.get(roundId) ?? 0
        const evaluationCount = globalScores.length
        const completionRate = assignmentCount > 0
          ? Math.round((evaluationCount / assignmentCount) * 100)
          : 0
        const averageScore = globalScores.length > 0
          ? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
          : null
        const distribution = Array.from({ length: 10 }, (_, i) => ({
          score: i + 1,
          count: globalScores.filter((s) => Math.round(s) === i + 1).length,
        }))

        return {
          roundId,
          roundName: roundMap.get(roundId) ?? roundId,
          projectCount: projectsByRound.get(roundId)?.size ?? 0,
          evaluationCount,
          completionRate,
          averageScore,
          scoreDistribution: distribution,
        }
      })
    }),

  /**
   * Get juror consistency metrics for a round
   */
  getJurorConsistency: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      const evaluations = await ctx.prisma.evaluation.findMany({
        where: evalWhere(input, { status: 'SUBMITTED' }),
        include: {
          assignment: {
            include: {
              user: { select: { id: true, name: true } },
            },
          },
        },
      })

      // Group scores by juror
      const jurorScores: Record<string, { name: string; scores: number[] }> = {}

      evaluations.forEach((e) => {
        const userId = e.assignment.userId
        if (!jurorScores[userId]) {
          jurorScores[userId] = {
            name: e.assignment.user.name || 'Unknown',
            scores: [],
          }
        }
        if (e.globalScore !== null) {
          jurorScores[userId].scores.push(e.globalScore)
        }
      })

      // Calculate overall average
      const allScores = Object.values(jurorScores).flatMap((j) => j.scores)
      const overallAverage = allScores.length > 0
        ? allScores.reduce((a, b) => a + b, 0) / allScores.length
        : 0

      // Calculate per-juror metrics
      const metrics = Object.entries(jurorScores).map(([userId, data]) => {
        const avg = data.scores.length > 0
          ? data.scores.reduce((a, b) => a + b, 0) / data.scores.length
          : 0
        const variance = data.scores.length > 1
          ? data.scores.reduce((sum, s) => sum + Math.pow(s - avg, 2), 0) / data.scores.length
          : 0
        const stddev = Math.sqrt(variance)
        const deviationFromOverall = Math.abs(avg - overallAverage)

        return {
          userId,
          name: data.name,
          evaluationCount: data.scores.length,
          averageScore: avg,
          stddev,
          deviationFromOverall,
          isOutlier: deviationFromOverall > 2, // Flag if 2+ points from mean
        }
      })

      return {
        overallAverage,
        jurors: metrics.sort((a, b) => b.deviationFromOverall - a.deviationFromOverall),
      }
    }),

  /**
   * Get diversity metrics for projects in a round
   */
  getDiversityMetrics: observerProcedure
    .input(editionOrRoundInput)
    .query(async ({ ctx, input }) => {
      const projects = await ctx.prisma.project.findMany({
        where: projectWhere(input),
        select: {
          country: true,
          competitionCategory: true,
          oceanIssue: true,
          tags: true,
        },
      })

      const total = projects.length
      if (total === 0) {
        return { total: 0, byCountry: [], byCategory: [], byOceanIssue: [], byTag: [] }
      }

      // By country (extract country from "City, Region, Country" format)
      const countryCounts: Record<string, number> = {}
      projects.forEach((p) => {
        const key = extractCountry(p.country)
        countryCounts[key] = (countryCounts[key] || 0) + 1
      })
      const byCountry = Object.entries(countryCounts)
        .map(([country, count]) => ({ country, count, percentage: (count / total) * 100 }))
        .sort((a, b) => b.count - a.count)

      // By competition category
      const categoryCounts: Record<string, number> = {}
      projects.forEach((p) => {
        const key = p.competitionCategory || 'Uncategorized'
        categoryCounts[key] = (categoryCounts[key] || 0) + 1
      })
      const byCategory = Object.entries(categoryCounts)
        .map(([category, count]) => ({ category, count, percentage: (count / total) * 100 }))
        .sort((a, b) => b.count - a.count)

      // By ocean issue
      const issueCounts: Record<string, number> = {}
      projects.forEach((p) => {
        const key = p.oceanIssue || 'Unspecified'
        issueCounts[key] = (issueCounts[key] || 0) + 1
      })
      const byOceanIssue = Object.entries(issueCounts)
        .map(([issue, count]) => ({ issue, count, percentage: (count / total) * 100 }))
        .sort((a, b) => b.count - a.count)

      // By tag
      const tagCounts: Record<string, number> = {}
      projects.forEach((p) => {
        (p.tags || []).forEach((tag) => {
          tagCounts[tag] = (tagCounts[tag] || 0) + 1
        })
      })
      const byTag = Object.entries(tagCounts)
        .map(([tag, count]) => ({ tag, count, percentage: (count / total) * 100 }))
        .sort((a, b) => b.count - a.count)

      return { total, byCountry, byCategory, byOceanIssue, byTag }
    }),

  /**
   * Get dashboard stats (optionally scoped to a round)
   */
  getDashboardStats: observerProcedure
    .input(z.object({ roundId: z.string().optional() }).optional())
    .query(async ({ ctx, input }) => {
      const roundId = input?.roundId

      const projectFilter = roundId
        ? { projectRoundStates: { some: { roundId } } }
        : {}
      const assignmentFilter = roundId ? { roundId } : {}
      const evalFilter = roundId
        ? { assignment: { roundId }, status: 'SUBMITTED' as const }
        : { status: 'SUBMITTED' as const }

      const [
        programCount,
        activeRounds,
        projectCount,
        jurorCount,
        submittedEvaluations,
        totalAssignments,
        evaluationScores,
      ] = await Promise.all([
        ctx.prisma.program.count(),
        ctx.prisma.round.findMany({
          where: { status: 'ROUND_ACTIVE' },
          select: { id: true, name: true },
          take: 5,
        }),
        ctx.prisma.project.count({ where: projectFilter }),
        roundId
          ? ctx.prisma.assignment.findMany({
              where: { roundId },
              select: { userId: true },
              distinct: ['userId'],
            }).then((rows) => rows.length)
          : ctx.prisma.user.count({ where: { roles: { has: 'JURY_MEMBER' }, status: 'ACTIVE' } }),
        ctx.prisma.evaluation.count({ where: evalFilter }),
        ctx.prisma.assignment.count({ where: assignmentFilter }),
        ctx.prisma.evaluation.findMany({
          where: { ...evalFilter, globalScore: { not: null } },
          select: { globalScore: true },
        }),
      ])

      const completionRate = totalAssignments > 0
        ? Math.min(100, Math.round((submittedEvaluations / totalAssignments) * 100))
        : 0

      const scores = evaluationScores.map((e) => e.globalScore!).filter((s) => s != null)
      const scoreDistribution = [
        { label: '9-10', min: 9, max: Infinity },
        { label: '7-8', min: 7, max: 9 },
        { label: '5-6', min: 5, max: 7 },
        { label: '3-4', min: 3, max: 5 },
        { label: '1-2', min: 1, max: 3 },
      ].map((b) => ({
        label: b.label,
        count: scores.filter((s) => s >= b.min && s < b.max).length,
      }))

      return {
        programCount,
        activeRoundCount: activeRounds.length,
        activeRoundName: activeRounds.length === 1 ? activeRounds[0].name : null,
        projectCount,
        jurorCount,
        submittedEvaluations,
        totalAssignments,
        completionRate,
        scoreDistribution,
      }
    }),

  // =========================================================================
  // Stage-Scoped Analytics (Phase 4)
  // =========================================================================

  /**
   * Get score distribution histogram for round evaluations
   */
  getRoundScoreDistribution: observerProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const evaluations = await ctx.prisma.evaluation.findMany({
        where: {
          status: 'SUBMITTED',
          assignment: { roundId: input.roundId },
        },
        select: {
          globalScore: true,
          criterionScoresJson: true,
        },
      })

      // Global score distribution (1-10 buckets)
      const globalScores = evaluations
        .map((e) => e.globalScore)
        .filter((s): s is number => s !== null)

      const globalDistribution = Array.from({ length: 10 }, (_, i) => ({
        score: i + 1,
        count: globalScores.filter((s) => Math.round(s) === i + 1).length,
      }))

      // Per-criterion score distribution
      const criterionScores: Record<string, number[]> = {}
      evaluations.forEach((e) => {
        const scores = e.criterionScoresJson as Record<string, number> | null
        if (scores) {
          Object.entries(scores).forEach(([key, value]) => {
            if (typeof value === 'number') {
              if (!criterionScores[key]) criterionScores[key] = []
              criterionScores[key].push(value)
            }
          })
        }
      })

      const criterionDistributions = Object.entries(criterionScores).map(([criterionId, scores]) => ({
        criterionId,
        average: scores.reduce((a, b) => a + b, 0) / scores.length,
        count: scores.length,
        distribution: Array.from({ length: 10 }, (_, i) => ({
          score: i + 1,
          count: scores.filter((s) => Math.round(s) === i + 1).length,
        })),
      }))

      return {
        globalDistribution,
        totalEvaluations: evaluations.length,
        averageGlobalScore:
          globalScores.length > 0
            ? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
            : 0,
        criterionDistributions,
      }
    }),

  /**
   * Get per-round completion summary for a competition
   * NOTE: This replaces the old pipeline-based getStageCompletionOverview
   */
  getRoundCompletionOverview: observerProcedure
    .input(z.object({ competitionId: z.string() }))
    .query(async ({ ctx, input }) => {
      // Get all rounds in the competition
      const rounds = await ctx.prisma.round.findMany({
        where: { competitionId: input.competitionId },
        orderBy: { sortOrder: 'asc' },
        select: {
          id: true,
          name: true,
          roundType: true,
          status: true,
          sortOrder: true,
          specialAwardId: true,
          specialAward: { select: { name: true } },
        },
      })

      // Batch all queries by roundIds to avoid N+1
      const roundIds = rounds.map((r) => r.id)

      const [
        allProjectRoundStates,
        allAssignmentCounts,
        allCompletedEvals,
        allDistinctJurors,
        allSubmissionFileCounts,
      ] = await Promise.all([
        ctx.prisma.projectRoundState.groupBy({
          by: ['roundId', 'state'],
          where: { roundId: { in: roundIds } },
          _count: true,
        }),
        ctx.prisma.assignment.groupBy({
          by: ['roundId'],
          where: { roundId: { in: roundIds } },
          _count: true,
        }),
        // groupBy on relation field not supported, use raw count per round
        ctx.prisma.$queryRaw<{ roundId: string; count: bigint }[]>`
          SELECT a."roundId", COUNT(e.id)::bigint as count
          FROM "Evaluation" e
          JOIN "Assignment" a ON e."assignmentId" = a.id
          WHERE a."roundId" = ANY(${roundIds}) AND e.status = 'SUBMITTED'
          GROUP BY a."roundId"
        `,
        ctx.prisma.assignment.groupBy({
          by: ['roundId', 'userId'],
          where: { roundId: { in: roundIds } },
        }),
        // Count distinct projects with files per round (for SUBMISSION completion)
        ctx.prisma.$queryRaw<{ roundId: string; teamsWithFiles: bigint; totalFiles: bigint }[]>`
          SELECT "roundId",
            COUNT(DISTINCT "projectId")::bigint as "teamsWithFiles",
            COUNT(id)::bigint as "totalFiles"
          FROM "ProjectFile"
          WHERE "roundId" = ANY(${roundIds})
          GROUP BY "roundId"
        `,
      ])

      // Build lookup maps
      const statesByRound = new Map<string, { state: string; count: number }[]>()
      for (const ps of allProjectRoundStates) {
        const list = statesByRound.get(ps.roundId) || []
        list.push({ state: ps.state, count: ps._count })
        statesByRound.set(ps.roundId, list)
      }

      const assignmentCountByRound = new Map<string, number>()
      for (const ac of allAssignmentCounts) {
        assignmentCountByRound.set(ac.roundId, ac._count)
      }

      const completedEvalsByRound = new Map<string, number>()
      for (const ce of allCompletedEvals) {
        completedEvalsByRound.set(ce.roundId, Number(ce.count))
      }

      const jurorCountByRound = new Map<string, number>()
      for (const j of allDistinctJurors) {
        jurorCountByRound.set(j.roundId, (jurorCountByRound.get(j.roundId) || 0) + 1)
      }

      const submissionFilesByRound = new Map<string, { teamsWithFiles: number; totalFiles: number }>()
      for (const sf of allSubmissionFileCounts) {
        submissionFilesByRound.set(sf.roundId, {
          teamsWithFiles: Number(sf.teamsWithFiles),
          totalFiles: Number(sf.totalFiles),
        })
      }

      const roundOverviews = rounds.map((round) => {
        const stateBreakdown = statesByRound.get(round.id) || []
        const totalProjects = stateBreakdown.reduce((sum, ps) => sum + ps.count, 0)
        const totalAssignments = assignmentCountByRound.get(round.id) || 0
        const completedEvaluations = completedEvalsByRound.get(round.id) || 0
        const submissionFiles = submissionFilesByRound.get(round.id)

        let completionRate: number
        if (round.status === 'ROUND_CLOSED' || round.status === 'ROUND_ARCHIVED') {
          completionRate = 100
        } else if (round.roundType === 'SUBMISSION') {
          // For submission rounds, completion = teams that uploaded at least one file / total projects
          completionRate = totalProjects > 0 && submissionFiles
            ? Math.min(100, Math.round((submissionFiles.teamsWithFiles / totalProjects) * 100))
            : 0
        } else if (totalAssignments > 0) {
          completionRate = Math.min(100, Math.round((completedEvaluations / totalAssignments) * 100))
        } else {
          completionRate = 0
        }

        return {
          roundId: round.id,
          roundName: round.name,
          roundType: round.roundType,
          roundStatus: round.status,
          sortOrder: round.sortOrder,
          specialAwardId: round.specialAwardId,
          specialAwardName: round.specialAward?.name ?? null,
          totalProjects,
          stateBreakdown,
          totalAssignments,
          completedEvaluations,
          pendingEvaluations: totalAssignments - completedEvaluations,
          completionRate,
          jurorCount: jurorCountByRound.get(round.id) || 0,
        }
      })

      return {
        competitionId: input.competitionId,
        rounds: roundOverviews,
        summary: {
          totalRounds: rounds.length,
          totalProjects: roundOverviews.reduce((sum, s) => sum + s.totalProjects, 0),
          totalAssignments: roundOverviews.reduce((sum, s) => sum + s.totalAssignments, 0),
          totalCompleted: roundOverviews.reduce((sum, s) => sum + s.completedEvaluations, 0),
        },
      }
    }),

  // =========================================================================
  // Award Analytics (Phase 5)
  // =========================================================================

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

  // NOTE: getAwardVoteDistribution procedure removed - depends on deleted Stage/Track/SpecialAward/AwardVote models
  // Will need to be reimplemented with new Competition/Round/Award architecture

  /**
   * Get all projects with pagination, filtering, and search (for observer dashboard)
   */
  getAllProjects: observerProcedure
    .input(
      z.object({
        roundId: z.string().optional(),
        search: z.string().optional(),
        status: z.string().optional(),
        sortBy: z.enum(['title', 'score', 'evaluations', 'status']).default('status'),
        sortDir: z.enum(['asc', 'desc']).default('asc'),
        page: z.number().min(1).default(1),
        perPage: z.number().min(1).max(100).default(20),
        exportAll: z.boolean().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const effectivePerPage = input.exportAll ? 10000 : input.perPage
      const where: Record<string, unknown> = {}

      if (input.roundId) {
        where.projectRoundStates = { some: { roundId: input.roundId } }
      }

      const ROUND_STATE_STATUSES = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN']
      if (input.status && !ROUND_STATE_STATUSES.includes(input.status)) {
        where.status = input.status
      }

      if (input.search) {
        where.OR = [
          { title: { contains: input.search, mode: 'insensitive' } },
          { teamName: { contains: input.search, mode: 'insensitive' } },
          { country: { contains: input.search, mode: 'insensitive' } },
          { institution: { contains: input.search, mode: 'insensitive' } },
          { geographicZone: { contains: input.search, mode: 'insensitive' } },
          { teamMembers: { some: { user: { name: { contains: input.search, mode: 'insensitive' } } } } },
          { teamMembers: { some: { user: { email: { contains: input.search, mode: 'insensitive' } } } } },
        ]
      }

      // Prisma-level sort for title; score/evaluations sorted post-query
      const prismaOrderBy = input.sortBy === 'title'
        ? { title: input.sortDir as 'asc' | 'desc' }
        : { title: 'asc' as const }

      const [projects, total] = await Promise.all([
        ctx.prisma.project.findMany({
          where,
          select: {
            id: true,
            title: true,
            teamName: true,
            status: true,
            country: true,
            logoKey: true,
            logoProvider: true,
            assignments: {
              where: input.roundId ? { roundId: input.roundId } : undefined,
              select: {
                roundId: true,
                round: { select: { id: true, name: true, sortOrder: true } },
                evaluation: {
                  select: { globalScore: true, status: true },
                },
              },
            },
            projectRoundStates: {
              select: {
                roundId: true,
                state: true,
                round: { select: { id: true, name: true, sortOrder: true } },
              },
              orderBy: { round: { sortOrder: 'desc' } },
              take: 1,
            },
          },
          orderBy: prismaOrderBy,
          // When sorting by computed fields or filtering by round-state status, fetch all then slice in JS
          ...(input.sortBy === 'title' && !ROUND_STATE_STATUSES.includes(input.status ?? '') && !input.exportAll
            ? { skip: (input.page - 1) * effectivePerPage, take: effectivePerPage }
            : {}),
        }),
        ctx.prisma.project.count({ where }),
      ])

      const mapped = await Promise.all(projects.map(async (p) => {
        const submitted = p.assignments
          .map((a) => a.evaluation)
          .filter((e) => e?.status === 'SUBMITTED')
        const drafts = p.assignments
          .map((a) => a.evaluation)
          .filter((e) => e?.status === 'DRAFT')
        const scores = submitted
          .map((e) => e?.globalScore)
          .filter((s): s is number => s !== null)
        const averageScore =
          scores.length > 0
            ? scores.reduce((a, b) => a + b, 0) / scores.length
            : null

        // Show the furthest round the project reached (from projectRoundStates, ordered by sortOrder desc)
        const furthestRoundState = p.projectRoundStates[0]
        // Fallback to assignment round if no round states
        const roundAssignment = input.roundId
          ? p.assignments.find((a) => a.roundId === input.roundId)
          : p.assignments[0]

        // Derive observer-friendly status from latest round state
        const observerStatus = furthestRoundState?.state ?? 'PENDING'

        const logoUrl = await getProjectLogoUrl(p.logoKey, p.logoProvider)

        return {
          id: p.id,
          title: p.title,
          teamName: p.teamName,
          status: p.status,
          observerStatus,
          country: p.country,
          logoUrl,
          roundId: furthestRoundState?.round?.id ?? roundAssignment?.round?.id ?? '',
          roundName: furthestRoundState?.round?.name ?? roundAssignment?.round?.name ?? '',
          averageScore,
          evaluationCount: submitted.length,
        }
      }))

      // Filter by round-state status in JS
      const observerStatusFilter = input.status && ROUND_STATE_STATUSES.includes(input.status)
        ? input.status
        : null
      const filtered = observerStatusFilter
        ? mapped.filter((p) => p.observerStatus === observerStatusFilter)
        : mapped
      const filteredTotal = observerStatusFilter ? filtered.length : total

      // Sort by computed fields (score, evaluations, status) in JS
      const STATUS_ORDER: Record<string, number> = {
        IN_PROGRESS: 0, PENDING: 1, COMPLETED: 2, PASSED: 3, REJECTED: 4, WITHDRAWN: 5,
      }
      let sorted = filtered
      if (input.sortBy === 'status') {
        sorted = filtered.sort((a, b) => {
          const oa = STATUS_ORDER[a.observerStatus] ?? 3
          const ob = STATUS_ORDER[b.observerStatus] ?? 3
          const cmp = oa - ob
          if (cmp !== 0) return input.sortDir === 'asc' ? cmp : -cmp
          return a.title.localeCompare(b.title)
        })
      } else if (input.sortBy === 'score') {
        sorted = filtered.sort((a, b) => {
          const sa = a.averageScore ?? -1
          const sb = b.averageScore ?? -1
          return input.sortDir === 'asc' ? sa - sb : sb - sa
        })
      } else if (input.sortBy === 'evaluations') {
        sorted = filtered.sort((a, b) =>
          input.sortDir === 'asc'
            ? a.evaluationCount - b.evaluationCount
            : b.evaluationCount - a.evaluationCount
        )
      }

      // Paginate in JS for computed-field sorts or observer status filter
      const needsJsPagination = (input.sortBy !== 'title') || observerStatusFilter
      const paginated = needsJsPagination
        ? sorted.slice((input.page - 1) * effectivePerPage, input.page * effectivePerPage)
        : sorted

      return {
        projects: paginated,
        total: filteredTotal,
        page: input.page,
        perPage: effectivePerPage,
        totalPages: Math.ceil(filteredTotal / effectivePerPage),
      }
    }),

  /**
   * Get round-type-aware stats for a specific round.
   * Returns different metrics depending on the round type.
   */
  getRoundTypeStats: observerProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { id: true, roundType: true, competitionId: true },
      })

      const roundType = round.roundType

      switch (roundType) {
        case 'INTAKE': {
          const [total, byState, byCategory, countryData] = await Promise.all([
            ctx.prisma.projectRoundState.count({ where: { roundId: input.roundId } }),
            ctx.prisma.projectRoundState.groupBy({
              by: ['state'],
              where: { roundId: input.roundId },
              _count: true,
            }),
            ctx.prisma.project.groupBy({
              by: ['competitionCategory'],
              where: { projectRoundStates: { some: { roundId: input.roundId } } },
              _count: true,
            }),
            ctx.prisma.project.findMany({
              where: { projectRoundStates: { some: { roundId: input.roundId } } },
              select: { country: true },
            }),
          ])
          const countries = new Set(countryData.map((p) => extractCountry(p.country)).filter((c) => c !== 'Unknown'))
          const startupCount = byCategory.find((c) => c.competitionCategory === 'STARTUP')?._count ?? 0
          const conceptCount = byCategory.find((c) => c.competitionCategory === 'BUSINESS_CONCEPT')?._count ?? 0
          return {
            roundType,
            stats: {
              totalProjects: total,
              startupCount,
              conceptCount,
              countryCount: countries.size,
              byState: byState.map((s) => ({ state: s.state, count: s._count })),
              byCategory: byCategory.map((c) => ({
                category: c.competitionCategory ?? 'Uncategorized',
                count: c._count,
              })),
            },
          }
        }

        case 'FILTERING': {
          const [total, byOutcome] = await Promise.all([
            ctx.prisma.filteringResult.count({ where: { roundId: input.roundId } }),
            ctx.prisma.filteringResult.groupBy({
              by: ['outcome'],
              where: { roundId: input.roundId },
              _count: true,
            }),
          ])
          const passed = byOutcome.find((o) => o.outcome === 'PASSED')?._count ?? 0
          return {
            roundType,
            stats: {
              totalScreened: total,
              passed,
              filteredOut: byOutcome.find((o) => o.outcome === 'FILTERED_OUT')?._count ?? 0,
              flagged: byOutcome.find((o) => o.outcome === 'FLAGGED')?._count ?? 0,
              passRate: total > 0 ? Math.round((passed / total) * 100) : 0,
            },
          }
        }

        case 'EVALUATION': {
          const [assignmentCount, submittedCount, jurorCount] = await Promise.all([
            ctx.prisma.assignment.count({ where: { roundId: input.roundId } }),
            ctx.prisma.evaluation.count({
              where: { assignment: { roundId: input.roundId }, status: 'SUBMITTED' },
            }),
            ctx.prisma.assignment.findMany({
              where: { roundId: input.roundId },
              select: { userId: true },
              distinct: ['userId'],
            }).then((rows) => rows.length),
          ])
          return {
            roundType,
            stats: {
              totalAssignments: assignmentCount,
              completedEvaluations: submittedCount,
              completionRate: assignmentCount > 0 ? Math.round((submittedCount / assignmentCount) * 100) : 0,
              activeJurors: jurorCount,
            },
          }
        }

        case 'SUBMISSION': {
          const [fileCount, teamsWithFiles] = await Promise.all([
            ctx.prisma.projectFile.count({ where: { roundId: input.roundId } }),
            ctx.prisma.projectFile.findMany({
              where: { roundId: input.roundId },
              select: { projectId: true },
              distinct: ['projectId'],
            }).then((rows) => rows.length),
          ])
          return {
            roundType,
            stats: {
              totalFiles: fileCount,
              teamsSubmitted: teamsWithFiles,
            },
          }
        }

        case 'MENTORING': {
          const [assignmentCount, messageCount] = await Promise.all([
            ctx.prisma.mentorAssignment.count({
              where: { project: { projectRoundStates: { some: { roundId: input.roundId } } } },
            }),
            ctx.prisma.mentorMessage.count({
              where: { project: { projectRoundStates: { some: { roundId: input.roundId } } } },
            }),
          ])
          return {
            roundType,
            stats: {
              mentorAssignments: assignmentCount,
              totalMessages: messageCount,
            },
          }
        }

        case 'LIVE_FINAL': {
          const session = await ctx.prisma.liveVotingSession.findUnique({
            where: { roundId: input.roundId },
            select: { id: true, status: true, _count: { select: { votes: true } } },
          })
          return {
            roundType,
            stats: {
              sessionStatus: session?.status ?? 'NOT_STARTED',
              voteCount: session?._count.votes ?? 0,
            },
          }
        }

        case 'DELIBERATION': {
          const [sessions, votes, locks] = await Promise.all([
            ctx.prisma.deliberationSession.count({ where: { roundId: input.roundId } }),
            ctx.prisma.deliberationVote.count({
              where: { session: { roundId: input.roundId } },
            }),
            ctx.prisma.resultLock.count({ where: { roundId: input.roundId } }),
          ])
          return {
            roundType,
            stats: {
              totalSessions: sessions,
              totalVotes: votes,
              resultsLocked: locks,
            },
          }
        }

        default:
          return { roundType, stats: {} }
      }
    }),

  /**
   * Observer-accessible project detail: project info + assignments with evaluations + competition rounds + files.
   * Read-only combined endpoint to avoid multiple round-trips.
   */
  getProjectDetail: observerProcedure
    .input(z.object({ id: z.string(), roundId: z.string().optional() }))
    .query(async ({ ctx, input }) => {
      const [projectRaw, projectTags, assignments, submittedEvaluations] = await Promise.all([
        ctx.prisma.project.findUniqueOrThrow({
          where: { id: input.id },
          include: {
            files: {
              select: {
                id: true, fileName: true, fileType: true, mimeType: true, size: true,
                bucket: true, objectKey: true, pageCount: true, textPreview: true,
                detectedLang: true, langConfidence: true, analyzedAt: true,
                roundId: true,
                requirementId: true,
                requirement: { select: { id: true, name: true, description: true, isRequired: true } },
              },
              orderBy: [{ createdAt: 'asc' }],
            },
            teamMembers: {
              include: {
                user: {
                  select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true },
                },
              },
              orderBy: { joinedAt: 'asc' },
            },
          },
        }),
        ctx.prisma.projectTag.findMany({
          where: { projectId: input.id },
          include: { tag: { select: { id: true, name: true, category: true, color: true } } },
          orderBy: { confidence: 'desc' },
        }).catch(() => [] as { id: string; projectId: string; tagId: string; confidence: number; tag: { id: string; name: string; category: string | null; color: string | null } }[]),
        ctx.prisma.assignment.findMany({
          where: { projectId: input.id },
          include: {
            user: { select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true } },
            round: { select: { id: true, name: true } },
            evaluation: {
              select: {
                id: true, status: true, submittedAt: true, globalScore: true,
                binaryDecision: true, criterionScoresJson: true, feedbackText: true,
              },
            },
          },
          orderBy: { createdAt: 'desc' },
        }),
        ctx.prisma.evaluation.findMany({
          where: {
            status: 'SUBMITTED',
            assignment: {
              projectId: input.id,
              ...(input.roundId ? { roundId: input.roundId } : {}),
            },
          },
        }),
      ])

      // Compute evaluation stats
      let stats = null
      if (submittedEvaluations.length > 0) {
        const globalScores = submittedEvaluations
          .map((e) => e.globalScore)
          .filter((s): s is number => s !== null)

        // Count recommendations: first check binaryDecision, then fall back to
        // boolean criteria in criterionScoresJson (when scoringMode isn't 'binary')
        const yesVotes = submittedEvaluations.filter((e) => {
          if (e.binaryDecision != null) return e.binaryDecision === true
          // Fall back: check if any boolean criterion is true
          const scores = e.criterionScoresJson as Record<string, unknown> | null
          if (!scores) return false
          const boolValues = Object.values(scores).filter((v) => typeof v === 'boolean')
          return boolValues.length > 0 && boolValues.every((v) => v === true)
        }).length

        // Check if recommendation data exists at all
        const hasRecommendationData = submittedEvaluations.some((e) => {
          if (e.binaryDecision != null) return true
          const scores = e.criterionScoresJson as Record<string, unknown> | null
          if (!scores) return false
          return Object.values(scores).some((v) => typeof v === 'boolean')
        })

        stats = {
          totalEvaluations: submittedEvaluations.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: submittedEvaluations.length - yesVotes,
          yesPercentage: hasRecommendationData
            ? (yesVotes / submittedEvaluations.length) * 100
            : null,
        }
      }

      // Get competition rounds for file grouping
      let competitionRounds: { id: string; name: string; roundType: string }[] = []
      const competition = await ctx.prisma.competition.findFirst({
        where: { programId: projectRaw.programId },
        include: { rounds: { select: { id: true, name: true, roundType: true }, orderBy: { sortOrder: 'asc' } } },
      })
      if (competition) {
        competitionRounds = competition.rounds
      }

      // Get project round states for round history
      const projectRoundStates = await ctx.prisma.projectRoundState.findMany({
        where: { projectId: input.id },
        select: { roundId: true, state: true, enteredAt: true, exitedAt: true },
      })

      // Get filtering result (AI screening) for rejected projects
      const filteringResult = projectRaw.status === 'REJECTED'
        ? await ctx.prisma.filteringResult.findFirst({
            where: { projectId: input.id },
            select: {
              outcome: true,
              finalOutcome: true,
              aiScreeningJson: true,
              overrideReason: true,
              round: { select: { id: true, name: true } },
            },
            orderBy: { createdAt: 'desc' },
          })
        : null

      // Get file requirements for all rounds
      let allRequirements: { id: string; roundId: string; name: string; description: string | null; isRequired: boolean; acceptedMimeTypes: string[]; maxSizeMB: number | null }[] = []
      if (competitionRounds.length > 0) {
        allRequirements = await ctx.prisma.fileRequirement.findMany({
          where: { roundId: { in: competitionRounds.map((r) => r.id) } },
          select: { id: true, roundId: true, name: true, description: true, isRequired: true, acceptedMimeTypes: true, maxSizeMB: true },
          orderBy: { sortOrder: 'asc' },
        })
      }

      // Attach avatar URLs
      const [teamMembersWithAvatars, assignmentsWithAvatars] = await Promise.all([
        Promise.all(
          projectRaw.teamMembers.map(async (member) => ({
            ...member,
            user: {
              ...member.user,
              avatarUrl: await getUserAvatarUrl(member.user.profileImageKey, member.user.profileImageProvider),
            },
          }))
        ),
        Promise.all(
          assignments.map(async (a) => ({
            ...a,
            user: {
              ...a.user,
              avatarUrl: await getUserAvatarUrl(a.user.profileImageKey, a.user.profileImageProvider),
            },
          }))
        ),
      ])

      return {
        project: {
          ...projectRaw,
          files: projectRaw.files,
          projectTags,
          teamMembers: teamMembersWithAvatars,
        },
        assignments: assignmentsWithAvatars,
        stats,
        competitionRounds,
        projectRoundStates,
        allRequirements,
        filteringResult,
      }
    }),

  /**
   * Activity feed — recent audit log entries for observer dashboard
   */
  getActivityFeed: observerProcedure
    .input(z.object({
      limit: z.number().min(1).max(50).default(10),
      roundId: z.string().optional(),
    }).optional())
    .query(async ({ ctx, input }) => {
      const limit = input?.limit ?? 10
      const roundId = input?.roundId

      // --- DecisionAuditLog entries (dot-notation events) ---
      const dalWhere: Record<string, unknown> = {}
      if (roundId) {
        dalWhere.OR = [
          { entityType: 'Round', entityId: roundId },
          { detailsJson: { path: ['roundId'], equals: roundId } },
        ]
      }
      const dalEntries = await ctx.prisma.decisionAuditLog.findMany({
        where: dalWhere,
        orderBy: { createdAt: 'desc' },
        take: limit * 2,
        select: {
          id: true,
          eventType: true,
          entityType: true,
          entityId: true,
          actorId: true,
          detailsJson: true,
          createdAt: true,
        },
      })

      // --- AuditLog entries (SCREAMING_SNAKE_CASE actions) ---
      const alActions = [
        'EVALUATION_SUBMITTED', 'EVALUATION_SAVE_DRAFT',
        'PROJECT_CREATE', 'PROJECT_UPDATE',
        'FILE_VIEWED', 'FILE_OPENED', 'FILE_DOWNLOADED',
      ]
      const alWhere: Record<string, unknown> = { action: { in: alActions } }
      if (roundId) {
        alWhere.detailsJson = { path: ['roundId'], equals: roundId }
      }
      const alEntries = await ctx.prisma.auditLog.findMany({
        where: alWhere,
        orderBy: { timestamp: 'desc' },
        take: limit * 2,
        select: {
          id: true,
          action: true,
          entityType: true,
          entityId: true,
          userId: true,
          detailsJson: true,
          timestamp: true,
        },
      })

      // Batch-fetch actor names
      const allActorIds = [
        ...new Set([
          ...dalEntries.map((e) => e.actorId),
          ...alEntries.map((e) => e.userId),
        ].filter(Boolean)),
      ] as string[]
      const actors = allActorIds.length > 0
        ? await ctx.prisma.user.findMany({
            where: { id: { in: allActorIds } },
            select: { id: true, name: true },
          })
        : []
      const actorMap = new Map(actors.map((a) => [a.id, a.name]))

      type FeedItem = {
        id: string
        description: string
        category: 'round' | 'evaluation' | 'project' | 'file' | 'deliberation' | 'system'
        createdAt: Date
      }

      // Humanize DecisionAuditLog entries
      const dalItems: FeedItem[] = dalEntries
        .filter((e) => !e.eventType.includes('transitioned') && !e.eventType.includes('cursor_updated'))
        .map((entry) => {
          const actor = entry.actorId ? actorMap.get(entry.actorId) ?? 'System' : 'System'
          const details = (entry.detailsJson ?? {}) as Record<string, unknown>
          const roundName = (details.roundName ?? '') as string
          const projectTitle = (details.projectTitle ?? details.projectName ?? '') as string

          let description: string
          let category: FeedItem['category'] = 'system'

          switch (entry.eventType) {
            case 'round.activated':
            case 'round.reopened':
              description = roundName ? `${roundName} was opened` : 'A round was opened'
              category = 'round'
              break
            case 'round.closed':
              description = roundName ? `${roundName} was closed` : 'A round was closed'
              category = 'round'
              break
            case 'round.archived':
              description = roundName ? `${roundName} was archived` : 'A round was archived'
              category = 'round'
              break
            case 'round.finalized':
              description = roundName ? `Results finalized for ${roundName}` : 'Round results finalized'
              category = 'round'
              break
            case 'results.locked':
              description = roundName ? `Results locked for ${roundName}` : 'Results were locked'
              category = 'round'
              break
            case 'results.unlocked':
              description = roundName ? `Results unlocked for ${roundName}` : 'Results were unlocked'
              category = 'round'
              break
            case 'override.applied':
              description = projectTitle
                ? `${actor} overrode decision for ${projectTitle}`
                : `${actor} applied a decision override`
              category = 'project'
              break
            case 'finalization.project_outcome':
              description = projectTitle
                ? `${projectTitle} outcome: ${(details.outcome as string) ?? 'determined'}`
                : 'Project outcome determined'
              category = 'project'
              break
            case 'deliberation.created':
              description = 'Deliberation session created'
              category = 'deliberation'
              break
            case 'deliberation.finalized':
              description = 'Deliberation session finalized'
              category = 'deliberation'
              break
            case 'deliberation.admin_override':
              description = `${actor} applied deliberation override`
              category = 'deliberation'
              break
            case 'live.session_started':
              description = 'Live voting session started'
              category = 'round'
              break
            case 'submission_window.opened':
              description = 'Submission window opened'
              category = 'round'
              break
            case 'submission_window.closed':
              description = 'Submission window closed'
              category = 'round'
              break
            case 'mentor_workspace.activated':
              description = projectTitle
                ? `Mentoring workspace activated for ${projectTitle}`
                : 'Mentoring workspace activated'
              category = 'project'
              break
            default:
              description = `${actor}: ${entry.eventType.replace(/[_.]/g, ' ')}`
              break
          }

          return { id: entry.id, description, category, createdAt: entry.createdAt }
        })

      // Humanize AuditLog entries
      const alItems: FeedItem[] = alEntries.map((entry) => {
        const actor = entry.userId ? actorMap.get(entry.userId) ?? 'Someone' : 'System'
        const details = (entry.detailsJson ?? {}) as Record<string, unknown>
        const projectTitle = (details.projectTitle ?? details.entityLabel ?? '') as string

        let description: string
        let category: FeedItem['category'] = 'system'

        switch (entry.action) {
          case 'EVALUATION_SUBMITTED':
            description = projectTitle
              ? `${actor} submitted evaluation for ${projectTitle}`
              : `${actor} submitted an evaluation`
            category = 'evaluation'
            break
          case 'EVALUATION_SAVE_DRAFT':
            description = projectTitle
              ? `${actor} saved draft evaluation for ${projectTitle}`
              : `${actor} saved a draft evaluation`
            category = 'evaluation'
            break
          case 'PROJECT_CREATE':
            description = projectTitle
              ? `New project submitted: ${projectTitle}`
              : 'New project submitted'
            category = 'project'
            break
          case 'PROJECT_UPDATE':
            description = projectTitle
              ? `${projectTitle} was updated`
              : 'A project was updated'
            category = 'project'
            break
          case 'FILE_VIEWED':
          case 'FILE_OPENED':
            description = `${actor} viewed a document${projectTitle ? ` for ${projectTitle}` : ''}`
            category = 'file'
            break
          case 'FILE_DOWNLOADED':
            description = `${actor} downloaded a document${projectTitle ? ` for ${projectTitle}` : ''}`
            category = 'file'
            break
          default:
            description = `${actor}: ${entry.action.replace(/_/g, ' ').toLowerCase()}`
            break
        }

        return {
          id: `al_${entry.id}`,
          description,
          category,
          createdAt: entry.timestamp,
        }
      })

      // Merge and sort by date, take limit
      const merged = [...dalItems, ...alItems]
        .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
        .slice(0, limit)

      return merged
    }),

  // =========================================================================
  // Round-Type-Specific Observer Reports
  // =========================================================================

  /**
   * Get filtering result stats for a round (observer proxy of filtering.getResultStats)
   */
  getFilteringResultStats: observerProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const [passed, filteredOut, flagged, overridden] = await Promise.all([
        ctx.prisma.filteringResult.count({
          where: {
            roundId: input.roundId,
            OR: [
              { finalOutcome: 'PASSED' },
              { finalOutcome: null, outcome: 'PASSED' },
            ],
          },
        }),
        ctx.prisma.filteringResult.count({
          where: {
            roundId: input.roundId,
            OR: [
              { finalOutcome: 'FILTERED_OUT' },
              { finalOutcome: null, outcome: 'FILTERED_OUT' },
            ],
          },
        }),
        ctx.prisma.filteringResult.count({
          where: {
            roundId: input.roundId,
            OR: [
              { finalOutcome: 'FLAGGED' },
              { finalOutcome: null, outcome: 'FLAGGED' },
            ],
          },
        }),
        ctx.prisma.filteringResult.count({
          where: { roundId: input.roundId, overriddenBy: { not: null } },
        }),
      ])

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

      let routedToAwards = 0
      if (round?.competitionId) {
        routedToAwards = await ctx.prisma.awardEligibility.count({
          where: {
            award: {
              competitionId: round.competitionId,
              eligibilityMode: 'SEPARATE_POOL',
            },
            shortlisted: true,
            confirmedAt: { not: null },
          },
        })
      }

      return { passed, filteredOut, flagged, overridden, routedToAwards, total: passed + filteredOut + flagged }
    }),

  /**
   * Get filtering results list for a round (observer proxy of filtering.getResults)
   */
  getFilteringResults: observerProcedure
    .input(z.object({
      roundId: z.string(),
      outcome: z.enum(['PASSED', 'FILTERED_OUT', 'FLAGGED']).optional(),
      page: z.number().int().min(1).default(1),
      perPage: z.number().int().min(1).max(100).default(20),
    }))
    .query(async ({ ctx, input }) => {
      const { roundId, outcome, page, perPage } = input
      const skip = (page - 1) * perPage

      const where: Record<string, unknown> = { roundId }
      if (outcome) {
        where.OR = [
          { finalOutcome: outcome },
          { finalOutcome: null, outcome },
        ]
      }

      const [results, total] = await Promise.all([
        ctx.prisma.filteringResult.findMany({
          where,
          skip,
          take: perPage,
          orderBy: { createdAt: 'desc' },
          select: {
            id: true,
            outcome: true,
            finalOutcome: true,
            aiScreeningJson: true,
            overrideReason: true,
            project: {
              select: {
                id: true,
                title: true,
                teamName: true,
                competitionCategory: true,
                country: true,
                awardEligibilities: {
                  where: {
                    shortlisted: true,
                    confirmedAt: { not: null },
                    award: { eligibilityMode: 'SEPARATE_POOL' },
                  },
                  select: {
                    award: { select: { name: true } },
                  },
                },
              },
            },
          },
        }),
        ctx.prisma.filteringResult.count({ where }),
      ])

      return {
        results,
        total,
        page,
        perPage,
        totalPages: Math.ceil(total / perPage),
      }
    }),

  /**
   * Get deliberation sessions for a round
   */
  getDeliberationSessions: observerProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const sessions = await ctx.prisma.deliberationSession.findMany({
        where: { roundId: input.roundId },
        select: {
          id: true,
          category: true,
          status: true,
          mode: true,
          _count: { select: { votes: true, participants: true } },
        },
        orderBy: { createdAt: 'desc' },
      })
      return sessions
    }),

  /**
   * Get aggregated vote results for a deliberation session
   */
  getDeliberationAggregate: observerProcedure
    .input(z.object({ sessionId: z.string() }))
    .query(async ({ ctx, input }) => {
      const agg = await aggregateVotes(input.sessionId, ctx.prisma)

      const projectIds = agg.rankings.map((r) => r.projectId)
      const projects = await ctx.prisma.project.findMany({
        where: { id: { in: projectIds } },
        select: { id: true, title: true, teamName: true },
      })
      const projectMap = new Map(projects.map((p) => [p.id, p]))

      return {
        rankings: agg.rankings.map((r) => ({
          ...r,
          projectTitle: projectMap.get(r.projectId)?.title ?? 'Unknown',
          teamName: projectMap.get(r.projectId)?.teamName ?? '',
        })),
        hasTies: agg.hasTies,
        tiedProjectIds: agg.tiedProjectIds,
      }
    }),

  /**
   * Get juror score matrix for a round (capped at 30 most-assigned projects)
   */
  getJurorScoreMatrix: observerProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const assignments = await ctx.prisma.assignment.findMany({
        where: { roundId: input.roundId },
        include: {
          user: { select: { id: true, name: true } },
          project: { select: { id: true, title: true } },
          evaluation: {
            select: { globalScore: true, status: true },
          },
        },
      })

      const jurorMap = new Map<string, string>()
      const projectMap = new Map<string, string>()
      const cells: { jurorId: string; projectId: string; score: number | null }[] = []

      for (const a of assignments) {
        jurorMap.set(a.user.id, a.user.name ?? 'Unknown')
        projectMap.set(a.project.id, a.project.title)

        if (a.evaluation?.status === 'SUBMITTED') {
          cells.push({
            jurorId: a.user.id,
            projectId: a.project.id,
            score: a.evaluation.globalScore,
          })
        }
      }

      const projectAssignCounts = new Map<string, number>()
      for (const a of assignments) {
        projectAssignCounts.set(a.project.id, (projectAssignCounts.get(a.project.id) ?? 0) + 1)
      }
      const topProjectIds = [...projectAssignCounts.entries()]
        .sort(([, a], [, b]) => b - a)
        .slice(0, 30)
        .map(([id]) => id)

      const topProjectSet = new Set(topProjectIds)

      return {
        jurors: [...jurorMap.entries()].map(([id, name]) => ({ id, name })),
        projects: topProjectIds.map((id) => ({ id, title: projectMap.get(id) ?? 'Unknown' })),
        cells: cells.filter((c) => topProjectSet.has(c.projectId)),
        truncated: projectAssignCounts.size > 30,
        totalProjects: projectAssignCounts.size,
      }
    }),

  // =========================================================================
  // Observer Dashboard V2 Queries
  // =========================================================================

  getPreviousRoundComparison: observerProcedure
    .input(z.object({ currentRoundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const currentRound = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.currentRoundId },
        select: { id: true, name: true, roundType: true, sortOrder: true, competitionId: true },
      })

      // Find the previous round by sortOrder
      const previousRound = await ctx.prisma.round.findFirst({
        where: {
          competitionId: currentRound.competitionId,
          sortOrder: { lt: currentRound.sortOrder },
        },
        orderBy: { sortOrder: 'desc' },
        select: { id: true, name: true, roundType: true, sortOrder: true },
      })

      if (!previousRound) {
        return { hasPrevious: false as const }
      }

      // Get project counts and category breakdowns for both rounds
      const [prevStates, currStates, prevProjects, currProjects] = await Promise.all([
        ctx.prisma.projectRoundState.findMany({
          where: { roundId: previousRound.id },
          select: { projectId: true, state: true },
        }),
        ctx.prisma.projectRoundState.findMany({
          where: { roundId: currentRound.id },
          select: { projectId: true, state: true },
        }),
        ctx.prisma.project.findMany({
          where: { projectRoundStates: { some: { roundId: previousRound.id } } },
          select: { id: true, competitionCategory: true, country: true },
        }),
        ctx.prisma.project.findMany({
          where: { projectRoundStates: { some: { roundId: currentRound.id } } },
          select: { id: true, competitionCategory: true, country: true },
        }),
      ])

      const prevPassedCount = prevStates.filter(s => s.state === 'PASSED').length
      const prevRejectedCount = prevStates.filter(s => s.state === 'REJECTED').length

      // Category breakdown
      const prevByCategory = new Map<string, number>()
      prevProjects.forEach(p => {
        const cat = p.competitionCategory ?? 'Uncategorized'
        prevByCategory.set(cat, (prevByCategory.get(cat) ?? 0) + 1)
      })
      const currByCategory = new Map<string, number>()
      currProjects.forEach(p => {
        const cat = p.competitionCategory ?? 'Uncategorized'
        currByCategory.set(cat, (currByCategory.get(cat) ?? 0) + 1)
      })
      const allCategories = new Set([...prevByCategory.keys(), ...currByCategory.keys()])
      const categoryBreakdown = [...allCategories].map(cat => ({
        category: cat,
        previous: prevByCategory.get(cat) ?? 0,
        current: currByCategory.get(cat) ?? 0,
        eliminated: (prevByCategory.get(cat) ?? 0) - (currByCategory.get(cat) ?? 0),
      }))

      const prevByCountry = new Map<string, number>()
      prevProjects.forEach(p => {
        const c = extractCountry(p.country)
        prevByCountry.set(c, (prevByCountry.get(c) ?? 0) + 1)
      })
      const currByCountry = new Map<string, number>()
      currProjects.forEach(p => {
        const c = extractCountry(p.country)
        currByCountry.set(c, (currByCountry.get(c) ?? 0) + 1)
      })
      const allCountries = new Set([...prevByCountry.keys(), ...currByCountry.keys()])
      const countryAttrition = [...allCountries]
        .map(country => ({
          country,
          previous: prevByCountry.get(country) ?? 0,
          current: currByCountry.get(country) ?? 0,
          lost: (prevByCountry.get(country) ?? 0) - (currByCountry.get(country) ?? 0),
        }))
        .filter(c => c.lost > 0)
        .sort((a, b) => b.lost - a.lost)
        .slice(0, 10)

      // Average scores (if evaluation rounds)
      let prevAvgScore: number | null = null
      let currAvgScore: number | null = null
      const [prevEvals, currEvals] = await Promise.all([
        ctx.prisma.evaluation.findMany({
          where: { assignment: { roundId: previousRound.id }, status: 'SUBMITTED', globalScore: { not: null } },
          select: { globalScore: true },
        }),
        ctx.prisma.evaluation.findMany({
          where: { assignment: { roundId: currentRound.id }, status: 'SUBMITTED', globalScore: { not: null } },
          select: { globalScore: true },
        }),
      ])
      if (prevEvals.length > 0) {
        prevAvgScore = prevEvals.reduce((sum, e) => sum + (e.globalScore ?? 0), 0) / prevEvals.length
      }
      if (currEvals.length > 0) {
        currAvgScore = currEvals.reduce((sum, e) => sum + (e.globalScore ?? 0), 0) / currEvals.length
      }

      return {
        hasPrevious: true as const,
        previousRound: {
          id: previousRound.id,
          name: previousRound.name,
          type: previousRound.roundType,
          projectCount: prevProjects.length,
          avgScore: prevAvgScore,
          passedCount: prevPassedCount,
          rejectedCount: prevRejectedCount,
        },
        currentRound: {
          id: currentRound.id,
          name: currentRound.name,
          type: currentRound.roundType,
          projectCount: currProjects.length,
          avgScore: currAvgScore,
        },
        eliminated: prevProjects.length - currProjects.length,
        categoryBreakdown,
        countryAttrition,
      }
    }),

  getRoundAdvancementConfig: observerProcedure
    .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 },
      })

      const config = round.configJson as Record<string, unknown> | null
      if (!config) return null

      return {
        advanceMode: (config.advanceMode as string) ?? 'count',
        startupAdvanceCount: config.startupAdvanceCount as number | undefined,
        conceptAdvanceCount: config.conceptAdvanceCount as number | undefined,
        advanceScoreThreshold: config.advanceScoreThreshold as number | undefined,
        advancementMode: config.advancementMode as string | undefined,
        advancementConfig: config.advancementConfig as Record<string, unknown> | undefined,
      }
    }),

  /**
   * Returns rounds the project has participated in, restricted to those that
   * are open or already closed. Used by the observer full project page to
   * resolve a default round when none is specified in the URL.
   */
  getProjectRoundsForObserver: observerProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      const states = await ctx.prisma.projectRoundState.findMany({
        where: { projectId: input.projectId },
        select: {
          round: { select: { id: true, name: true, status: true, sortOrder: true } },
        },
      })
      return states
        .map((s) => s.round)
        .filter((r) => r.status === 'ROUND_ACTIVE' || r.status === 'ROUND_CLOSED')
        .sort((a, b) => a.sortOrder - b.sortOrder)
    }),

  getRecentFiles: observerProcedure
    .input(z.object({ roundId: z.string(), limit: z.number().min(1).max(50).default(10) }))
    .query(async ({ ctx, input }) => {
      const files = await ctx.prisma.projectFile.findMany({
        where: { roundId: input.roundId },
        orderBy: { createdAt: 'desc' },
        take: input.limit,
        select: {
          id: true,
          fileName: true,
          fileType: true,
          createdAt: true,
          pageCount: true,
          detectedLang: true,
          langConfidence: true,
          analyzedAt: true,
          project: {
            select: { id: true, title: true, teamName: true, logoKey: true },
          },
        },
      })
      return files
    }),

  getMentoringDashboard: observerProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const assignments = await ctx.prisma.mentorAssignment.findMany({
        where: {
          project: { projectRoundStates: { some: { roundId: input.roundId } } },
        },
        select: {
          id: true,
          mentorId: true,
          projectId: true,
          completionStatus: true,
          mentor: { select: { id: true, name: true } },
          project: { select: { id: true, title: true, teamName: true } },
          messages: { select: { id: true } },
        },
      })

      // Group by mentor
      const mentorMap = new Map<string, {
        mentorName: string
        mentorId: string
        projects: { id: string; title: string; teamName: string | null; messageCount: number }[]
      }>()

      let totalMessages = 0
      const activeMentorIds = new Set<string>()

      for (const a of assignments) {
        const mentorId = a.mentorId
        if (!mentorMap.has(mentorId)) {
          mentorMap.set(mentorId, {
            mentorName: a.mentor.name ?? 'Unknown',
            mentorId,
            projects: [],
          })
        }
        const messageCount = a.messages.length
        totalMessages += messageCount
        if (messageCount > 0) activeMentorIds.add(mentorId)

        mentorMap.get(mentorId)!.projects.push({
          id: a.project.id,
          title: a.project.title,
          teamName: a.project.teamName,
          messageCount,
        })
      }

      return {
        assignments: [...mentorMap.values()],
        totalMessages,
        activeMentors: activeMentorIds.size,
        totalMentors: mentorMap.size,
      }
    }),

  getLiveFinalDashboard: observerProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      // Get round config for observer visibility setting
      const round = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { configJson: true, roundType: true },
      })

      let observerScoreVisibility: 'realtime' | 'after_completion' | 'hidden' = 'after_completion'
      try {
        if (round.roundType === 'LIVE_FINAL') {
          const config = validateRoundConfig('LIVE_FINAL', round.configJson) as LiveFinalConfig
          observerScoreVisibility = config.observerScoreVisibility ?? 'after_completion'
        }
      } catch (err) { console.error('Failed to parse LIVE_FINAL round config for observer score visibility:', err) /* use default */ }

      const session = await ctx.prisma.liveVotingSession.findUnique({
        where: { roundId: input.roundId },
        select: {
          id: true,
          status: true,
          currentProjectIndex: true,
          projectOrderJson: true,
          votingMode: true,
          votes: {
            select: {
              userId: true,
              projectId: true,
              score: true,
            },
          },
        },
      })

      if (!session) {
        return {
          sessionStatus: 'NOT_STARTED' as const,
          observerScoreVisibility,
          voteCount: 0,
          jurors: [] as { id: string; name: string; hasVoted: boolean }[],
          standings: null,
        }
      }

      // Get jurors assigned to this round
      const jurorUsers = await ctx.prisma.assignment.findMany({
        where: { roundId: input.roundId },
        select: { userId: true, user: { select: { id: true, name: true } } },
        distinct: ['userId'],
      })

      const voterIds = new Set(session.votes.map(v => v.userId))
      const jurors = jurorUsers.map(j => ({
        id: j.user.id,
        name: j.user.name ?? 'Unknown',
        hasVoted: voterIds.has(j.user.id),
      }))

      // Calculate standings if visibility allows
      const showScores =
        observerScoreVisibility === 'realtime' ||
        (observerScoreVisibility === 'after_completion' && session.status === 'COMPLETED')

      let standings: { projectId: string; projectTitle: string; avgScore: number; voteCount: number }[] | null = null

      if (showScores && session.votes.length > 0) {
        const projectScores = new Map<string, number[]>()
        for (const v of session.votes) {
          if (v.score != null) {
            if (!projectScores.has(v.projectId)) projectScores.set(v.projectId, [])
            projectScores.get(v.projectId)!.push(v.score)
          }
        }

        const projectIds = [...projectScores.keys()]
        const projects = await ctx.prisma.project.findMany({
          where: { id: { in: projectIds } },
          select: { id: true, title: true },
        })
        const projMap = new Map(projects.map(p => [p.id, p.title]))

        standings = [...projectScores.entries()]
          .map(([projectId, scores]) => ({
            projectId,
            projectTitle: projMap.get(projectId) ?? 'Unknown',
            avgScore: scores.reduce((a, b) => a + b, 0) / scores.length,
            voteCount: scores.length,
          }))
          .sort((a, b) => b.avgScore - a.avgScore)
      }

      return {
        sessionStatus: session.status,
        observerScoreVisibility,
        voteCount: session.votes.length,
        jurors,
        standings,
      }
    }),

  /**
   * AI-powered juror calibration analysis for an evaluation round.
   * Produces a plain-language explanation of the per-juror z-score balance
   * already applied in ranking — describes, does not prescribe.
   */
  generateJurorCalibration: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return generateJurorCalibration({
        roundId: input.roundId,
        userId: ctx.user.id,
        prisma: ctx.prisma,
      })
    }),

  /**
   * Nationality breakdown for the applicants (team members) of projects in
   * the selected scope. Counts UNIQUE users so a single applicant on
   * multiple teams isn't double-counted.
   *
   * Scope:
   *   - roundId set → projects with a ProjectRoundState in that round
   *   - programId set → projects in that program
   *   - neither → all team members across all projects (global)
   */
  getApplicantNationalities: adminProcedure
    .input(
      z
        .object({
          roundId: z.string().optional(),
          programId: z.string().optional(),
        })
        .optional()
    )
    .query(async ({ ctx, input }) => {
      const roundId = input?.roundId
      const programId = input?.programId

      const projectFilter = roundId
        ? { projectRoundStates: { some: { roundId } } }
        : programId
          ? { programId }
          : {}

      // Pull all distinct team-member userIds + their nationality in one query.
      // `distinct: ['userId']` collapses a user appearing on multiple teams in
      // the same scope to a single row.
      const teamMembers = await ctx.prisma.teamMember.findMany({
        where: { project: projectFilter },
        select: { userId: true, user: { select: { nationality: true } } },
        distinct: ['userId'],
      })

      const total = teamMembers.length
      const declaredEntries = teamMembers.filter(
        (tm) => tm.user?.nationality && tm.user.nationality.trim().length > 0
      )
      const declared = declaredEntries.length
      const notDeclared = total - declared

      const counts = new Map<string, number>()
      for (const tm of declaredEntries) {
        const code = (tm.user!.nationality as string).trim()
        counts.set(code, (counts.get(code) ?? 0) + 1)
      }

      const byCountry = Array.from(counts.entries())
        .map(([country, count]) => ({ country, count }))
        .sort((a, b) => b.count - a.count || a.country.localeCompare(b.country))

      return {
        total,
        declared,
        notDeclared,
        byCountry,
      }
    }),
})
