import { z } from 'zod'
import { router, adminProcedure } from '../trpc'
import type { RoundType, RoundStatus, ProjectRoundStateValue } from '@prisma/client'
import { generateInviteToken, getInviteExpiryMs } from '../utils/invite'
import { sendBatchNotifications } from '../services/notification-sender'
import type { NotificationItem } from '../services/notification-sender'
import { getBaseUrl } from '@/lib/email'

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

type ProjectStateCounts = {
  PENDING: number
  IN_PROGRESS: number
  PASSED: number
  REJECTED: number
  COMPLETED: number
  WITHDRAWN: number
  total: number
}

export type PipelineRound = {
  id: string
  name: string
  slug: string
  roundType: RoundType
  status: RoundStatus
  sortOrder: number
  windowOpenAt: Date | null
  windowCloseAt: Date | null
  projectStates: ProjectStateCounts
  assignmentCount: number
  evalSubmitted: number
  evalDraft: number
  evalTotal: number
  filteringPassed: number
  filteringRejected: number
  filteringFlagged: number
  filteringTotal: number
  liveSessionStatus: string | null
  deliberationCount: number
}

export type DashboardAction = {
  id: string
  severity: 'critical' | 'warning' | 'info'
  title: string
  description: string
  href: string
  roundId?: string
  roundType?: RoundType
  count?: number
}

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

function emptyStateCounts(): ProjectStateCounts {
  return { PENDING: 0, IN_PROGRESS: 0, PASSED: 0, REJECTED: 0, COMPLETED: 0, WITHDRAWN: 0, total: 0 }
}

