import crypto from 'crypto'
import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@prisma/client'
import { router, protectedProcedure, adminProcedure } from '../trpc'
import { getUserAvatarUrl } from '../utils/avatar-url'
import {
  notifyProjectTeam,
  NotificationTypes,
} from '../services/in-app-notification'
import { normalizeCountryToCode } from '@/lib/countries'
import { logAudit } from '../utils/audit'
import { sendInvitationEmail } from '@/lib/email'

const INVITE_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
const STATUSES_WITH_TEAM_NOTIFICATIONS = ['SEMIFINALIST', 'FINALIST', 'REJECTED'] as const

// Valid project status transitions
const VALID_PROJECT_TRANSITIONS: Record<string, string[]> = {
  SUBMITTED: ['ELIGIBLE', 'REJECTED'],           // New submissions get screened
  ELIGIBLE: ['ASSIGNED', 'REJECTED'],             // Eligible projects get assigned to jurors
  ASSIGNED: ['SEMIFINALIST', 'FINALIST', 'REJECTED'], // After evaluation
  SEMIFINALIST: ['FINALIST', 'REJECTED'],         // Semi-finalists advance or get cut
  FINALIST: ['REJECTED'],                          // Finalists can only be rejected (rare)
  REJECTED: ['SUBMITTED'],                         // Rejected can be re-submitted (admin override)
}

