/**
 * Round Finalization Service
 *
 * Handles the post-close lifecycle of a round:
 * - processRoundClose: auto-sets project states after a round closes
 * - getFinalizationSummary: aggregates data for the finalization review UI
 * - confirmFinalization: single transaction to apply outcomes, advance projects, send emails
 */

import type { PrismaClient, ProjectRoundStateValue, RoundType, Prisma } from '@prisma/client'
import { transitionProject, isTerminalState } from './round-engine'
import { logAudit } from '@/server/utils/audit'
import { getRejectionNotificationTemplate } from '@/lib/email'
import { createBulkNotifications } from '../services/in-app-notification'
import { generateInviteToken, getInviteExpiryMs } from '@/server/utils/invite'
import { sendBatchNotifications } from './notification-sender'
import type { NotificationItem } from './notification-sender'

// ─── Types ──────────────────────────────────────────────────────────────────

export type FinalizationSummary = {
  roundId: string
  roundName: string
  roundType: RoundType
  isGracePeriodActive: boolean
  gracePeriodEndsAt: Date | null
  isFinalized: boolean
  finalizedAt: Date | null
  stats: {
    pending: number
    inProgress: number
    completed: number
    passed: number
    rejected: number
    withdrawn: number
  }
  projects: Array<{
    id: string
    title: string
    teamName: string | null
    category: string | null
    country: string | null
    currentState: ProjectRoundStateValue
    proposedOutcome: ProjectRoundStateValue | null
    evaluationScore?: number | null
    rankPosition?: number | null
    documentsSubmitted?: number | null
    documentsRequired?: number | null
  }>
  categoryTargets: {
    startupTarget: number | null
    conceptTarget: number | null
    startupProposed: number
    conceptProposed: number
  }
  nextRound: { id: string; name: string } | null
  /**
   * Set when this round has no further round to advance to — passing teams
   * are winners, not advancers. Covers both the main-track terminal round
   * (winners of the competition, possibly multiple e.g. top 3) and the
   * terminal round of a special award. `label` is what we tell the team they
   * have won (competition name or award name); `awardId` is set only when
   * the round is attached to a SpecialAward.
   */
  winnerContext: { label: string; awardId: string | null } | null
  accountStats: {
    needsInvite: number
    hasAccount: number
  }
}

export type ConfirmFinalizationResult = {
  advanced: number
  rejected: number
  emailsSent: number
  emailsFailed: number
}

// ─── processRoundClose ──────────────────────────────────────────────────────

/**
 * Process project states after a round closes.
 * Auto-transitions projects to COMPLETED/REJECTED and sets proposedOutcome defaults.
 * Called immediately on close (if no grace period) or after grace period expires.
 */