function daysUntil(date: Date): number {
  return Math.ceil((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
}

function formatRoundType(rt: RoundType): string {
  return rt.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase())
}

// ─── Router ──────────────────────────────────────────────────────────────────

export const dashboardRouter = router({
  /**
   * Get all dashboard stats in a single query batch.
   * Returns pipeline rounds, smart actions, and supporting data.
   */
  getStats: adminProcedure
    .input(z.object({ editionId: z.string() }))
    .query(async ({ ctx, input }) => {
      const { editionId } = input

      const edition = await ctx.prisma.program.findUnique({
        where: { id: editionId },
        select: { name: true, year: true },
      })

      if (!edition) return null

      const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)

      // ── All queries in parallel ──────────────────────────────────────

      const [
        // Pipeline rounds (all, ordered by sortOrder)
        allRounds,
        // Per-round project state breakdown
        stateBreakdown,
        // Per-round eval data (assignments with eval status)
        roundEvalData,
        // Per-round filtering results
        filteringStats,
        // Live session statuses
        liveSessions,
        // Deliberation session counts
        deliberationCounts,
        // Summary counts
        projectCount,
        newProjectsThisWeek,
        totalJurors,
        activeJurors,
        evaluationStats,
        totalAssignments,
        // Lists
        latestProjects,
        categoryBreakdown,
        oceanIssueBreakdown,
        recentActivity,
        // Recently active
        recentlyActiveEvals,
        // Action signals
        pendingCOIs,
      ] = await Promise.all([
        // 1. All pipeline rounds
        ctx.prisma.round.findMany({
          where: { competition: { programId: editionId } },
          orderBy: { sortOrder: 'asc' },
          select: {
            id: true,
            name: true,
            slug: true,
            roundType: true,
            status: true,
            sortOrder: true,
            windowOpenAt: true,
            windowCloseAt: true,
            _count: {
              select: {
                projectRoundStates: true,
                assignments: true,
              },
            },
          },
        }),

        // 2. Per-round project state counts
        ctx.prisma.projectRoundState.groupBy({
          by: ['roundId', 'state'],
          where: { round: { competition: { programId: editionId } } },
          _count: true,
        }),

        // 3. Assignments with eval status (for per-round eval aggregation)
        ctx.prisma.assignment.findMany({
          where: { round: { competition: { programId: editionId } } },
          select: {
            roundId: true,
            evaluation: { select: { status: true } },
          },
        }),

        // 4. Filtering results per round
        ctx.prisma.filteringResult.groupBy({
          by: ['roundId', 'outcome'],
          where: { round: { competition: { programId: editionId } } },
          _count: true,
        }),

        // 5. Live session statuses
        ctx.prisma.liveVotingSession.findMany({
          where: { round: { competition: { programId: editionId } } },
          select: { roundId: true, status: true },
        }),

        // 6. Deliberation session counts
        ctx.prisma.deliberationSession.groupBy({
          by: ['roundId'],
          where: { competition: { programId: editionId } },
          _count: true,
        }),

        // 7. Project count
        ctx.prisma.project.count({
          where: { programId: editionId },
        }),

        // 8. New projects this week
        ctx.prisma.project.count({
          where: { programId: editionId, createdAt: { gte: sevenDaysAgo } },
        }),

        // 9. Total jurors
        ctx.prisma.user.count({
          where: {
            roles: { has: 'JURY_MEMBER' },
            status: { in: ['ACTIVE', 'INVITED', 'NONE'] },
            assignments: { some: { round: { competition: { programId: editionId } } } },
          },
        }),

        // 10. Active jurors
        ctx.prisma.user.count({
          where: {
            roles: { has: 'JURY_MEMBER' },
            status: 'ACTIVE',
            assignments: { some: { round: { competition: { programId: editionId } } } },
          },
        }),

        // 11. Global evaluation stats
        ctx.prisma.evaluation.groupBy({
          by: ['status'],
          where: { assignment: { round: { competition: { programId: editionId } } } },
          _count: true,
        }),

        // 12. Total assignments
        ctx.prisma.assignment.count({
          where: { round: { competition: { programId: editionId } } },
        }),

        // 13. Latest projects
        ctx.prisma.project.findMany({
          where: { programId: editionId },
          orderBy: { createdAt: 'desc' },
          take: 8,
          select: {
            id: true,
            title: true,
            teamName: true,
            country: true,
            competitionCategory: true,
            oceanIssue: true,
            logoKey: true,
            createdAt: true,
            submittedAt: true,
            status: true,
          },
        }),

        // 14. Category breakdown
        ctx.prisma.project.groupBy({
          by: ['competitionCategory'],
          where: { programId: editionId },
          _count: true,
        }),

        // 15. Ocean issue breakdown
        ctx.prisma.project.groupBy({
          by: ['oceanIssue'],
          where: { programId: editionId },
          _count: true,
        }),

        // 16. Recent activity
        ctx.prisma.auditLog.findMany({
          where: { timestamp: { gte: sevenDaysAgo } },
          orderBy: { timestamp: 'desc' },
          take: 8,
          select: {
            id: true,
            action: true,
            entityType: true,
            timestamp: true,
            user: { select: { name: true } },
          },
        }),

        // 17. Recently active projects (with recent evaluations)
        ctx.prisma.evaluation.findMany({
          where: {
            status: 'SUBMITTED',
            assignment: {
              round: { competition: { programId: editionId } },
            },
          },
          orderBy: { submittedAt: 'desc' },
          take: 8,
          select: {
            id: true,
            globalScore: true,
            submittedAt: true,
            assignment: {
              select: {
                user: { select: { name: true } },
                project: {
                  select: {
                    id: true,
                    title: true,
                    teamName: true,
                    country: true,
                    competitionCategory: true,
                    oceanIssue: true,
                    logoKey: true,
                    createdAt: true,
                    submittedAt: true,
                    status: true,
                  },
                },
              },
            },
          },
        }),

        // 18. Pending COIs
        ctx.prisma.conflictOfInterest.count({
          where: {
            hasConflict: true,
            reviewedAt: null,
            assignment: { round: { competition: { programId: editionId } } },
          },
        }),
      ])

      // ── Assemble pipeline rounds ────────────────────────────────────

      // Build state counts map: roundId -> ProjectStateCounts
      const stateMap = new Map<string, ProjectStateCounts>()
      for (const row of stateBreakdown) {
        if (!stateMap.has(row.roundId)) stateMap.set(row.roundId, emptyStateCounts())
        const counts = stateMap.get(row.roundId)!
        const state = row.state as ProjectRoundStateValue
        if (state in counts) {
          counts[state as keyof Omit<ProjectStateCounts, 'total'>] = row._count
          counts.total += row._count
        }
      }

      // Build eval map: roundId -> { submitted, draft, total }
      const evalMap = new Map<string, { submitted: number; draft: number; total: number }>()
      for (const a of roundEvalData) {
        if (!evalMap.has(a.roundId)) evalMap.set(a.roundId, { submitted: 0, draft: 0, total: 0 })
        const entry = evalMap.get(a.roundId)!
        entry.total++
        if (a.evaluation?.status === 'SUBMITTED') entry.submitted++
        else if (a.evaluation?.status === 'DRAFT') entry.draft++
      }

      // Build filtering map: roundId -> { passed, rejected, flagged, total }
      const filterMap = new Map<string, { passed: number; rejected: number; flagged: number; total: number }>()
      for (const row of filteringStats) {
        if (!filterMap.has(row.roundId)) filterMap.set(row.roundId, { passed: 0, rejected: 0, flagged: 0, total: 0 })
        const entry = filterMap.get(row.roundId)!
        entry.total += row._count
        if (row.outcome === 'PASSED') entry.passed = row._count
        else if (row.outcome === 'FILTERED_OUT') entry.rejected = row._count
        else if (row.outcome === 'FLAGGED') entry.flagged = row._count
      }

      // Build live session map: roundId -> status
      const liveMap = new Map<string, string>()
      for (const s of liveSessions) {
        if (s.roundId) liveMap.set(s.roundId, s.status)
      }

      // Build deliberation map: roundId -> count
      const delibMap = new Map<string, number>()
      for (const row of deliberationCounts) {
        delibMap.set(row.roundId, row._count)
      }

      // Assemble pipeline rounds
      const pipelineRounds: PipelineRound[] = allRounds.map((round) => {
        const states = stateMap.get(round.id) ?? emptyStateCounts()
        const evals = evalMap.get(round.id) ?? { submitted: 0, draft: 0, total: 0 }
        const filters = filterMap.get(round.id) ?? { passed: 0, rejected: 0, flagged: 0, total: 0 }
        return {
          id: round.id,
          name: round.name,
          slug: round.slug,
          roundType: round.roundType,
          status: round.status,
          sortOrder: round.sortOrder,
          windowOpenAt: round.windowOpenAt,
          windowCloseAt: round.windowCloseAt,
          projectStates: states,
          assignmentCount: round._count.assignments,
          evalSubmitted: evals.submitted,
          evalDraft: evals.draft,
          evalTotal: evals.total,
          filteringPassed: filters.passed,
          filteringRejected: filters.rejected,
          filteringFlagged: filters.flagged,
          filteringTotal: filters.total,
          liveSessionStatus: liveMap.get(round.id) ?? null,
          deliberationCount: delibMap.get(round.id) ?? 0,
        }
      })

      // ── Determine active round ──────────────────────────────────────

      const activeRound = pipelineRounds.find((r) => r.status === 'ROUND_ACTIVE') ?? null
      const activeRoundId = activeRound?.id ?? null

      // ── Compute smart actions ───────────────────────────────────────

      const nextActions: DashboardAction[] = []
      const activeRounds = pipelineRounds.filter((r) => r.status === 'ROUND_ACTIVE')
      const lastActiveSortOrder = Math.max(...activeRounds.map((r) => r.sortOrder), -1)

      // 1. Next draft round (only the first one after the last active)
      const nextDraft = pipelineRounds.find(
        (r) => r.status === 'ROUND_DRAFT' && r.sortOrder > lastActiveSortOrder
      )
      if (nextDraft) {
        nextActions.push({
          id: `draft-${nextDraft.id}`,
          severity: 'info',
          title: `Configure "${nextDraft.name}"`,
          description: `Next round (${formatRoundType(nextDraft.roundType)}) is in draft`,
          href: `/admin/rounds/${nextDraft.id}`,
          roundId: nextDraft.id,
          roundType: nextDraft.roundType,
        })
      }

      // 2. Per-active-round actions
      for (const round of activeRounds) {
        // Evaluation rounds: flag unassigned projects
        if (round.roundType === 'EVALUATION' && round.projectStates.total > 0 && round.assignmentCount === 0) {
          nextActions.push({
            id: `unassigned-${round.id}`,
            severity: 'warning',
            title: `${round.projectStates.total} unassigned projects`,
            description: `"${round.name}" has projects without jury assignments`,
            href: `/admin/rounds/${round.id}`,
            roundId: round.id,
            roundType: round.roundType,
            count: round.projectStates.total,
          })
        }

        // Filtering rounds: flag if filtering not started
        if (round.roundType === 'FILTERING' && round.filteringTotal === 0 && round.projectStates.total > 0) {
          nextActions.push({
            id: `filtering-${round.id}`,
            severity: 'warning',
            title: 'Filtering not started',
            description: `"${round.name}" has ${round.projectStates.total} projects awaiting filtering`,
            href: `/admin/rounds/${round.id}`,
            roundId: round.id,
            roundType: round.roundType,
          })
        }

        // Deadline warnings
        if (round.windowCloseAt) {
          const days = daysUntil(round.windowCloseAt)
          if (days > 0 && days <= 3) {
            nextActions.push({
              id: `deadline-${round.id}`,
              severity: 'critical',
              title: `${days}d until "${round.name}" closes`,
              description: `Window closes ${round.windowCloseAt.toLocaleDateString()}`,
              href: `/admin/rounds/${round.id}`,
              roundId: round.id,
              roundType: round.roundType,
            })
          } else if (days > 3 && days <= 7) {
            nextActions.push({
              id: `deadline-${round.id}`,
              severity: 'warning',
              title: `${days}d until "${round.name}" closes`,
              description: `Window closes ${round.windowCloseAt.toLocaleDateString()}`,
              href: `/admin/rounds/${round.id}`,
              roundId: round.id,
              roundType: round.roundType,
            })
          }
        }
      }

      // 3. Pending COIs
      if (pendingCOIs > 0) {
        nextActions.push({
          id: 'pending-cois',
          severity: 'warning',
          title: `${pendingCOIs} COI declarations pending`,
          description: 'Jury members have declared conflicts that need admin review',
          href: '/admin/rounds',
          count: pendingCOIs,
        })
      }

      // Sort by severity
      const severityOrder = { critical: 0, warning: 1, info: 2 }
      nextActions.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity])

      // ── Return ──────────────────────────────────────────────────────

      // Deduplicate recently active projects (same project may have multiple evals)
      const seenProjectIds = new Set<string>()
      const recentlyActiveProjects = recentlyActiveEvals
        .filter((e) => {
          const pid = e.assignment.project.id
          if (seenProjectIds.has(pid)) return false
          seenProjectIds.add(pid)
          return true
        })
        .map((e) => ({
          ...e.assignment.project,
          latestEvaluator: e.assignment.user.name,
          latestScore: e.globalScore,
          evaluatedAt: e.submittedAt,
        }))

      return {
        edition,
        // Pipeline
        pipelineRounds,
        activeRoundId,
        // Smart actions
        nextActions,
        // Summary counts
        projectCount,
        newProjectsThisWeek,
        totalJurors,
        activeJurors,
        evaluationStats,
        totalAssignments,
        pendingCOIs,
        // Lists
        latestProjects,
        recentlyActiveProjects,
        categoryBreakdown,
        oceanIssueBreakdown,
        recentActivity,
      }
    }),

  getRecentEvaluations: adminProcedure
    .input(z.object({ editionId: z.string(), limit: z.number().int().min(1).max(50).optional() }))
    .query(async ({ ctx, input }) => {
      const take = input.limit ?? 10

      const evaluations = await ctx.prisma.evaluation.findMany({
        where: {
          status: 'SUBMITTED',
          assignment: {
            round: { competition: { programId: input.editionId } },
          },
        },
        orderBy: { submittedAt: 'desc' },
        take,
        select: {
          id: true,
          globalScore: true,
          binaryDecision: true,
          submittedAt: true,
          feedbackText: true,
          assignment: {
            select: {
              project: { select: { id: true, title: true } },
              round: { select: { id: true, name: true } },
              user: { select: { id: true, name: true, email: true } },
            },
          },
        },
      })

      return evaluations
    }),

  getRecentActivity: adminProcedure
    .input(z.object({ limit: z.number().int().min(1).max(20).optional() }))
    .query(async ({ ctx, input }) => {
      const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
      return ctx.prisma.auditLog.findMany({
        where: { timestamp: { gte: sevenDaysAgo } },
        orderBy: { timestamp: 'desc' },
        take: input.limit ?? 8,
        select: {
          id: true,
          action: true,
          entityType: true,
          timestamp: true,
          user: { select: { name: true } },
        },
      })
    }),

  /**
   * Get semi-finalist stats: counts by category, by award, and unactivated projects.
   * "Semi-finalist" = project with PASSED ProjectRoundState.
   * "Account set up" = at least 1 team member has passwordHash.
   */
  getSemiFinalistStats: adminProcedure
    .input(z.object({ editionId: z.string() }))
    .query(async ({ ctx, input }) => {
      const { editionId } = input

      // Find projects whose LATEST terminal state (PASSED/REJECTED/WITHDRAWN) is PASSED.
      // A project that passed round 1 but was rejected in round 2 is NOT a semi-finalist.
      const terminalStates = await ctx.prisma.projectRoundState.findMany({
        where: {
          state: { in: ['PASSED', 'REJECTED', 'WITHDRAWN'] },
          round: { competition: { programId: editionId } },
        },
        select: {
          projectId: true,
          state: true,
          round: { select: { id: true, name: true, sortOrder: true } },
          project: {
            select: {
              id: true,
              title: true,
              competitionCategory: true,
              teamMembers: {
                select: {
                  user: {
                    select: {
                      id: true,
                      email: true,
                      name: true,
                      status: true,
                      passwordHash: true,
                    },
                  },
                },
              },
            },
          },
        },
      })

      // For each project, keep only the terminal state from the highest-sortOrder round
      const projectMap = new Map<string, (typeof terminalStates)[0]>()
      for (const ts of terminalStates) {
        const existing = projectMap.get(ts.projectId)
        if (!existing || ts.round.sortOrder > existing.round.sortOrder) {
          projectMap.set(ts.projectId, ts)
        }
      }

      // Only include projects whose latest terminal state is PASSED
      const uniqueProjects = Array.from(projectMap.values()).filter(ps => ps.state === 'PASSED')

      // Group by category
      const catMap = new Map<string, { total: number; accountsSet: number; accountsNotSet: number }>()
      for (const ps of uniqueProjects) {
        const cat = ps.project.competitionCategory || 'UNKNOWN'
        if (!catMap.has(cat)) catMap.set(cat, { total: 0, accountsSet: 0, accountsNotSet: 0 })
        const entry = catMap.get(cat)!
        entry.total++
        const hasActivated = ps.project.teamMembers.some((tm) => tm.user.passwordHash !== null || tm.user.status === 'ACTIVE')
        if (hasActivated) entry.accountsSet++
        else entry.accountsNotSet++
      }

      const byCategory = Array.from(catMap.entries()).map(([category, counts]) => ({
        category,
        ...counts,
      }))

      // Get award eligibility for PASSED projects
      const passedProjectIds = uniqueProjects.map((p) => p.projectId)
      const awardEligibility = passedProjectIds.length > 0
        ? await ctx.prisma.awardEligibility.findMany({
            where: {
              projectId: { in: passedProjectIds },
              eligible: true,
            },
            select: {
              projectId: true,
              award: { select: { id: true, name: true } },
            },
          })
        : []

      // Group by award
      const awardMap = new Map<string, { awardName: string; projectIds: Set<string> }>()
      for (const ae of awardEligibility) {
        if (!awardMap.has(ae.award.id)) {
          awardMap.set(ae.award.id, { awardName: ae.award.name, projectIds: new Set() })
        }
        awardMap.get(ae.award.id)!.projectIds.add(ae.projectId)
      }

      const byAward = Array.from(awardMap.entries()).map(([awardId, { awardName, projectIds }]) => {
        let accountsSet = 0
        let accountsNotSet = 0
        for (const pid of projectIds) {
          const ps = projectMap.get(pid)
          if (ps) {
            const hasActivated = ps.project.teamMembers.some((tm) => tm.user.passwordHash !== null || tm.user.status === 'ACTIVE')
            if (hasActivated) accountsSet++
            else accountsNotSet++
          }
        }
        return { awardId, awardName, total: projectIds.size, accountsSet, accountsNotSet }
      })

      // Unactivated projects: no team member has set up their account
      const unactivatedProjects = uniqueProjects
        .filter((ps) => !ps.project.teamMembers.some((tm) => tm.user.passwordHash !== null || tm.user.status === 'ACTIVE'))
        .map((ps) => ({
          projectId: ps.projectId,
          projectTitle: ps.project.title,
          category: ps.project.competitionCategory,
          teamEmails: ps.project.teamMembers
            .filter((tm) => tm.user.passwordHash === null && tm.user.status !== 'ACTIVE')
            .map((tm) => tm.user.email),
          roundName: ps.round.name,
        }))

      return { byCategory, byAward, unactivatedProjects }
    }),

  /**
   * Round User Tracker: for a given round, find projects that PASSED that round
   * and show team member account activation stats grouped by category.
   * If no roundId is provided, uses the latest active or most recently closed round.
   */
  getRoundUserStats: adminProcedure
    .input(z.object({ editionId: z.string(), roundId: z.string().optional() }))
    .query(async ({ ctx, input }) => {
      const { editionId, roundId } = input

      // Get all rounds for this edition to power the round selector
      const allRounds = await ctx.prisma.round.findMany({
        where: { competition: { programId: editionId } },
        select: { id: true, name: true, sortOrder: true, status: true, roundType: true },
        orderBy: { sortOrder: 'asc' },
      })

      // Determine which round to show — pick the latest round that has PASSED projects
      let selectedRoundId = roundId
      if (!selectedRoundId) {
        // Find rounds that actually have PASSED projects (most useful default)
        const roundsWithPassed = await ctx.prisma.projectRoundState.groupBy({
          by: ['roundId'],
          where: {
            state: 'PASSED',
            roundId: { in: allRounds.map(r => r.id) },
          },
        })
        const passedRoundIds = new Set(roundsWithPassed.map(r => r.roundId))
        // Pick the latest round (highest sortOrder) that has PASSED projects
        const roundsWithPassedSorted = allRounds
          .filter(r => passedRoundIds.has(r.id))
          .sort((a, b) => b.sortOrder - a.sortOrder)
        if (roundsWithPassedSorted.length > 0) {
          selectedRoundId = roundsWithPassedSorted[0].id
        } else {
          // Fallback: active round, then most recently closed
          const activeRound = allRounds.find(r => r.status === 'ROUND_ACTIVE')
          if (activeRound) {
            selectedRoundId = activeRound.id
          } else {
            const closedRounds = allRounds.filter(r => r.status === 'ROUND_CLOSED' || r.status === 'ROUND_ARCHIVED')
            if (closedRounds.length > 0) {
              selectedRoundId = closedRounds[closedRounds.length - 1].id
            }
          }
        }
      }

      if (!selectedRoundId) {
        return { rounds: allRounds, selectedRoundId: null, byCategory: [], unactivatedProjects: [] }
      }

      const selectedRound = allRounds.find(r => r.id === selectedRoundId)

      // Find projects that PASSED this specific round
      const passedStates = await ctx.prisma.projectRoundState.findMany({
        where: {
          roundId: selectedRoundId,
          state: 'PASSED',
        },
        select: {
          projectId: true,
          project: {
            select: {
              id: true,
              title: true,
              competitionCategory: true,
              teamMembers: {
                select: {
                  user: {
                    select: {
                      id: true,
                      email: true,
                      name: true,
                      status: true,
                      passwordHash: true,
                    },
                  },
                },
              },
            },
          },
        },
      })

      // Group by category
      const catMap = new Map<string, { total: number; accountsSet: number; accountsNotSet: number }>()
      for (const ps of passedStates) {
        const cat = ps.project.competitionCategory || 'UNKNOWN'
        if (!catMap.has(cat)) catMap.set(cat, { total: 0, accountsSet: 0, accountsNotSet: 0 })
        const entry = catMap.get(cat)!
        entry.total++
        const hasActivated = ps.project.teamMembers.some(
          (tm) => tm.user.passwordHash !== null || tm.user.status === 'ACTIVE'
        )
        if (hasActivated) entry.accountsSet++
        else entry.accountsNotSet++
      }

      const byCategory = Array.from(catMap.entries()).map(([category, counts]) => ({
        category,
        ...counts,
      }))

      // Unactivated projects
      const unactivatedProjects = passedStates
        .filter((ps) => !ps.project.teamMembers.some(
          (tm) => tm.user.passwordHash !== null || tm.user.status === 'ACTIVE'
        ))
        .map((ps) => ({
          projectId: ps.projectId,
          projectTitle: ps.project.title,
          category: ps.project.competitionCategory,
          teamEmails: ps.project.teamMembers
            .filter((tm) => tm.user.passwordHash === null && tm.user.status !== 'ACTIVE')
            .map((tm) => tm.user.email),
          roundName: selectedRound?.name ?? '',
        }))

      return {
        rounds: allRounds,
        selectedRoundId,
        byCategory,
        unactivatedProjects,
      }
    }),

  /**
   * Get detailed semi-finalist list for the "See All" page.
   * Returns every project whose latest terminal state is PASSED, with team and round info.
   */
  getSemiFinalistDetail: adminProcedure
    .input(z.object({ editionId: z.string() }))
    .query(async ({ ctx, input }) => {
      const { editionId } = input

      // Fetch all terminal states for projects in this edition
      const terminalStates = await ctx.prisma.projectRoundState.findMany({
        where: {
          state: { in: ['PASSED', 'REJECTED', 'WITHDRAWN'] },
          round: { competition: { programId: editionId } },
        },
        select: {
          projectId: true,
          state: true,
          round: { select: { id: true, name: true, sortOrder: true, roundType: true } },
          project: {
            select: {
              id: true,
              title: true,
              teamName: true,
              competitionCategory: true,
              country: true,
              teamMembers: {
                select: {
                  role: true,
                  user: {
                    select: {
                      id: true,
                      email: true,
                      name: true,
                      status: true,
                      passwordHash: true,
                      lastLoginAt: true,
                    },
                  },
                },
              },
            },
          },
        },
      })

      // Keep the latest terminal state per project
      const projectMap = new Map<string, (typeof terminalStates)[0]>()
      for (const ts of terminalStates) {
        const existing = projectMap.get(ts.projectId)
        if (!existing || ts.round.sortOrder > existing.round.sortOrder) {
          projectMap.set(ts.projectId, ts)
        }
      }

      // Only include PASSED projects
      const semiFinalists = Array.from(projectMap.values())
        .filter(ps => ps.state === 'PASSED')
        .map(ps => ({
          projectId: ps.projectId,
          title: ps.project.title,
          teamName: ps.project.teamName,
          category: ps.project.competitionCategory,
          country: ps.project.country,
          currentRound: ps.round.name,
          currentRoundType: ps.round.roundType,
          teamMembers: ps.project.teamMembers.map(tm => ({
            name: tm.user.name,
            email: tm.user.email,
            role: tm.role,
            accountStatus: tm.user.passwordHash !== null
              ? 'active' as const
              : tm.user.status === 'ACTIVE'
                ? 'active' as const
                : tm.user.status === 'INVITED'
                  ? 'invited' as const
                  : 'none' as const,
            lastLogin: tm.user.lastLoginAt,
          })),
          allActivated: ps.project.teamMembers.every(
            tm => tm.user.passwordHash !== null || tm.user.status === 'ACTIVE'
          ),
        }))
        .sort((a, b) => a.title.localeCompare(b.title))

      return semiFinalists
    }),

  /**
   * Send account setup reminder emails to semi-finalist team members
   * who haven't set their password yet.
   */
  sendAccountReminders: adminProcedure
    .input(z.object({
      projectIds: z.array(z.string()).optional(),
      category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).optional(),
      awardId: z.string().optional(),
      roundId: z.string().optional(),
      editionId: z.string(),
    }))
    .mutation(async ({ ctx, input }) => {
      const { editionId, projectIds, category, awardId, roundId } = input

      // Build filter for projects
      let targetProjectIds: string[] = projectIds ?? []

      if (!projectIds?.length) {
        // Find PASSED projects matching filters
        const passedWhere: Record<string, unknown> = {
          state: 'PASSED' as const,
        }

        // If roundId is provided, scope to that specific round; otherwise edition-wide
        if (roundId) {
          passedWhere.roundId = roundId
        } else {
          passedWhere.round = { competition: { programId: editionId } }
        }

        if (category) {
          passedWhere.project = { competitionCategory: category }
        }

        const passedStates = await ctx.prisma.projectRoundState.findMany({
          where: passedWhere,
          select: { projectId: true },
          distinct: ['projectId'],
        })

        targetProjectIds = passedStates.map((ps) => ps.projectId)

        // If filtering by award, intersect with award eligibility
        if (awardId) {
          const eligible = await ctx.prisma.awardEligibility.findMany({
            where: {
              projectId: { in: targetProjectIds },
              awardId,
              eligible: true,
            },
            select: { projectId: true },
          })
          const eligibleSet = new Set(eligible.map((e) => e.projectId))
          targetProjectIds = targetProjectIds.filter((id) => eligibleSet.has(id))
        }
      }

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

      // Find team members without passwordHash on these projects
      const projects = await ctx.prisma.project.findMany({
        where: { id: { in: targetProjectIds } },
        select: {
          id: true,
          title: true,
          teamMembers: {
            select: {
              user: {
                select: {
                  id: true,
                  email: true,
                  name: true,
                  status: true,
                  passwordHash: true,
                },
              },
            },
          },
        },
      })

      // Skip projects that already had a recent ACCOUNT_REMINDER
      const recentReminders = await ctx.prisma.notificationLog.findMany({
        where: {
          type: 'ACCOUNT_REMINDER',
          status: 'SENT',
          createdAt: { gte: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000) }, // 3 days
        },
        select: { email: true },
      })
      const recentReminderEmails = new Set(recentReminders.map((r) => r.email).filter(Boolean))

      const expiryMs = await getInviteExpiryMs(ctx.prisma)
      const expiresAt = new Date(Date.now() + expiryMs)
      const baseUrl = getBaseUrl()
      const notifications: NotificationItem[] = []

      for (const project of projects) {
        const unactivated = project.teamMembers.filter(
          (tm) => tm.user.passwordHash === null && tm.user.status !== 'ACTIVE' && !recentReminderEmails.has(tm.user.email)
        )

        for (const tm of unactivated) {
          // Generate invite token and mark as INVITED
          const token = generateInviteToken()
          await ctx.prisma.user.update({
            where: { id: tm.user.id },
            data: { inviteToken: token, inviteTokenExpiresAt: expiresAt, status: 'INVITED' },
          })

          const accountUrl = `/accept-invite?token=${token}`

          notifications.push({
            email: tm.user.email,
            name: tm.user.name || '',
            type: 'ACCOUNT_REMINDER',
            context: {
              title: 'Set Up Your Account',
              message: `Your project "${project.title}" is a semi-finalist. Please set up your account.`,
              linkUrl: `${baseUrl}${accountUrl}`,
              metadata: {
                projectName: project.title,
                accountUrl,
              },
            },
            projectId: project.id,
            userId: tm.user.id,
          })
        }
      }

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

      const result = await sendBatchNotifications(notifications)
      return { sent: result.sent, failed: result.failed, total: notifications.length }
    }),
})