export const projectRouter = router({
  /**
   * List projects with filtering and pagination
   * Admin sees all, jury sees only assigned projects
   */
  list: protectedProcedure
    .input(
      z.object({
        programId: z.string().optional(),
        roundId: z.string().optional(),
        status: z
          .enum([
            'SUBMITTED',
            'ELIGIBLE',
            'ASSIGNED',
            'SEMIFINALIST',
            'FINALIST',
            'REJECTED',
          ])
          .optional(),
        statuses: z.array(
          z.enum([
            'SUBMITTED',
            'ELIGIBLE',
            'ASSIGNED',
            'SEMIFINALIST',
            'FINALIST',
            'REJECTED',
          ])
        ).optional(),
        excludeInRoundId: z.string().optional(), // Exclude projects already in this round
        unassignedOnly: z.boolean().optional(), // Projects not in any round
        search: z.string().optional(),
        tags: z.array(z.string()).optional(),
        competitionCategory: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).optional(),
        oceanIssue: z.enum([
          'POLLUTION_REDUCTION', 'CLIMATE_MITIGATION', 'TECHNOLOGY_INNOVATION',
          'SUSTAINABLE_SHIPPING', 'BLUE_CARBON', 'HABITAT_RESTORATION',
          'COMMUNITY_CAPACITY', 'SUSTAINABLE_FISHING', 'CONSUMER_AWARENESS',
          'OCEAN_ACIDIFICATION', 'OTHER',
        ]).optional(),
        country: z.string().optional(),
        wantsMentorship: z.boolean().optional(),
        hasFiles: z.boolean().optional(),
        hasAssignments: z.boolean().optional(),
        page: z.number().int().min(1).default(1),
        perPage: z.number().int().min(1).max(200).default(20),
      })
    )
    .query(async ({ ctx, input }) => {
      const {
        programId, roundId, excludeInRoundId, status, statuses, unassignedOnly, search, tags,
        competitionCategory, oceanIssue, country,
        wantsMentorship, hasFiles, hasAssignments,
        page, perPage,
      } = input
      const skip = (page - 1) * perPage

      // Build where clause
      const where: Record<string, unknown> = {}

      // Filter by program
      if (programId) where.programId = programId

      // Filter by round (via RoundAssignment join)
      if (roundId) {
        where.roundAssignments = { some: { roundId } }
      }

      // Exclude projects already in a specific round
      if (excludeInRoundId) {
        where.roundAssignments = { none: { roundId: excludeInRoundId } }
      }

      // Filter by unassigned (not in any round)
      if (unassignedOnly) {
        where.roundAssignments = { none: {} }
      }

      // Status filter
      if (statuses?.length || status) {
        const statusValues = statuses?.length ? statuses : status ? [status] : []
        if (statusValues.length > 0) {
          where.status = { in: statusValues }
        }
      }

      if (tags && tags.length > 0) {
        where.tags = { hasSome: tags }
      }
      if (competitionCategory) where.competitionCategory = competitionCategory
      if (oceanIssue) where.oceanIssue = oceanIssue
      if (country) where.country = country
      if (wantsMentorship !== undefined) where.wantsMentorship = wantsMentorship
      if (hasFiles === true) where.files = { some: {} }
      if (hasFiles === false) where.files = { none: {} }
      if (hasAssignments === true) where.assignments = { some: {} }
      if (hasAssignments === false) where.assignments = { none: {} }

      if (search) {
        where.OR = [
          { title: { contains: search, mode: 'insensitive' } },
          { teamName: { contains: search, mode: 'insensitive' } },
          { description: { contains: search, mode: 'insensitive' } },
        ]
      }

      // Jury members can only see assigned projects
      if (ctx.user.role === 'JURY_MEMBER') {
        where.assignments = {
          ...((where.assignments as Record<string, unknown>) || {}),
          some: { userId: ctx.user.id },
        }
      }

      const [projects, total] = await Promise.all([
        ctx.prisma.project.findMany({
          where,
          skip,
          take: perPage,
          orderBy: { createdAt: 'desc' },
          include: {
            program: { select: { id: true, name: true, year: true } },
            _count: { select: { assignments: true, files: true } },
          },
        }),
        ctx.prisma.project.count({ where }),
      ])

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

  /**
   * List all project IDs matching filters (no pagination).
   * Used for "select all across pages" in bulk operations.
   */
  listAllIds: adminProcedure
    .input(
      z.object({
        programId: z.string().optional(),
        roundId: z.string().optional(),
        excludeInRoundId: z.string().optional(),
        unassignedOnly: z.boolean().optional(),
        search: z.string().optional(),
        statuses: z.array(
          z.enum([
            'SUBMITTED',
            'ELIGIBLE',
            'ASSIGNED',
            'SEMIFINALIST',
            'FINALIST',
            'REJECTED',
          ])
        ).optional(),
        tags: z.array(z.string()).optional(),
        competitionCategory: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).optional(),
        oceanIssue: z.enum([
          'POLLUTION_REDUCTION', 'CLIMATE_MITIGATION', 'TECHNOLOGY_INNOVATION',
          'SUSTAINABLE_SHIPPING', 'BLUE_CARBON', 'HABITAT_RESTORATION',
          'COMMUNITY_CAPACITY', 'SUSTAINABLE_FISHING', 'CONSUMER_AWARENESS',
          'OCEAN_ACIDIFICATION', 'OTHER',
        ]).optional(),
        country: z.string().optional(),
        wantsMentorship: z.boolean().optional(),
        hasFiles: z.boolean().optional(),
        hasAssignments: z.boolean().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const {
        programId, roundId, excludeInRoundId, unassignedOnly,
        search, statuses, tags,
        competitionCategory, oceanIssue, country,
        wantsMentorship, hasFiles, hasAssignments,
      } = input

      const where: Record<string, unknown> = {}

      if (programId) where.programId = programId
      if (roundId) {
        where.roundAssignments = { some: { roundId } }
      }
      if (excludeInRoundId) {
        where.roundAssignments = { none: { roundId: excludeInRoundId } }
      }
      if (unassignedOnly) {
        where.roundAssignments = { none: {} }
      }
      if (statuses?.length) where.status = { in: statuses }
      if (tags && tags.length > 0) where.tags = { hasSome: tags }
      if (competitionCategory) where.competitionCategory = competitionCategory
      if (oceanIssue) where.oceanIssue = oceanIssue
      if (country) where.country = country
      if (wantsMentorship !== undefined) where.wantsMentorship = wantsMentorship
      if (hasFiles === true) where.files = { some: {} }
      if (hasFiles === false) where.files = { none: {} }
      if (hasAssignments === true) where.assignments = { some: {} }
      if (hasAssignments === false) where.assignments = { none: {} }
      if (search) {
        where.OR = [
          { title: { contains: search, mode: 'insensitive' } },
          { teamName: { contains: search, mode: 'insensitive' } },
          { description: { contains: search, mode: 'insensitive' } },
        ]
      }

      const projects = await ctx.prisma.project.findMany({
        where,
        select: { id: true },
        orderBy: { createdAt: 'desc' },
      })

      return { ids: projects.map((p) => p.id) }
    }),

  /**
   * Preview project-team recipients before bulk status update notifications.
   * Used by admin UI confirmation dialog to verify notification audience.
   */
  previewStatusNotificationRecipients: adminProcedure
    .input(
      z.object({
        ids: z.array(z.string()).min(1).max(10000),
        status: z.enum([
          'SUBMITTED',
          'ELIGIBLE',
          'ASSIGNED',
          'SEMIFINALIST',
          'FINALIST',
          'REJECTED',
        ]),
      })
    )
    .query(async ({ ctx, input }) => {
      const statusTriggersNotification = STATUSES_WITH_TEAM_NOTIFICATIONS.includes(
        input.status as (typeof STATUSES_WITH_TEAM_NOTIFICATIONS)[number]
      )

      if (!statusTriggersNotification) {
        return {
          status: input.status,
          statusTriggersNotification,
          totalProjects: 0,
          projectsWithRecipients: 0,
          totalRecipients: 0,
          projects: [] as Array<{
            id: string
            title: string
            recipientCount: number
            recipientsPreview: string[]
            hasMoreRecipients: boolean
          }>,
        }
      }

      const projects = await ctx.prisma.project.findMany({
        where: { id: { in: input.ids } },
        select: {
          id: true,
          title: true,
          teamMembers: {
            select: {
              userId: true,
              user: {
                select: {
                  email: true,
                },
              },
            },
          },
        },
        orderBy: { title: 'asc' },
      })

      const MAX_PREVIEW_RECIPIENTS_PER_PROJECT = 8

      const mappedProjects = projects.map((project) => {
        const uniqueEmails = Array.from(
          new Set(
            project.teamMembers
              .map((member) => member.user?.email?.toLowerCase().trim() ?? '')
              .filter((email) => email.length > 0)
          )
        )

        return {
          id: project.id,
          title: project.title,
          recipientCount: uniqueEmails.length,
          recipientsPreview: uniqueEmails.slice(0, MAX_PREVIEW_RECIPIENTS_PER_PROJECT),
          hasMoreRecipients: uniqueEmails.length > MAX_PREVIEW_RECIPIENTS_PER_PROJECT,
        }
      })

      const projectsWithRecipients = mappedProjects.filter((p) => p.recipientCount > 0).length
      const totalRecipients = mappedProjects.reduce((sum, project) => sum + project.recipientCount, 0)

      return {
        status: input.status,
        statusTriggersNotification,
        totalProjects: mappedProjects.length,
        projectsWithRecipients,
        totalRecipients,
        projects: mappedProjects,
      }
    }),

  /**
   * Get filter options for the project list (distinct values)
   */
  getFilterOptions: protectedProcedure
    .query(async ({ ctx }) => {
      const [countries, categories, issues] = await Promise.all([
        ctx.prisma.project.findMany({
          where: { country: { not: null } },
          select: { country: true },
          distinct: ['country'],
          orderBy: { country: 'asc' },
        }),
        ctx.prisma.project.groupBy({
          by: ['competitionCategory'],
          where: { competitionCategory: { not: null } },
          _count: true,
        }),
        ctx.prisma.project.groupBy({
          by: ['oceanIssue'],
          where: { oceanIssue: { not: null } },
          _count: true,
        }),
      ])

      return {
        countries: countries.map((c) => c.country).filter(Boolean) as string[],
        categories: categories.map((c) => ({
          value: c.competitionCategory!,
          count: c._count,
        })),
        issues: issues.map((i) => ({
          value: i.oceanIssue!,
          count: i._count,
        })),
      }
    }),

  /**
   * Get a single project with details
   */
  get: protectedProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const project = await ctx.prisma.project.findUniqueOrThrow({
        where: { id: input.id },
        include: {
          files: true,
          teamMembers: {
            include: {
              user: {
                select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true },
              },
            },
            orderBy: { joinedAt: 'asc' },
          },
          mentorAssignment: {
            include: {
              mentor: {
                select: { id: true, name: true, email: true, expertiseTags: true, profileImageKey: true, profileImageProvider: true },
              },
            },
          },
        },
      })

      // Fetch project tags separately (table may not exist if migrations are pending)
      let projectTags: { id: string; projectId: string; tagId: string; confidence: number; tag: { id: string; name: string; category: string | null; color: string | null } }[] = []
      try {
        projectTags = await ctx.prisma.projectTag.findMany({
          where: { projectId: input.id },
          include: { tag: { select: { id: true, name: true, category: true, color: true } } },
          orderBy: { confidence: 'desc' },
        })
      } catch {
        // ProjectTag table may not exist yet
      }

      // Check access for jury members
      if (ctx.user.role === 'JURY_MEMBER') {
        const assignment = await ctx.prisma.assignment.findFirst({
          where: {
            projectId: input.id,
            userId: ctx.user.id,
          },
        })

        if (!assignment) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You are not assigned to this project',
          })
        }
      }

      // Attach avatar URLs to team members and mentor
      const teamMembersWithAvatars = await Promise.all(
        project.teamMembers.map(async (member) => ({
          ...member,
          user: {
            ...member.user,
            avatarUrl: await getUserAvatarUrl(member.user.profileImageKey, member.user.profileImageProvider),
          },
        }))
      )

      const mentorWithAvatar = project.mentorAssignment
        ? {
            ...project.mentorAssignment,
            mentor: {
              ...project.mentorAssignment.mentor,
              avatarUrl: await getUserAvatarUrl(
                project.mentorAssignment.mentor.profileImageKey,
                project.mentorAssignment.mentor.profileImageProvider
              ),
            },
          }
        : null

      return {
        ...project,
        projectTags,
        teamMembers: teamMembersWithAvatars,
        mentorAssignment: mentorWithAvatar,
      }
    }),

  /**
   * Create a single project (admin only)
   * Projects belong to a program.
   */
  create: adminProcedure
    .input(
      z.object({
        programId: z.string(),
        title: z.string().min(1).max(500),
        teamName: z.string().optional(),
        description: z.string().optional(),
        tags: z.array(z.string()).optional(),
        country: z.string().optional(),
        competitionCategory: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).optional(),
        oceanIssue: z.enum([
          'POLLUTION_REDUCTION', 'CLIMATE_MITIGATION', 'TECHNOLOGY_INNOVATION',
          'SUSTAINABLE_SHIPPING', 'BLUE_CARBON', 'HABITAT_RESTORATION',
          'COMMUNITY_CAPACITY', 'SUSTAINABLE_FISHING', 'CONSUMER_AWARENESS',
          'OCEAN_ACIDIFICATION', 'OTHER',
        ]).optional(),
        institution: z.string().optional(),
        contactPhone: z.string().optional(),
        contactEmail: z.string().email('Invalid email address').optional(),
        contactName: z.string().optional(),
        city: z.string().optional(),
        metadataJson: z.record(z.unknown()).optional(),
        teamMembers: z.array(z.object({
          name: z.string().min(1),
          email: z.string().email(),
          role: z.enum(['LEAD', 'MEMBER', 'ADVISOR']),
          title: z.string().optional(),
          phone: z.string().optional(),
          sendInvite: z.boolean().default(false),
        })).max(10).optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const {
        metadataJson,
        contactPhone, contactEmail, contactName, city,
        teamMembers: teamMembersInput,
        ...rest
      } = input

      const resolvedProgramId = input.programId

      // Build metadata from contact fields + any additional metadata
      const fullMetadata: Record<string, unknown> = { ...metadataJson }
      if (contactPhone) fullMetadata.contactPhone = contactPhone
      if (contactEmail) fullMetadata.contactEmail = contactEmail
      if (contactName) fullMetadata.contactName = contactName
      if (city) fullMetadata.city = city

      // Normalize country to ISO code if provided
      const normalizedCountry = input.country
        ? normalizeCountryToCode(input.country)
        : undefined

      const { project, membersToInvite } = await ctx.prisma.$transaction(async (tx) => {
        const created = await tx.project.create({
          data: {
            programId: resolvedProgramId,
            title: input.title,
            teamName: input.teamName,
            description: input.description,
            tags: input.tags || [],
            country: normalizedCountry,
            competitionCategory: input.competitionCategory,
            oceanIssue: input.oceanIssue,
            institution: input.institution,
            metadataJson: Object.keys(fullMetadata).length > 0
              ? (fullMetadata as Prisma.InputJsonValue)
              : undefined,
            status: 'SUBMITTED',
          },
        })

        // Create team members if provided
        const inviteList: { userId: string; email: string; name: string }[] = []
        if (teamMembersInput && teamMembersInput.length > 0) {
          for (const member of teamMembersInput) {
            // Find or create user
            let user = await tx.user.findUnique({
              where: { email: member.email.toLowerCase() },
              select: { id: true, status: true },
            })

            if (!user) {
              user = await tx.user.create({
                data: {
                  email: member.email.toLowerCase(),
                  name: member.name,
                  role: 'APPLICANT',
                  status: 'NONE',
                  phoneNumber: member.phone || null,
                },
                select: { id: true, status: true },
              })
            }

            // Create TeamMember link (skip if already linked)
            await tx.teamMember.upsert({
              where: {
                projectId_userId: {
                  projectId: created.id,
                  userId: user.id,
                },
              },
              create: {
                projectId: created.id,
                userId: user.id,
                role: member.role,
                title: member.title || null,
              },
              update: {
                role: member.role,
                title: member.title || null,
              },
            })

            if (member.sendInvite) {
              inviteList.push({ userId: user.id, email: member.email.toLowerCase(), name: member.name })
            }
          }
        }

        return { project: created, membersToInvite: inviteList }
      })

      // Audit outside transaction so failures don't roll back the project creation
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'Project',
        entityId: project.id,
        detailsJson: {
          title: input.title,
          programId: resolvedProgramId,
          teamMembersCount: teamMembersInput?.length || 0,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      // Send invite emails outside the transaction (never fail project creation)
      if (membersToInvite.length > 0) {
        const baseUrl = process.env.NEXTAUTH_URL || 'https://monaco-opc.com'
        for (const member of membersToInvite) {
          try {
            const token = crypto.randomBytes(32).toString('hex')
            await ctx.prisma.user.update({
              where: { id: member.userId },
              data: {
                status: 'INVITED',
                inviteToken: token,
                inviteTokenExpiresAt: new Date(Date.now() + INVITE_TOKEN_EXPIRY_MS),
              },
            })

            const inviteUrl = `${baseUrl}/accept-invite?token=${token}`
            await sendInvitationEmail(member.email, member.name, inviteUrl, 'APPLICANT')

            // Log notification
            try {
              await ctx.prisma.notificationLog.create({
                data: {
                  userId: member.userId,
                  channel: 'EMAIL',
                  type: 'JURY_INVITATION',
                  status: 'SENT',
                },
              })
            } catch {
              // Never fail on notification logging
            }
          } catch {
            // Email sending failure should not break project creation
            console.error(`Failed to send invite to ${member.email}`)
          }
        }
      }

      return project
    }),

  /**
   * Update a project (admin only)
   */
  update: adminProcedure
    .input(
      z.object({
        id: z.string(),
        title: z.string().min(1).max(500).optional(),
        teamName: z.string().optional().nullable(),
        description: z.string().optional().nullable(),
        country: z.string().optional().nullable(), // ISO-2 code or country name (will be normalized)
        status: z
          .enum([
            'SUBMITTED',
            'ELIGIBLE',
            'ASSIGNED',
            'SEMIFINALIST',
            'FINALIST',
            'REJECTED',
          ])
          .optional(),
        tags: z.array(z.string()).optional(),
        metadataJson: z.record(z.unknown()).optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, metadataJson, status, country, ...data } = input

      // Normalize country to ISO-2 code if provided
      const normalizedCountry = country !== undefined
        ? (country === null ? null : normalizeCountryToCode(country))
        : undefined

      // Validate status transition if status is being changed
      if (status) {
        const currentProject = await ctx.prisma.project.findUniqueOrThrow({
          where: { id },
          select: { status: true },
        })
        const allowedTransitions = VALID_PROJECT_TRANSITIONS[currentProject.status] || []
        if (!allowedTransitions.includes(status)) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: `Invalid status transition: cannot change from ${currentProject.status} to ${status}. Allowed: ${allowedTransitions.join(', ') || 'none'}`,
          })
        }
      }

      const project = await ctx.prisma.$transaction(async (tx) => {
        const updated = await tx.project.update({
          where: { id },
          data: {
            ...data,
            ...(status && { status }),
            ...(normalizedCountry !== undefined && { country: normalizedCountry }),
            metadataJson: metadataJson as Prisma.InputJsonValue ?? undefined,
          },
        })

        // Record status change in history
        if (status) {
          await tx.projectStatusHistory.create({
            data: {
              projectId: id,
              status,
              changedBy: ctx.user.id,
            },
          })
        }

        return updated
      })

      // Send notifications if status changed
      if (status) {
        const notificationConfig: Record<
          string,
          { type: string; title: string; message: string }
        > = {
          SEMIFINALIST: {
            type: NotificationTypes.ADVANCED_SEMIFINAL,
            title: "Congratulations! You're a Semi-Finalist",
            message: `Your project "${project.title}" has advanced to the semi-finals!`,
          },
          FINALIST: {
            type: NotificationTypes.ADVANCED_FINAL,
            title: "Amazing News! You're a Finalist",
            message: `Your project "${project.title}" has been selected as a finalist!`,
          },
          REJECTED: {
            type: NotificationTypes.NOT_SELECTED,
            title: 'Application Status Update',
            message: `We regret to inform you that "${project.title}" was not selected for the next round.`,
          },
        }

        const config = notificationConfig[status]
        if (config) {
          await notifyProjectTeam(id, {
            type: config.type,
            title: config.title,
            message: config.message,
            linkUrl: `/team/projects/${id}`,
            linkLabel: 'View Project',
            priority: status === 'REJECTED' ? 'normal' : 'high',
            metadata: {
              projectName: project.title,
            },
          })
        }
      }

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPDATE',
        entityType: 'Project',
        entityId: id,
        detailsJson: { ...data, status, metadataJson } as Record<string, unknown>,
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return project
    }),

  /**
   * Delete a project (admin only)
   */
  delete: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const target = await ctx.prisma.project.findUniqueOrThrow({
        where: { id: input.id },
        select: { id: true, title: true },
      })

      const project = await ctx.prisma.project.delete({
        where: { id: input.id },
      })

      // Audit outside transaction so failures don't roll back the delete
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'Project',
        entityId: input.id,
        detailsJson: { title: target.title },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return project
    }),

  /**
   * Bulk delete projects (admin only)
   */
  bulkDelete: adminProcedure
    .input(
      z.object({
        ids: z.array(z.string()).min(1).max(200),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const projects = await ctx.prisma.project.findMany({
        where: { id: { in: input.ids } },
        select: { id: true, title: true },
      })

      if (projects.length === 0) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'No projects found to delete',
        })
      }

      const result = await ctx.prisma.project.deleteMany({
        where: { id: { in: projects.map((p) => p.id) } },
      })

      // Audit outside transaction so failures don't roll back the bulk delete
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'BULK_DELETE',
        entityType: 'Project',
        detailsJson: {
          count: projects.length,
          titles: projects.map((p) => p.title),
          ids: projects.map((p) => p.id),
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { deleted: result.count }
    }),

  /**
   * Import projects from CSV data (admin only)
   * Projects belong to a program.
   */
  importCSV: adminProcedure
    .input(
      z.object({
        programId: z.string(),
        projects: z.array(
          z.object({
            title: z.string().min(1),
            teamName: z.string().optional(),
            description: z.string().optional(),
            tags: z.array(z.string()).optional(),
            metadataJson: z.record(z.unknown()).optional(),
          })
        ),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Verify program exists
      await ctx.prisma.program.findUniqueOrThrow({
        where: { id: input.programId },
      })

      // Create projects in a transaction
      const result = await ctx.prisma.$transaction(async (tx) => {
        const projectData = input.projects.map((p) => {
          const { metadataJson, ...rest } = p
          return {
            ...rest,
            programId: input.programId,
            status: 'SUBMITTED' as const,
            metadataJson: metadataJson as Prisma.InputJsonValue ?? undefined,
          }
        })

        const created = await tx.project.createManyAndReturn({
          data: projectData,
          select: { id: true },
        })

        return { imported: created.length }
      })

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'IMPORT',
        entityType: 'Project',
        detailsJson: { programId: input.programId, count: result.imported },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return result
    }),

  /**
   * Get all unique tags used in projects
   */
  getTags: protectedProcedure
    .input(z.object({
      programId: z.string().optional(),
    }))
    .query(async ({ ctx, input }) => {
      const where: Record<string, unknown> = {}
      if (input.programId) where.programId = input.programId

      const projects = await ctx.prisma.project.findMany({
        where: Object.keys(where).length > 0 ? where : undefined,
        select: { tags: true },
      })

      const allTags = projects.flatMap((p) => p.tags)
      const uniqueTags = [...new Set(allTags)].sort()

      return uniqueTags
    }),

  /**
   * Update project status in bulk (admin only)
   */
  bulkUpdateStatus: adminProcedure
    .input(
      z.object({
        ids: z.array(z.string()),
        status: z.enum([
          'SUBMITTED',
          'ELIGIBLE',
          'ASSIGNED',
          'SEMIFINALIST',
          'FINALIST',
          'REJECTED',
        ]),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Fetch matching projects BEFORE update so notifications match actually-updated records
      const projects = await ctx.prisma.project.findMany({
        where: {
          id: { in: input.ids },
        },
        select: { id: true, title: true },
      })

      const matchingIds = projects.map((p) => p.id)

      // Validate status transitions for all projects
      const projectsWithStatus = await ctx.prisma.project.findMany({
        where: { id: { in: matchingIds } },
        select: { id: true, title: true, status: true },
      })
      const invalidTransitions: string[] = []
      for (const p of projectsWithStatus) {
        const allowed = VALID_PROJECT_TRANSITIONS[p.status] || []
        if (!allowed.includes(input.status)) {
          invalidTransitions.push(`"${p.title}" (${p.status} → ${input.status})`)
        }
      }
      if (invalidTransitions.length > 0) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: `Invalid transitions for ${invalidTransitions.length} project(s): ${invalidTransitions.slice(0, 3).join('; ')}${invalidTransitions.length > 3 ? ` and ${invalidTransitions.length - 3} more` : ''}`,
        })
      }

      const updated = await ctx.prisma.$transaction(async (tx) => {
        const result = await tx.project.updateMany({
          where: { id: { in: matchingIds } },
          data: { status: input.status },
        })

        if (matchingIds.length > 0) {
          await tx.projectStatusHistory.createMany({
            data: matchingIds.map((projectId) => ({
              projectId,
              status: input.status,
              changedBy: ctx.user.id,
            })),
          })
        }

        return result
      })

      // Audit outside transaction so failures don't roll back the bulk update
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'BULK_UPDATE_STATUS',
        entityType: 'Project',
        detailsJson: { ids: matchingIds, status: input.status, count: updated.count },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      // Notify project teams based on status
      if (projects.length > 0) {
        const notificationConfig: Record<
          string,
          { type: string; titleFn: (name: string) => string; messageFn: (name: string) => string }
        > = {
          SEMIFINALIST: {
            type: NotificationTypes.ADVANCED_SEMIFINAL,
            titleFn: () => "Congratulations! You're a Semi-Finalist",
            messageFn: (name) => `Your project "${name}" has advanced to the semi-finals!`,
          },
          FINALIST: {
            type: NotificationTypes.ADVANCED_FINAL,
            titleFn: () => "Amazing News! You're a Finalist",
            messageFn: (name) => `Your project "${name}" has been selected as a finalist!`,
          },
          REJECTED: {
            type: NotificationTypes.NOT_SELECTED,
            titleFn: () => 'Application Status Update',
            messageFn: (name) =>
              `We regret to inform you that "${name}" was not selected for the next round.`,
          },
        }

        const config = notificationConfig[input.status]
        if (config) {
          for (const project of projects) {
            await notifyProjectTeam(project.id, {
              type: config.type,
              title: config.titleFn(project.title),
              message: config.messageFn(project.title),
              linkUrl: `/team/projects/${project.id}`,
              linkLabel: 'View Project',
              priority: input.status === 'REJECTED' ? 'normal' : 'high',
              metadata: {
                projectName: project.title,
              },
            })
          }
        }
      }

      return { updated: updated.count }
    }),

  /**
   * List projects in a program's pool (not assigned to any stage)
   */
  listPool: adminProcedure
    .input(
      z.object({
        programId: z.string(),
        search: z.string().optional(),
        page: z.number().int().min(1).default(1),
        perPage: z.number().int().min(1).max(100).default(50),
      })
    )
    .query(async ({ ctx, input }) => {
      const { programId, search, page, perPage } = input
      const skip = (page - 1) * perPage

      const where: Record<string, unknown> = {
        programId,
        roundAssignments: { none: {} }, // Projects not assigned to any round
      }

      if (search) {
        where.OR = [
          { title: { contains: search, mode: 'insensitive' } },
          { teamName: { contains: search, mode: 'insensitive' } },
        ]
      }

      const [projects, total] = await Promise.all([
        ctx.prisma.project.findMany({
          where,
          skip,
          take: perPage,
          orderBy: { createdAt: 'desc' },
          select: {
            id: true,
            title: true,
            teamName: true,
            country: true,
            competitionCategory: true,
            createdAt: true,
          },
        }),
        ctx.prisma.project.count({ where }),
      ])

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

  /**
   * Get full project detail with assignments and evaluation stats in one call.
   * Reduces client-side waterfall by combining project.get + assignment.listByProject + evaluation.getProjectStats.
   */
  getFullDetail: adminProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const [projectRaw, projectTags, assignments, submittedEvaluations] = await Promise.all([
        ctx.prisma.project.findUniqueOrThrow({
          where: { id: input.id },
          include: {
            files: true,
            teamMembers: {
              include: {
                user: {
                  select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true },
                },
              },
              orderBy: { joinedAt: 'asc' },
            },
            mentorAssignment: {
              include: {
                mentor: {
                  select: { id: true, name: true, email: true, expertiseTags: true, profileImageKey: true, profileImageProvider: true },
                },
              },
            },
          },
        }),
        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, expertiseTags: true, profileImageKey: true, profileImageProvider: true } },
            evaluation: { select: { status: true, submittedAt: true, globalScore: true, binaryDecision: true } },
          },
          orderBy: { createdAt: 'desc' },
        }),
        ctx.prisma.evaluation.findMany({
          where: {
            status: 'SUBMITTED',
            assignment: { projectId: input.id },
          },
        }),
      ])

      // Compute evaluation stats
      let stats = null
      if (submittedEvaluations.length > 0) {
        const globalScores = submittedEvaluations
          .map((e) => e.globalScore)
          .filter((s): s is number => s !== null)
        const yesVotes = submittedEvaluations.filter((e) => e.binaryDecision === true).length
        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: (yesVotes / submittedEvaluations.length) * 100,
        }
      }

      // Attach avatar URLs in parallel
      const [teamMembersWithAvatars, assignmentsWithAvatars, mentorWithAvatar] = 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),
            },
          }))
        ),
        projectRaw.mentorAssignment
          ? (async () => ({
              ...projectRaw.mentorAssignment!,
              mentor: {
                ...projectRaw.mentorAssignment!.mentor,
                avatarUrl: await getUserAvatarUrl(
                  projectRaw.mentorAssignment!.mentor.profileImageKey,
                  projectRaw.mentorAssignment!.mentor.profileImageProvider
                ),
              },
            }))()
          : Promise.resolve(null),
      ])

      return {
        project: {
          ...projectRaw,
          projectTags,
          teamMembers: teamMembersWithAvatars,
          mentorAssignment: mentorWithAvatar,
        },
        assignments: assignmentsWithAvatars,
        stats,
      }
    }),
})