export async function processRoundClose(
  roundId: string,
  actorId: string,
  prisma: PrismaClient,
): Promise<{ processed: number }> {
  const round = await prisma.round.findUnique({
    where: { id: roundId },
    include: {
      competition: {
        select: {
          rounds: {
            select: { id: true, name: true, sortOrder: true },
            orderBy: { sortOrder: 'asc' as const },
          },
        },
      },
    },
  })

  if (!round) throw new Error(`Round ${roundId} not found`)

  const projectStates = await prisma.projectRoundState.findMany({
    where: { roundId },
    include: {
      project: {
        select: {
          id: true,
          competitionCategory: true,
          files: { where: { roundId }, select: { id: true, requirementId: true, submissionFileRequirementId: true } },
          assignments: { where: { roundId }, select: { isCompleted: true } },
          filteringResults: { where: { roundId }, select: { outcome: true, finalOutcome: true } },
        },
      },
    },
  })

  let processed = 0

  // Pre-compute pass set for EVALUATION rounds using ranking scores + config.
  // Respects admin drag-reorder overrides stored in reordersJson.
  let evaluationPassSet: Set<string> | null = null
  if ((round.roundType as RoundType) === 'EVALUATION') {
    evaluationPassSet = new Set<string>()
    const snapshot = await prisma.rankingSnapshot.findFirst({
      where: { roundId },
      orderBy: { createdAt: 'desc' as const },
      select: { startupRankingJson: true, conceptRankingJson: true, reordersJson: true },
    })
    if (snapshot) {
      const config = (round.configJson as Record<string, unknown>) ?? {}
      const advanceMode = (config.advanceMode as string) || 'count'
      const advanceScoreThreshold = (config.advanceScoreThreshold as number) ?? 6
      const startupAdvanceCount = (config.startupAdvanceCount as number) ?? 0
      const conceptAdvanceCount = (config.conceptAdvanceCount as number) ?? 0

      type RankEntry = { projectId: string; avgGlobalScore: number | null; rank: number }
      const startupRanked = (snapshot.startupRankingJson ?? []) as RankEntry[]
      const conceptRanked = (snapshot.conceptRankingJson ?? []) as RankEntry[]

      // Apply admin drag-reorder overrides (reordersJson is append-only, latest per category wins)
      type ReorderEvent = { category: 'STARTUP' | 'BUSINESS_CONCEPT'; orderedProjectIds: string[] }
      const reorders = (snapshot.reordersJson as ReorderEvent[] | null) ?? []
      const latestStartupReorder = [...reorders].reverse().find((r) => r.category === 'STARTUP')
      const latestConceptReorder = [...reorders].reverse().find((r) => r.category === 'BUSINESS_CONCEPT')

      // Build effective order: if admin reordered, use that; otherwise use computed rank order
      const effectiveStartup = latestStartupReorder
        ? latestStartupReorder.orderedProjectIds
        : [...startupRanked].sort((a, b) => a.rank - b.rank).map((r) => r.projectId)
      const effectiveConcept = latestConceptReorder
        ? latestConceptReorder.orderedProjectIds
        : [...conceptRanked].sort((a, b) => a.rank - b.rank).map((r) => r.projectId)

      // Build score lookup for threshold mode
      const scoreMap = new Map<string, number>()
      for (const r of [...startupRanked, ...conceptRanked]) {
        if (r.avgGlobalScore != null) scoreMap.set(r.projectId, r.avgGlobalScore)
      }

      if (advanceMode === 'threshold') {
        for (const id of [...effectiveStartup, ...effectiveConcept]) {
          const score = scoreMap.get(id)
          if (score != null && score >= advanceScoreThreshold) {
            evaluationPassSet.add(id)
          }
        }
      } else {
        // 'count' mode — top N per category using effective (possibly reordered) order
        for (let i = 0; i < Math.min(startupAdvanceCount, effectiveStartup.length); i++) {
          evaluationPassSet.add(effectiveStartup[i])
        }
        for (let i = 0; i < Math.min(conceptAdvanceCount, effectiveConcept.length); i++) {
          evaluationPassSet.add(effectiveConcept[i])
        }
      }
    }
  }

  // ── Phase 1: Compute target states and proposed outcomes in-memory ──
  type StateUpdate = {
    prsId: string
    projectId: string
    currentState: string
    targetState: ProjectRoundStateValue
    proposedOutcome: ProjectRoundStateValue
    needsTransition: boolean
  }

  const updates: StateUpdate[] = []

  for (const prs of projectStates) {
    // Skip already-terminal states
    if (isTerminalState(prs.state)) {
      if (!prs.proposedOutcome) {
        updates.push({
          prsId: prs.id,
          projectId: prs.projectId,
          currentState: prs.state,
          targetState: prs.state as ProjectRoundStateValue,
          proposedOutcome: prs.state as ProjectRoundStateValue,
          needsTransition: false,
        })
      }
      processed++
      continue
    }

    let targetState: ProjectRoundStateValue = prs.state
    let proposedOutcome: ProjectRoundStateValue = 'PASSED'

    switch (round.roundType as RoundType) {
      case 'INTAKE':
      case 'SUBMISSION': {
        if (prs.state === 'PENDING') {
          targetState = 'REJECTED' as ProjectRoundStateValue
          proposedOutcome = 'REJECTED' as ProjectRoundStateValue
        } else if (prs.state === 'IN_PROGRESS' || prs.state === 'COMPLETED') {
          if (prs.state === 'IN_PROGRESS') targetState = 'COMPLETED' as ProjectRoundStateValue
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        }
        break
      }

      case 'EVALUATION': {
        const hasEvals = prs.project.assignments.some((a: { isCompleted: boolean }) => a.isCompleted)
        const shouldPass = evaluationPassSet?.has(prs.projectId) ?? false
        if (prs.state === 'IN_PROGRESS' || (prs.state === 'PENDING' && hasEvals)) {
          targetState = 'COMPLETED' as ProjectRoundStateValue
          proposedOutcome = (shouldPass ? 'PASSED' : 'REJECTED') as ProjectRoundStateValue
        } else if (prs.state === 'PENDING') {
          targetState = 'REJECTED' as ProjectRoundStateValue
          proposedOutcome = 'REJECTED' as ProjectRoundStateValue
        } else if (prs.state === 'COMPLETED') {
          proposedOutcome = (shouldPass ? 'PASSED' : 'REJECTED') as ProjectRoundStateValue
        }
        break
      }

      case 'FILTERING': {
        const fr = prs.project.filteringResults?.[0] as { outcome: string; finalOutcome: string | null } | undefined
        const effectiveOutcome = fr?.finalOutcome || fr?.outcome
        const filterPassed = effectiveOutcome !== 'FILTERED_OUT'

        if (prs.state === 'COMPLETED') {
          proposedOutcome = (filterPassed ? 'PASSED' : 'REJECTED') as ProjectRoundStateValue
        } else if (prs.state === 'IN_PROGRESS') {
          targetState = 'COMPLETED' as ProjectRoundStateValue
          proposedOutcome = (filterPassed ? 'PASSED' : 'REJECTED') as ProjectRoundStateValue
        } else if (prs.state === 'PENDING') {
          if (fr) {
            targetState = 'COMPLETED' as ProjectRoundStateValue
            proposedOutcome = (filterPassed ? 'PASSED' : 'REJECTED') as ProjectRoundStateValue
          } else {
            targetState = 'REJECTED' as ProjectRoundStateValue
            proposedOutcome = 'REJECTED' as ProjectRoundStateValue
          }
        }
        break
      }

      case 'MENTORING': {
        if (prs.state === 'PASSED') {
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        } else if (prs.state === 'IN_PROGRESS') {
          targetState = 'COMPLETED' as ProjectRoundStateValue
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        } else if (prs.state === 'COMPLETED') {
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        } else if (prs.state === 'PENDING') {
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
          targetState = 'COMPLETED' as ProjectRoundStateValue
        }
        break
      }

      case 'LIVE_FINAL': {
        if (prs.state === 'IN_PROGRESS' || prs.state === 'PENDING') {
          targetState = 'COMPLETED' as ProjectRoundStateValue
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        } else if (prs.state === 'COMPLETED') {
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        }
        break
      }

      case 'DELIBERATION': {
        if (prs.state === 'IN_PROGRESS' || prs.state === 'PENDING') {
          targetState = 'COMPLETED' as ProjectRoundStateValue
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        } else if (prs.state === 'COMPLETED') {
          proposedOutcome = 'PASSED' as ProjectRoundStateValue
        }
        break
      }
    }

    const needsTransition = targetState !== prs.state && !isTerminalState(prs.state)
    updates.push({
      prsId: prs.id,
      projectId: prs.projectId,
      currentState: prs.state,
      targetState,
      proposedOutcome,
      needsTransition,
    })
    processed++
  }

  // ── Phase 2: Batch state transitions in a single transaction ──
  const transitionUpdates = updates.filter((u) => u.needsTransition)
  const now = new Date()

  if (transitionUpdates.length > 0) {
    await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
      // Step through intermediate states in bulk
      // PENDING → IN_PROGRESS for projects going to COMPLETED
      const pendingToCompleted = transitionUpdates.filter(
        (u) => u.currentState === 'PENDING' && u.targetState === ('COMPLETED' as string),
      )
      if (pendingToCompleted.length > 0) {
        await tx.projectRoundState.updateMany({
          where: { id: { in: pendingToCompleted.map((u) => u.prsId) } },
          data: { state: 'IN_PROGRESS' },
        })
      }

      // IN_PROGRESS → COMPLETED (includes those just moved from PENDING)
      const toCompleted = transitionUpdates.filter(
        (u) => u.targetState === ('COMPLETED' as string) &&
          (u.currentState === 'PENDING' || u.currentState === 'IN_PROGRESS'),
      )
      if (toCompleted.length > 0) {
        await tx.projectRoundState.updateMany({
          where: { id: { in: toCompleted.map((u) => u.prsId) } },
          data: { state: 'COMPLETED' },
        })
      }

      // PENDING → REJECTED (direct terminal transition)
      const pendingToRejected = transitionUpdates.filter(
        (u) => u.currentState === 'PENDING' && u.targetState === ('REJECTED' as string),
      )
      if (pendingToRejected.length > 0) {
        await tx.projectRoundState.updateMany({
          where: { id: { in: pendingToRejected.map((u) => u.prsId) } },
          data: { state: 'REJECTED', exitedAt: now },
        })
      }

      // Other single-step transitions (e.g., IN_PROGRESS → COMPLETED already handled)
      const otherTransitions = transitionUpdates.filter(
        (u) =>
          !(u.currentState === 'PENDING' && (u.targetState === ('COMPLETED' as string) || u.targetState === ('REJECTED' as string))) &&
          !(u.currentState === 'IN_PROGRESS' && u.targetState === ('COMPLETED' as string)),
      )
      if (otherTransitions.length > 0) {
        await tx.projectRoundState.updateMany({
          where: { id: { in: otherTransitions.map((u) => u.prsId) } },
          data: { state: otherTransitions[0].targetState },
        })
      }

      // Batch create audit logs for all transitions
      await tx.decisionAuditLog.createMany({
        data: transitionUpdates.map((u) => ({
          eventType: 'project_round.transitioned',
          entityType: 'ProjectRoundState',
          entityId: u.prsId,
          actorId,
          detailsJson: {
            projectId: u.projectId,
            roundId,
            previousState: u.currentState,
            newState: u.targetState,
            batchProcessed: true,
          } as Prisma.InputJsonValue,
          snapshotJson: {
            timestamp: now.toISOString(),
            emittedBy: 'round-finalization',
          },
        })),
      })
    })
  }

  // ── Phase 3: Batch update proposed outcomes ──
  const outcomeUpdates = updates.filter((u) => u.proposedOutcome)
  // Group by proposed outcome for efficient updateMany calls
  const outcomeGroups = new Map<ProjectRoundStateValue, string[]>()
  for (const u of outcomeUpdates) {
    const ids = outcomeGroups.get(u.proposedOutcome) ?? []
    ids.push(u.prsId)
    outcomeGroups.set(u.proposedOutcome, ids)
  }
  await Promise.all(
    Array.from(outcomeGroups.entries()).map(([outcome, ids]) =>
      prisma.projectRoundState.updateMany({
        where: { id: { in: ids } },
        data: { proposedOutcome: outcome },
      }),
    ),
  )

  return { processed }
}

// ─── Helpers ────────────────────────────────────────────────────────────────

/**
 * Pick the round to *display* as the destination after finalization.
 *
 * Mentoring rounds are opt-in (eligibility: "requested_only") — only teams that
 * requested mentorship enter, and the rest auto-pass through to the round after.
 * For user-facing copy ("X teams will advance to ..."), we therefore skip past
 * MENTORING rounds and report the next "proper" round (e.g. Grand Finale)
 * that all advancing teams ultimately share. The actual targetRoundId used by
 * the round engine for state transitions is unaffected.
 */
export function findDisplayNextRound<T extends { id: string; name: string; roundType?: RoundType }>(
  rounds: readonly T[],
  currentRoundId: string,
): T | null {
  const currentIdx = rounds.findIndex((r) => r.id === currentRoundId)
  if (currentIdx < 0) return null
  for (let i = currentIdx + 1; i < rounds.length; i++) {
    if (rounds[i].roundType !== 'MENTORING') return rounds[i]
  }
  return null
}

// ─── getFinalizationSummary ─────────────────────────────────────────────────

export async function getFinalizationSummary(
  roundId: string,
  prisma: PrismaClient,
): Promise<FinalizationSummary> {
  const round = await prisma.round.findUniqueOrThrow({
    where: { id: roundId },
    include: {
      competition: {
        select: {
          name: true,
          rounds: {
            select: { id: true, name: true, sortOrder: true, roundType: true },
            orderBy: { sortOrder: 'asc' as const },
          },
        },
      },
      specialAward: { select: { id: true, name: true } },
    },
  })

  const now = new Date()
  const isGracePeriodActive = !!(round.gracePeriodEndsAt && new Date(round.gracePeriodEndsAt) > now && !round.finalizedAt)
  const isFinalized = !!round.finalizedAt

  // Get config for category targets
  const config = (round.configJson as Record<string, unknown>) ?? {}

  // Find next round (skip MENTORING rounds — those are opt-in and not the
  // shared destination for all advancing teams)
  const rounds = round.competition.rounds
  const nextRound = findDisplayNextRound(rounds, roundId)

  // Terminal round: no further round to advance to. Passing teams are
  // winners (possibly multiple — e.g. top 3 in the main bracket, or the
  // recipient of a special award). The label is what we tell them they've won.
  const winnerContext = !nextRound
    ? round.specialAward
      ? { label: round.specialAward.name, awardId: round.specialAward.id }
      : { label: round.competition.name, awardId: null }
    : null

  // Get all project states with project details
  const projectStates = await prisma.projectRoundState.findMany({
    where: { roundId },
    include: {
      project: {
        select: {
          id: true,
          title: true,
          teamName: true,
          competitionCategory: true,
          country: true,
        },
      },
    },
    orderBy: { createdAt: 'asc' as const },
  })

  // Compute stats
  const stats = { pending: 0, inProgress: 0, completed: 0, passed: 0, rejected: 0, withdrawn: 0 }
  for (const prs of projectStates) {
    switch (prs.state) {
      case 'PENDING': stats.pending++; break
      case 'IN_PROGRESS': stats.inProgress++; break
      case 'COMPLETED': stats.completed++; break
      case 'PASSED': stats.passed++; break
      case 'REJECTED': stats.rejected++; break
      case 'WITHDRAWN': stats.withdrawn++; break
    }
  }

  // Get evaluation scores if this is an evaluation round
  let scoreMap = new Map<string, number>()
  let rankMap = new Map<string, number>()

  if (round.roundType === 'EVALUATION') {
    // Get latest ranking snapshot (per-category fields)
    const snapshot = await prisma.rankingSnapshot.findFirst({
      where: { roundId },
      orderBy: { createdAt: 'desc' as const },
      select: { startupRankingJson: true, conceptRankingJson: true },
    })
    if (snapshot) {
      type RankEntry = { projectId: string; avgGlobalScore?: number; compositeScore?: number; rank?: number }
      const allRanked = [
        ...((snapshot.startupRankingJson ?? []) as RankEntry[]),
        ...((snapshot.conceptRankingJson ?? []) as RankEntry[]),
      ]
      for (const r of allRanked) {
        if (r.avgGlobalScore != null) scoreMap.set(r.projectId, r.avgGlobalScore)
        else if (r.compositeScore != null) scoreMap.set(r.projectId, r.compositeScore)
        if (r.rank != null) rankMap.set(r.projectId, r.rank)
      }
    }
  }

  // Get document submission counts for SUBMISSION/INTAKE rounds
  let documentsRequired: number | null = null
  let docCountMap = new Map<string, number>()

  if (round.roundType === 'SUBMISSION' || round.roundType === 'INTAKE') {
    const requirements = await prisma.fileRequirement.findMany({
      where: { roundId, isRequired: true },
      select: { id: true },
    })
    documentsRequired = requirements.length

    if (documentsRequired > 0) {
      const projectIds = projectStates.map((prs: any) => prs.project.id)
      const fileCounts = await prisma.projectFile.groupBy({
        by: ['projectId'],
        where: {
          roundId,
          projectId: { in: projectIds },
          OR: [
            { requirementId: { in: requirements.map((r) => r.id) } },
            { requirementId: null },
          ],
        },
        _count: { _all: true },
      })
      for (const fc of fileCounts) {
        docCountMap.set(fc.projectId, fc._count._all)
      }
    }
  }

  // Build project list
  const projects = projectStates.map((prs: any) => ({
    id: prs.project.id,
    title: prs.project.title,
    teamName: prs.project.teamName,
    category: prs.project.competitionCategory,
    country: prs.project.country,
    currentState: prs.state as ProjectRoundStateValue,
    proposedOutcome: prs.proposedOutcome as ProjectRoundStateValue | null,
    evaluationScore: scoreMap.get(prs.project.id) ?? null,
    rankPosition: rankMap.get(prs.project.id) ?? null,
    documentsSubmitted: documentsRequired != null ? (docCountMap.get(prs.project.id) ?? 0) : null,
    documentsRequired,
  }))

  // Category target progress
  const startupTarget = (config.startupAdvanceCount as number | undefined) ?? null
  const conceptTarget = (config.conceptAdvanceCount as number | undefined) ?? null

  let startupProposed = 0
  let conceptProposed = 0
  for (const p of projects) {
    if (p.proposedOutcome === 'PASSED') {
      if (p.category === 'STARTUP') startupProposed++
      else if (p.category === 'BUSINESS_CONCEPT') conceptProposed++
    }
  }

  // Account stats: count how many advancing projects need invite vs already have accounts
  let needsInvite = 0
  let hasAccount = 0
  const passedProjectIds = projects.filter((p: { proposedOutcome: string | null }) => p.proposedOutcome === 'PASSED').map((p: { id: string }) => p.id)
  if (passedProjectIds.length > 0) {
    const passedProjects = await prisma.project.findMany({
      where: { id: { in: passedProjectIds } },
      select: {
        id: true,
        submittedBy: { select: { passwordHash: true } },
        teamMembers: { select: { user: { select: { passwordHash: true } } } },
      },
    })
    for (const p of passedProjects) {
      // Check team members first, then submittedBy
      const users = p.teamMembers.length > 0
        ? p.teamMembers.map((tm: any) => tm.user)
        : p.submittedBy ? [p.submittedBy] : []
      const anyHasPassword = users.some((u: any) => !!u.passwordHash)
      if (anyHasPassword) hasAccount++
      else needsInvite++
    }
  }

  return {
    roundId,
    roundName: round.name,
    roundType: round.roundType,
    isGracePeriodActive,
    gracePeriodEndsAt: round.gracePeriodEndsAt,
    isFinalized,
    finalizedAt: round.finalizedAt,
    stats,
    projects,
    categoryTargets: {
      startupTarget,
      conceptTarget,
      startupProposed,
      conceptProposed,
    },
    nextRound: nextRound ? { id: nextRound.id, name: nextRound.name } : null,
    winnerContext,
    accountStats: { needsInvite, hasAccount },
  }
}

// ─── confirmFinalization ────────────────────────────────────────────────────

export async function confirmFinalization(
  roundId: string,
  options: {
    targetRoundId?: string
    advancementMessage?: string
    rejectionMessage?: string
  },
  actorId: string,
  prisma: PrismaClient,
): Promise<ConfirmFinalizationResult> {
  // Validate: round is CLOSED, not already finalized, grace period expired
  const round = await prisma.round.findUniqueOrThrow({
    where: { id: roundId },
    include: {
      competition: {
        select: {
          id: true,
          name: true,
          rounds: {
            select: { id: true, name: true, sortOrder: true, roundType: true },
            orderBy: { sortOrder: 'asc' as const },
          },
        },
      },
      specialAward: { select: { id: true, name: true } },
    },
  })

  if (round.status !== 'ROUND_CLOSED') {
    throw new Error(`Round must be ROUND_CLOSED to finalize, got ${round.status}`)
  }

  if (round.finalizedAt) {
    throw new Error('Round is already finalized')
  }

  const now = new Date()
  if (round.gracePeriodEndsAt && new Date(round.gracePeriodEndsAt) > now) {
    throw new Error('Cannot finalize: grace period is still active')
  }

  // Determine target round for routing: the actual next round by sortOrder
  // (may be a MENTORING round — opt-in teams stay there, others auto-pass through).
  const rounds = round.competition.rounds
  const currentIdx = rounds.findIndex((r: { id: string }) => r.id === roundId)
  const targetRoundId = options.targetRoundId
    ?? (currentIdx >= 0 && currentIdx < rounds.length - 1
      ? rounds[currentIdx + 1].id
      : undefined)

  // Display name for emails / notifications: skip MENTORING rounds so the
  // copy reflects the shared destination (e.g. "Grand Finale") rather than
  // the opt-in mentoring step.
  const displayNextRound = findDisplayNextRound(rounds, roundId)
  const targetRoundName = displayNextRound?.name ?? 'Next Round'

  // Terminal round: no further round to advance to. Passing teams are
  // winners — of the special award if one is attached, otherwise of the
  // competition itself (where there may be multiple winners, e.g. top 3).
  const winnerLabel = !displayNextRound
    ? round.specialAward?.name ?? round.competition.name
    : null

  // Execute finalization in a transaction
  const result = await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
    const projectStates = await tx.projectRoundState.findMany({
      where: { roundId, proposedOutcome: { not: null } },
      include: {
        project: {
          select: {
            id: true,
            title: true,
            status: true,
          },
        },
      },
    })

    let advanced = 0
    let rejected = 0

    for (const prs of projectStates) {
      const proposed = prs.proposedOutcome as ProjectRoundStateValue

      // Skip if already in the proposed state
      if (prs.state === proposed) {
        if (proposed === 'PASSED') advanced++
        else if (proposed === 'REJECTED') rejected++
        continue
      }

      // Transition to proposed outcome
      if (proposed === 'PASSED' || proposed === 'REJECTED') {
        // Ensure we're in COMPLETED before transitioning to PASSED/REJECTED
        if (prs.state !== 'COMPLETED' && prs.state !== 'PASSED' && prs.state !== 'REJECTED') {
          // Force through intermediate states
          if (prs.state === 'PENDING') {
            await tx.projectRoundState.update({
              where: { id: prs.id },
              data: { state: 'IN_PROGRESS' },
            })
          }
          if (prs.state === 'PENDING' || prs.state === 'IN_PROGRESS') {
            await tx.projectRoundState.update({
              where: { id: prs.id },
              data: { state: 'COMPLETED' },
            })
          }
        }

        // Now transition to final state
        await tx.projectRoundState.update({
          where: { id: prs.id },
          data: {
            state: proposed,
            exitedAt: now,
          },
        })

        if (proposed === 'PASSED') {
          advanced++

          // Create ProjectRoundState in target round (if exists)
          if (targetRoundId) {
            await tx.projectRoundState.upsert({
              where: {
                projectId_roundId: {
                  projectId: prs.projectId,
                  roundId: targetRoundId,
                },
              },
              create: {
                projectId: prs.projectId,
                roundId: targetRoundId,
                state: 'PENDING',
                enteredAt: now,
              },
              update: {}, // skip if already exists
            })
          }

          // Update Project.status to ASSIGNED
          await tx.project.update({
            where: { id: prs.projectId },
            data: { status: 'ASSIGNED' },
          })

          // Create ProjectStatusHistory
          await tx.projectStatusHistory.create({
            data: {
              projectId: prs.projectId,
              status: 'ASSIGNED',
              changedBy: actorId,
            },
          })
        } else {
          rejected++
        }

        // Audit log per project
        await tx.decisionAuditLog.create({
          data: {
            eventType: 'finalization.project_outcome',
            entityType: 'ProjectRoundState',
            entityId: prs.id,
            actorId,
            detailsJson: {
              projectId: prs.projectId,
              roundId,
              previousState: prs.state,
              outcome: proposed,
              targetRoundId: proposed === 'PASSED' ? targetRoundId : null,
            } as Prisma.InputJsonValue,
            snapshotJson: {
              timestamp: now.toISOString(),
              emittedBy: 'round-finalization',
            },
          },
        })
      }
    }

    // Mark round as finalized
    await tx.round.update({
      where: { id: roundId },
      data: {
        finalizedAt: now,
        finalizedBy: actorId,
      },
    })

    // Finalization audit
    await tx.decisionAuditLog.create({
      data: {
        eventType: 'round.finalized',
        entityType: 'Round',
        entityId: roundId,
        actorId,
        detailsJson: {
          roundName: round.name,
          advanced,
          rejected,
          targetRoundId,
          hasCustomAdvancementMessage: !!options.advancementMessage,
          hasCustomRejectionMessage: !!options.rejectionMessage,
        } as Prisma.InputJsonValue,
        snapshotJson: {
          timestamp: now.toISOString(),
          emittedBy: 'round-finalization',
        },
      },
    })

    return { advanced, rejected }
  })

  // Send emails outside transaction (non-fatal)
  let emailsSent = 0
  let emailsFailed = 0

  try {
    // Get all projects that were finalized
    const finalizedStates = await prisma.projectRoundState.findMany({
      where: { roundId, state: { in: ['PASSED', 'REJECTED'] } },
      include: {
        project: {
          select: {
            id: true,
            title: true,
            submittedByEmail: true,
            submittedByUserId: true,
            submittedBy: { select: { id: true, email: true, name: true, passwordHash: true } },
            teamMembers: {
              select: { user: { select: { id: true, email: true, name: true, passwordHash: true } } },
            },
          },
        },
      },
    })

    // Pre-generate invite tokens for passwordless users on advancing projects
    const inviteTokenMap = new Map<string, string>() // userId → token
    const expiryMs = await getInviteExpiryMs(prisma)

    // Collect all passwordless users needing invite tokens, then batch update
    const tokenUpdates: Array<{ userId: string; token: string }> = []
    for (const prs of finalizedStates) {
      if (prs.state !== 'PASSED') continue
      const users = prs.project.teamMembers.length > 0
        ? prs.project.teamMembers.map((tm: any) => tm.user)
        : prs.project.submittedBy ? [prs.project.submittedBy] : []
      for (const user of users) {
        if (user && !user.passwordHash && !inviteTokenMap.has(user.id)) {
          const token = generateInviteToken()
          inviteTokenMap.set(user.id, token)
          tokenUpdates.push({ userId: user.id, token })
        }
      }
    }
    // Batch update all invite tokens concurrently
    if (tokenUpdates.length > 0) {
      const tokenExpiry = new Date(Date.now() + expiryMs)
      await Promise.all(
        tokenUpdates.map((t) =>
          prisma.user.update({
            where: { id: t.userId },
            data: {
              inviteToken: t.token,
              inviteTokenExpiresAt: tokenExpiry,
              status: 'INVITED',
            },
          }),
        ),
      )
    }

    const advancedUserIds = new Set<string>()
    const rejectedUserIds = new Set<string>()
    const notificationItems: NotificationItem[] = []

    for (const prs of finalizedStates) {
      type Recipient = { email: string; name: string | null; userId: string | null }
      const recipients: Recipient[] = []
      for (const tm of prs.project.teamMembers) {
        if (tm.user.email) {
          recipients.push({ email: tm.user.email, name: tm.user.name, userId: tm.user.id })
          if (prs.state === 'PASSED') advancedUserIds.add(tm.user.id)
          else rejectedUserIds.add(tm.user.id)
        }
      }
      if (recipients.length === 0 && prs.project.submittedBy?.email) {
        recipients.push({
          email: prs.project.submittedBy.email,
          name: prs.project.submittedBy.name,
          userId: prs.project.submittedBy.id,
        })
        if (prs.state === 'PASSED') advancedUserIds.add(prs.project.submittedBy.id)
        else rejectedUserIds.add(prs.project.submittedBy.id)
      } else if (recipients.length === 0 && prs.project.submittedByEmail) {
        recipients.push({ email: prs.project.submittedByEmail, name: null, userId: null })
      }

      for (const recipient of recipients) {
        if (prs.state === 'PASSED') {
          const token = recipient.userId ? inviteTokenMap.get(recipient.userId) : undefined
          const accountUrl = token ? `/accept-invite?token=${token}` : undefined

          if (winnerLabel) {
            notificationItems.push({
              email: recipient.email,
              name: recipient.name || '',
              type: 'AWARD_WINNER_NOTIFICATION',
              context: {
                title: 'Your project has won!',
                message: '',
                linkUrl: accountUrl || '/applicant',
                metadata: {
                  projectName: prs.project.title,
                  awardName: winnerLabel,
                  customMessage: options.advancementMessage || undefined,
                  accountUrl,
                },
              },
              projectId: prs.projectId,
              userId: recipient.userId || undefined,
              roundId: round.id,
            })
          } else {
            notificationItems.push({
              email: recipient.email,
              name: recipient.name || '',
              type: 'ADVANCEMENT_NOTIFICATION',
              context: {
                title: 'Your project has advanced!',
                message: '',
                linkUrl: accountUrl || '/applicant',
                metadata: {
                  projectName: prs.project.title,
                  fromRoundName: round.name,
                  toRoundName: targetRoundName,
                  customMessage: options.advancementMessage || undefined,
                  accountUrl,
                },
              },
              projectId: prs.projectId,
              userId: recipient.userId || undefined,
              roundId: round.id,
            })
          }
        } else {
          notificationItems.push({
            email: recipient.email,
            name: recipient.name || '',
            type: 'REJECTION_NOTIFICATION',
            context: {
              title: `Update on your application: "${prs.project.title}"`,
              message: '',
              metadata: {
                projectName: prs.project.title,
                roundName: round.name,
                customMessage: options.rejectionMessage || undefined,
              },
            },
            projectId: prs.projectId,
            userId: recipient.userId || undefined,
            roundId: round.id,
          })
        }
      }
    }

    const batchResult = await sendBatchNotifications(notificationItems)
    emailsSent = batchResult.sent
    emailsFailed = batchResult.failed

    // Create in-app notifications
    if (advancedUserIds.size > 0) {
      createBulkNotifications({
        userIds: [...advancedUserIds],
        type: winnerLabel ? 'project_won' : 'project_advanced',
        title: winnerLabel ? 'Your project has won!' : 'Your project has advanced!',
        message: winnerLabel
          ? `Your project has been selected as a winner of "${winnerLabel}". Our team will reach out about next steps.`
          : `Your project has advanced from "${round.name}" to "${targetRoundName}".`,
        linkUrl: '/applicant',
        linkLabel: 'View Dashboard',
        icon: 'Trophy',
        priority: 'high',
      }).catch((err) => {
        console.error('[Finalization] createBulkNotifications (advanced) failed:', err)
      })
    }

    if (rejectedUserIds.size > 0) {
      createBulkNotifications({
        userIds: [...rejectedUserIds],
        type: 'project_rejected',
        title: 'Competition Update',
        message: `Your project did not advance past "${round.name}".`,
        linkUrl: '/applicant',
        linkLabel: 'View Dashboard',
        icon: 'Info',
        priority: 'normal',
      }).catch((err) => {
        console.error('[Finalization] createBulkNotifications (rejected) failed:', err)
      })
    }
  } catch (emailError) {
    console.error('[Finalization] Email batch failed (non-fatal):', emailError)
  }

  // External audit log
  await logAudit({
    userId: actorId,
    action: 'ROUND_FINALIZED',
    entityType: 'Round',
    entityId: roundId,
    detailsJson: {
      roundName: round.name,
      advanced: result.advanced,
      rejected: result.rejected,
      emailsSent,
      emailsFailed,
    },
  })

  return {
    advanced: result.advanced,
    rejected: result.rejected,
    emailsSent,
    emailsFailed,
  }
}
