import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { Prisma, PrismaClient } from '@prisma/client'
import { router, adminProcedure, withAIRateLimit } from '../trpc'
import { executeFilteringRules, type ProgressCallback, type AwardCriteriaInput, type AwardMatchResult } from '../services/ai-filtering'
import { sanitizeUserInput } from '../services/ai-prompt-guard'
import { logAudit } from '../utils/audit'
import { isOpenAIConfigured, testOpenAIConnection } from '@/lib/openai'
import { prisma } from '@/lib/prisma'
import {
  notifyAdmins,
  NotificationTypes,
} from '../services/in-app-notification'
import { triggerInProgressOnActivity } from '../services/round-engine'

/**
 * Verify the current session user exists in the database.
 * Guards against stale JWT sessions (e.g., after database reseed).
 */
async function ensureUserExists(db: PrismaClient, userId: string): Promise<string> {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { id: true },
  })
  if (!user) {
    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'Your session refers to a user that no longer exists. Please log out and log back in.',
    })
  }
  return user.id
}

/**
 * Extract a numeric confidence/quality score from aiScreeningJson.
 * Looks for common keys: overallScore, confidenceScore, score, qualityScore.
 * Returns 0 if no score found.
 */
function getAIConfidenceScore(aiScreeningJson: Prisma.JsonValue | null): number {
  if (!aiScreeningJson || typeof aiScreeningJson !== 'object' || Array.isArray(aiScreeningJson)) {
    return 0
  }
  let obj = aiScreeningJson as Record<string, unknown>
  // aiScreeningJson is nested under rule ID: { [ruleId]: { confidence, ... } }
  // Unwrap if top-level keys don't include expected score fields
  const scoreKeys = ['overallScore', 'confidenceScore', 'score', 'qualityScore', 'confidence']
  if (!scoreKeys.some((k) => k in obj)) {
    const keys = Object.keys(obj)
    if (keys.length > 0) {
      const inner = obj[keys[0]]
      if (inner && typeof inner === 'object' && !Array.isArray(inner)) {
        obj = inner as Record<string, unknown>
      }
    }
  }
  for (const key of scoreKeys) {
    if (typeof obj[key] === 'number') {
      return obj[key] as number
    }
  }
  return 0
}

export async function runFilteringJob(jobId: string, roundId: string, userId: string) {
  try {
    // Update job to running
    await prisma.filteringJob.update({
      where: { id: jobId },
      data: { status: 'RUNNING', startedAt: new Date() },
    })

    // Get rules
    const rules = await prisma.filteringRule.findMany({
      where: { roundId },
      orderBy: { priority: 'asc' },
    })

    // Get current round with config
    const currentRound = await prisma.round.findUniqueOrThrow({
      where: { id: roundId },
      select: { id: true, name: true, configJson: true, competitionId: true },
    })
    const roundConfig = (currentRound.configJson as Record<string, unknown>) || {}
    const aiParseFiles = !!roundConfig.aiParseFiles

    // Load special awards for integrated AI evaluation
    let awardsForAI: AwardCriteriaInput[] = []
    if (currentRound.competitionId) {
      const rawAwards = await prisma.specialAward.findMany({
        where: {
          competitionId: currentRound.competitionId,
          useAiEligibility: true,
          criteriaText: { not: null },
        },
        select: { id: true, name: true, criteriaText: true },
        orderBy: { sortOrder: 'asc' },
      })
      for (const a of rawAwards) {
        if (a.criteriaText && a.criteriaText.trim().length > 0) {
          const { sanitized } = sanitizeUserInput(a.criteriaText)
          awardsForAI.push({
            awardId: a.id,
            awardName: a.name,
            criteriaText: sanitized,
          })
        }
      }
      if (awardsForAI.length > 0) {
        console.log(`[Filtering] Including ${awardsForAI.length} special award(s) in AI evaluation`)
      }
    }

    // Get projects in this round via ProjectRoundState
    const projectStates = await prisma.projectRoundState.findMany({
      where: {
        roundId,
        exitedAt: null,
        state: { in: ['PENDING', 'IN_PROGRESS'] },
      },
      include: {
        project: {
          include: {
            files: {
              select: {
                id: true,
                fileName: true,
                fileType: true,
                mimeType: true,
                size: true,
                pageCount: true,
                detectedLang: true,
                langConfidence: true,
                objectKey: true,
                roundId: true,
                createdAt: true,
              },
              orderBy: { createdAt: 'desc' },
            },
          },
        },
      },
    })

    // Get round names for file tagging
    const roundIds = new Set<string>()
    for (const pss of projectStates) {
      for (const f of (pss as any).project?.files || []) {
        if (f.roundId) roundIds.add(f.roundId)
      }
    }
    const roundNames = new Map<string, string>()
    if (roundIds.size > 0) {
      const rounds = await prisma.round.findMany({
        where: { id: { in: [...roundIds] } },
        select: { id: true, name: true },
      })
      for (const r of rounds) roundNames.set(r.id, r.name)
    }

    // Optionally extract file contents
    let fileContents: Map<string, string> | undefined
    if (aiParseFiles) {
      const { extractMultipleFileContents } = await import('@/server/services/file-content-extractor')
      const allFiles = projectStates.flatMap((pss: any) =>
        ((pss.project?.files || []) as Array<{ id: string; fileName: string; mimeType: string; objectKey: string }>)
      )
      const extractions = await extractMultipleFileContents(allFiles)
      fileContents = new Map()
      for (const e of extractions) {
        if (e.content) fileContents.set(e.fileId, e.content)
      }
    }

    // Enrich projects with round-tagged file data
    const projects = projectStates.map((pss: any) => {
      const project = pss.project
      if (project?.files) {
        project.files = project.files.map((f: any) => ({
          ...f,
          roundName: f.roundId ? (roundNames.get(f.roundId) || 'Unknown Round') : null,
          isCurrentRound: f.roundId === roundId,
          textContent: fileContents?.get(f.id) || undefined,
        }))
      }
      return project
    }).filter(Boolean)

    // Calculate batch info
    const BATCH_SIZE = 20
    const totalBatches = Math.ceil(projects.length / BATCH_SIZE)

    await prisma.filteringJob.update({
      where: { id: jobId },
      data: { totalProjects: projects.length, totalBatches },
    })

    // Progress callback
    const onProgress: ProgressCallback = async (progress) => {
      await prisma.filteringJob.update({
        where: { id: jobId },
        data: {
          currentBatch: progress.currentBatch,
          processedCount: progress.processedCount,
        },
      })
    }

    // Auto-transition: mark all projects as IN_PROGRESS when filtering starts
    for (const pss of projectStates) {
      await triggerInProgressOnActivity(pss.projectId, roundId, userId, prisma)
    }

    // Execute rules — upsert results per batch for streaming to the UI
    const results = await executeFilteringRules(rules, projects, userId, roundId, onProgress, async (batchResults) => {
      if (batchResults.length === 0) return
      await prisma.$transaction(
        batchResults.map((r) =>
          prisma.filteringResult.upsert({
            where: {
              roundId_projectId: {
                roundId,
                projectId: r.projectId,
              },
            },
            create: {
              roundId,
              projectId: r.projectId,
              outcome: r.outcome,
              ruleResultsJson: r.ruleResults as unknown as Prisma.InputJsonValue,
              aiScreeningJson: (r.aiScreeningJson ?? Prisma.JsonNull) as Prisma.InputJsonValue,
            },
            update: {
              outcome: r.outcome,
              ruleResultsJson: r.ruleResults as unknown as Prisma.InputJsonValue,
              aiScreeningJson: (r.aiScreeningJson ?? Prisma.JsonNull) as Prisma.InputJsonValue,
              overriddenBy: null,
              overriddenAt: null,
              overrideReason: null,
              finalOutcome: null,
            },
          })
        )
      )

      // Upsert AwardEligibility for PASSED projects with award matches
      if (awardsForAI.length > 0) {
        const awardUpserts: Prisma.PrismaPromise<unknown>[] = []
        for (const r of batchResults) {
          if (r.outcome !== 'PASSED' || !r.awardMatches || r.awardMatches.length === 0) continue
          for (const am of r.awardMatches) {
            awardUpserts.push(
              prisma.awardEligibility.upsert({
                where: {
                  awardId_projectId: {
                    awardId: am.awardId,
                    projectId: r.projectId,
                  },
                },
                create: {
                  awardId: am.awardId,
                  projectId: r.projectId,
                  eligible: am.eligible,
                  method: 'AUTO',
                  qualityScore: am.qualityScore,
                  aiReasoningJson: { reasoning: am.reasoning, confidence: am.confidence },
                },
                update: {
                  // Only update AI-computed fields; preserve manual overrides
                  qualityScore: am.qualityScore,
                  aiReasoningJson: { reasoning: am.reasoning, confidence: am.confidence },
                },
              })
            )
          }
        }
        if (awardUpserts.length > 0) {
          await prisma.$transaction(awardUpserts)
        }
      }
    }, awardsForAI)

    // Sync eligible/method for non-overridden award eligibility records
    if (awardsForAI.length > 0) {
      const syncAwardIds = awardsForAI.map((a) => a.awardId)
      const nonOverridden = await prisma.awardEligibility.findMany({
        where: { awardId: { in: syncAwardIds }, overriddenBy: null },
        select: { awardId: true, projectId: true },
      })
      const nonOverriddenSet = new Set(nonOverridden.map((r) => `${r.awardId}:${r.projectId}`))

      const eligibleUpdates: Prisma.PrismaPromise<unknown>[] = []
      for (const r of results) {
        if (r.outcome !== 'PASSED' || !r.awardMatches) continue
        for (const am of r.awardMatches) {
          if (nonOverriddenSet.has(`${am.awardId}:${r.projectId}`)) {
            eligibleUpdates.push(
              prisma.awardEligibility.update({
                where: {
                  awardId_projectId: { awardId: am.awardId, projectId: r.projectId },
                },
                data: { eligible: am.eligible, method: 'AUTO' },
              })
            )
          }
        }
      }
      if (eligibleUpdates.length > 0) {
        await prisma.$transaction(eligibleUpdates)
      }
    }

    // Auto-shortlist top-N per award and mark eligibility job as completed
    if (awardsForAI.length > 0) {
      // Collect all award matches from PASSED results
      const awardMatchesByAward = new Map<string, Array<{ projectId: string; qualityScore: number }>>()
      for (const r of results) {
        if (r.outcome !== 'PASSED' || !r.awardMatches) continue
        for (const am of r.awardMatches) {
          if (!am.eligible) continue
          const arr = awardMatchesByAward.get(am.awardId) || []
          arr.push({ projectId: r.projectId, qualityScore: am.qualityScore })
          awardMatchesByAward.set(am.awardId, arr)
        }
      }

      // Load shortlistSize per award
      const awardIds = awardsForAI.map((a) => a.awardId)
      const awardsWithSize = await prisma.specialAward.findMany({
        where: { id: { in: awardIds } },
        select: { id: true, shortlistSize: true },
      })

      for (const award of awardsWithSize) {
        const eligible = awardMatchesByAward.get(award.id) || []
        const shortlistSize = award.shortlistSize ?? 10

        // Preserve manually-shortlisted records and account for them in count
        const alreadyShortlisted = await prisma.awardEligibility.findMany({
          where: { awardId: award.id, shortlisted: true, overriddenBy: { not: null } },
          select: { projectId: true },
        })
        const manuallyShortlistedIds = new Set(alreadyShortlisted.map((r) => r.projectId))

        const topN = eligible
          .filter((e) => !manuallyShortlistedIds.has(e.projectId))
          .sort((a, b) => b.qualityScore - a.qualityScore)
          .slice(0, Math.max(0, shortlistSize - manuallyShortlistedIds.size))

        if (topN.length > 0) {
          await prisma.$transaction(
            topN.map((e) =>
              prisma.awardEligibility.update({
                where: {
                  awardId_projectId: {
                    awardId: award.id,
                    projectId: e.projectId,
                  },
                },
                data: { shortlisted: true },
              })
            )
          )
        }

        // Mark award eligibility job as completed
        await prisma.specialAward.update({
          where: { id: award.id },
          data: {
            eligibilityJobStatus: 'COMPLETED',
            eligibilityJobDone: results.length,
            eligibilityJobTotal: results.length,
          },
        })
      }

      console.log(`[Filtering] Auto-shortlisted for ${awardsWithSize.length} award(s)`)
    }

    // Clean up stale results from previous runs (projects no longer in this round)
    const processedIds = projects.map((p: any) => p.id)
    if (processedIds.length > 0) {
      await prisma.filteringResult.deleteMany({
        where: {
          roundId,
          projectId: { notIn: processedIds },
        },
      })

      // Clean up stale award eligibilities for linked awards
      if (awardsForAI.length > 0) {
        const cleanupAwardIds = awardsForAI.map((a) => a.awardId)
        await prisma.awardEligibility.deleteMany({
          where: {
            awardId: { in: cleanupAwardIds },
            projectId: { notIn: processedIds },
          },
        })
      }
    }

    // Count outcomes
    const passedCount = results.filter((r) => r.outcome === 'PASSED').length
    const filteredCount = results.filter((r) => r.outcome === 'FILTERED_OUT').length
    const flaggedCount = results.filter((r) => r.outcome === 'FLAGGED').length

    // Mark job as completed
    await prisma.filteringJob.update({
      where: { id: jobId },
      data: {
        status: 'COMPLETED',
        completedAt: new Date(),
        processedCount: projects.length,
        passedCount,
        filteredCount,
        flaggedCount,
      },
    })

    // Audit log
    await logAudit({
      userId,
      action: 'UPDATE',
      entityType: 'Stage',
      entityId: roundId,
      detailsJson: {
        action: 'EXECUTE_FILTERING',
        jobId,
        projectCount: projects.length,
        passed: passedCount,
        filteredOut: filteredCount,
        flagged: flaggedCount,
      },
    })

    // Get round name for notification
    const round = await prisma.round.findUnique({
      where: { id: roundId },
      select: { name: true },
    })

    // Notify admins that filtering is complete
    await notifyAdmins({
      type: NotificationTypes.FILTERING_COMPLETE,
      title: 'AI Filtering Complete',
      message: `Filtering complete for ${round?.name || 'round'}: ${passedCount} passed, ${flaggedCount} flagged, ${filteredCount} filtered out`,
      linkUrl: `/admin/rounds/${roundId}`,
      linkLabel: 'View Results',
      priority: 'high',
      metadata: {
        roundId,
        jobId,
        projectCount: projects.length,
        passedCount,
        filteredCount,
        flaggedCount,
      },
    })
  } catch (error) {
    console.error('[Filtering Job] Error:', error)
    await prisma.filteringJob.update({
      where: { id: jobId },
      data: {
        status: 'FAILED',
        completedAt: new Date(),
        errorMessage: error instanceof Error ? error.message : 'Unknown error',
      },
    })

    await notifyAdmins({
      type: NotificationTypes.FILTERING_FAILED,
      title: 'AI Filtering Failed',
      message: `Filtering job failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
      linkUrl: `/admin/rounds/${roundId}`,
      linkLabel: 'View Details',
      priority: 'urgent',
      metadata: { roundId, jobId, error: error instanceof Error ? error.message : 'Unknown error' },
    })
  }
}

export const filteringRouter = router({
  /**
   * Check if AI is configured and ready for filtering
   */
  checkAIStatus: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const aiRules = await ctx.prisma.filteringRule.count({
        where: {
          roundId: input.roundId,
          ruleType: 'AI_SCREENING',
          isActive: true,
        },
      })

      if (aiRules === 0) {
        return { hasAIRules: false, configured: true, error: null }
      }

      // Check if OpenAI is configured
      const configured = await isOpenAIConfigured()
      if (!configured) {
        return {
          hasAIRules: true,
          configured: false,
          error: 'OpenAI API key not configured',
        }
      }

      // Test the connection
      const testResult = await testOpenAIConnection()
      return {
        hasAIRules: true,
        configured: testResult.success,
        error: testResult.error || null,
        model: testResult.modelTested,
      }
    }),

  /**
   * Get filtering rules for a stage
   */
  getRules: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.filteringRule.findMany({
        where: { roundId: input.roundId, isActive: true },
        orderBy: { priority: 'asc' },
      })
    }),

  /**
   * Create a filtering rule
   */
  createRule: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        name: z.string().min(1),
        ruleType: z.enum(['FIELD_BASED', 'DOCUMENT_CHECK', 'AI_SCREENING']),
        configJson: z.record(z.unknown()),
        priority: z.number().int().default(0),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const rule = await ctx.prisma.filteringRule.create({
        data: {
          roundId: input.roundId,
          name: input.name,
          ruleType: input.ruleType,
          configJson: input.configJson as Prisma.InputJsonValue,
          priority: input.priority,
        },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'FilteringRule',
        entityId: rule.id,
        detailsJson: { roundId: input.roundId, name: input.name, ruleType: input.ruleType },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return rule
    }),

  /**
   * Update a filtering rule
   */
  updateRule: adminProcedure
    .input(
      z.object({
        id: z.string(),
        name: z.string().min(1).optional(),
        ruleType: z.enum(['FIELD_BASED', 'DOCUMENT_CHECK', 'AI_SCREENING']).optional(),
        configJson: z.record(z.unknown()).optional(),
        priority: z.number().int().optional(),
        isActive: z.boolean().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, configJson, ...rest } = input
      const rule = await ctx.prisma.filteringRule.update({
        where: { id },
        data: {
          ...rest,
          ...(configJson !== undefined && { configJson: configJson as Prisma.InputJsonValue }),
        },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPDATE',
        entityType: 'FilteringRule',
        entityId: id,
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return rule
    }),

  /**
   * Delete a filtering rule
   */
  deleteRule: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.filteringRule.delete({ where: { id: input.id } })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'FilteringRule',
        entityId: input.id,
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })
    }),

  /**
   * Reorder rules (batch update priorities)
   */
  reorderRules: adminProcedure
    .input(
      z.object({
        rules: z.array(
          z.object({
            id: z.string(),
            priority: z.number().int(),
          })
        ),
      })
    )
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.$transaction(
        input.rules.map((r) =>
          ctx.prisma.filteringRule.update({
            where: { id: r.id },
            data: { priority: r.priority },
          })
        )
      )
    }),

  /**
   * Start a filtering job (runs in background)
   */
  startJob: adminProcedure
    .use(withAIRateLimit)
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const existingJob = await ctx.prisma.filteringJob.findFirst({
        where: { roundId: input.roundId, status: 'RUNNING' },
      })
      if (existingJob) {
        throw new TRPCError({
          code: 'CONFLICT',
          message: 'A filtering job is already running for this stage',
        })
      }

      const rules = await ctx.prisma.filteringRule.findMany({
        where: { roundId: input.roundId },
        orderBy: { priority: 'asc' },
      })

      if (rules.length === 0) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'No filtering rules configured for this stage',
        })
      }

      const hasAIRules = rules.some((r) => r.ruleType === 'AI_SCREENING' && r.isActive)
      if (hasAIRules) {
        const aiConfigured = await isOpenAIConfigured()
        if (!aiConfigured) {
          throw new TRPCError({
            code: 'PRECONDITION_FAILED',
            message:
              'AI screening rules require OpenAI to be configured. Go to Settings → AI to configure your API key.',
          })
        }
        const testResult = await testOpenAIConnection()
        if (!testResult.success) {
          throw new TRPCError({
            code: 'PRECONDITION_FAILED',
            message: `AI configuration error: ${testResult.error}. Go to Settings → AI to fix.`,
          })
        }
      }

      const projectCount = await ctx.prisma.projectRoundState.count({
        where: {
          roundId: input.roundId,
          exitedAt: null,
          state: { in: ['PENDING', 'IN_PROGRESS'] },
        },
      })
      if (projectCount === 0) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'No projects found in this stage',
        })
      }

      // Reset award eligibility job status for linked awards (safe — just UI progress indicators)
      // NOTE: We no longer delete filteringResults or awardEligibilities here.
      // The job uses upserts, and stale records are cleaned up AFTER the job succeeds.
      // This prevents data loss if the job fails mid-run.
      const roundForComp = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { competitionId: true },
      })
      if (roundForComp.competitionId) {
        const linkedAwards = await ctx.prisma.specialAward.findMany({
          where: {
            competitionId: roundForComp.competitionId,
            useAiEligibility: true,
          },
          select: { id: true },
        })
        const awardIds = linkedAwards.map((a) => a.id)
        if (awardIds.length > 0) {
          await ctx.prisma.specialAward.updateMany({
            where: { id: { in: awardIds } },
            data: {
              eligibilityJobStatus: null,
              eligibilityJobTotal: null,
              eligibilityJobDone: null,
              eligibilityJobError: null,
              eligibilityJobStarted: null,
            },
          })
        }
      }

      const job = await ctx.prisma.filteringJob.create({
        data: {
          roundId: input.roundId,
          status: 'PENDING',
          totalProjects: projectCount,
        },
      })

      setImmediate(() => {
        runFilteringJob(job.id, input.roundId, ctx.user.id).catch(console.error)
      })

      return { jobId: job.id, message: 'Filtering job started' }
    }),

  /**
   * Get current job status
   */
  getJobStatus: adminProcedure
    .input(z.object({ jobId: z.string() }))
    .query(async ({ ctx, input }) => {
      const job = await ctx.prisma.filteringJob.findUnique({
        where: { id: input.jobId },
      })
      if (!job) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Job not found' })
      }
      return job
    }),

  /**
   * Get latest job for a stage
   */
  getLatestJob: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.filteringJob.findFirst({
        where: { roundId: input.roundId },
        orderBy: { createdAt: 'desc' },
      })
    }),

  /**
   * Execute all filtering rules against projects in a stage (synchronous)
   */
  executeRules: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const rules = await ctx.prisma.filteringRule.findMany({
        where: { roundId: input.roundId },
        orderBy: { priority: 'asc' },
      })

      if (rules.length === 0) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'No filtering rules configured for this stage',
        })
      }

      const hasAIRules = rules.some((r) => r.ruleType === 'AI_SCREENING' && r.isActive)

      if (hasAIRules) {
        const aiConfigured = await isOpenAIConfigured()
        if (!aiConfigured) {
          throw new TRPCError({
            code: 'PRECONDITION_FAILED',
            message:
              'AI screening rules require OpenAI to be configured. Go to Settings → AI to configure your API key.',
          })
        }

        const testResult = await testOpenAIConnection()
        if (!testResult.success) {
          throw new TRPCError({
            code: 'PRECONDITION_FAILED',
            message: `AI configuration error: ${testResult.error}. Go to Settings → AI to fix.`,
          })
        }
      }

      const projectStates = await ctx.prisma.projectRoundState.findMany({
        where: {
          roundId: input.roundId,
          exitedAt: null,
          state: { in: ['PENDING', 'IN_PROGRESS'] },
        },
        include: {
          project: {
            include: {
              files: {
                select: { id: true, fileName: true, fileType: true, size: true, pageCount: true },
              },
            },
          },
        },
      })
      const projects = projectStates.map((pss: any) => pss.project).filter(Boolean)

      if (projects.length === 0) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'No projects found in this stage',
        })
      }

      const results = await executeFilteringRules(rules, projects)

      const BATCH_SIZE = 25
      for (let i = 0; i < results.length; i += BATCH_SIZE) {
        const batch = results.slice(i, i + BATCH_SIZE)
        await ctx.prisma.$transaction(
          batch.map((r) =>
            ctx.prisma.filteringResult.upsert({
              where: {
                roundId_projectId: {
                  roundId: input.roundId,
                  projectId: r.projectId,
                },
              },
              create: {
                roundId: input.roundId,
                projectId: r.projectId,
                outcome: r.outcome,
                ruleResultsJson: r.ruleResults as unknown as Prisma.InputJsonValue,
                aiScreeningJson: (r.aiScreeningJson ?? Prisma.JsonNull) as Prisma.InputJsonValue,
              },
              update: {
                outcome: r.outcome,
                ruleResultsJson: r.ruleResults as unknown as Prisma.InputJsonValue,
                aiScreeningJson: (r.aiScreeningJson ?? Prisma.JsonNull) as Prisma.InputJsonValue,
                overriddenBy: null,
                overriddenAt: null,
                overrideReason: null,
                finalOutcome: null,
              },
            })
          )
        )
      }

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPDATE',
        entityType: 'Stage',
        entityId: input.roundId,
        detailsJson: {
          action: 'EXECUTE_FILTERING',
          projectCount: projects.length,
          passed: results.filter((r) => r.outcome === 'PASSED').length,
          filteredOut: results.filter((r) => r.outcome === 'FILTERED_OUT').length,
          flagged: results.filter((r) => r.outcome === 'FLAGGED').length,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return {
        total: results.length,
        passed: results.filter((r) => r.outcome === 'PASSED').length,
        filteredOut: results.filter((r) => r.outcome === 'FILTERED_OUT').length,
        flagged: results.filter((r) => r.outcome === 'FLAGGED').length,
      }
    }),

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

      const where: Record<string, unknown> = { roundId }
      if (outcome) {
        // Filter by effective outcome (finalOutcome if overridden, otherwise original outcome)
        where.OR = [
          { finalOutcome: outcome },
          { finalOutcome: null, outcome },
        ]
      }

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

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

  /**
   * Get aggregate stats for filtering results
   */
  getResultStats: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      // Use effective outcome (finalOutcome if overridden, otherwise original outcome)
      const [passed, filteredOut, flagged, overridden] = await Promise.all([
        ctx.prisma.filteringResult.count({
          where: {
            roundId: input.roundId,
            OR: [
              { finalOutcome: 'PASSED' },
              { finalOutcome: null, outcome: 'PASSED' },
            ],
          },
        }),
        ctx.prisma.filteringResult.count({
          where: {
            roundId: input.roundId,
            OR: [
              { finalOutcome: 'FILTERED_OUT' },
              { finalOutcome: null, outcome: 'FILTERED_OUT' },
            ],
          },
        }),
        ctx.prisma.filteringResult.count({
          where: {
            roundId: input.roundId,
            OR: [
              { finalOutcome: 'FLAGGED' },
              { finalOutcome: null, outcome: 'FLAGGED' },
            ],
          },
        }),
        ctx.prisma.filteringResult.count({
          where: { roundId: input.roundId, overriddenBy: { not: null } },
        }),
      ])

      // Count projects routed to SEPARATE_POOL award tracks (confirmed shortlist)
      const round = await ctx.prisma.round.findUnique({
        where: { id: input.roundId },
        select: { competitionId: true },
      })

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

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

  /**
   * Override a single filtering result
   */
  overrideResult: adminProcedure
    .input(
      z.object({
        id: z.string(),
        finalOutcome: z.enum(['PASSED', 'FILTERED_OUT', 'FLAGGED']),
        reason: z.string().min(1),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id)

      const result = await ctx.prisma.filteringResult.update({
        where: { id: input.id },
        data: {
          finalOutcome: input.finalOutcome,
          overriddenByUser: { connect: { id: verifiedUserId } },
          overriddenAt: new Date(),
          overrideReason: input.reason,
        },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: verifiedUserId,
        action: 'UPDATE',
        entityType: 'FilteringResult',
        entityId: input.id,
        detailsJson: {
          action: 'OVERRIDE',
          originalOutcome: result.outcome,
          finalOutcome: input.finalOutcome,
          reason: input.reason,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return result
    }),

  /**
   * Bulk override multiple results
   */
  bulkOverride: adminProcedure
    .input(
      z.object({
        ids: z.array(z.string()),
        finalOutcome: z.enum(['PASSED', 'FILTERED_OUT', 'FLAGGED']),
        reason: z.string().min(1),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id)

      await ctx.prisma.filteringResult.updateMany({
        where: { id: { in: input.ids } },
        data: {
          finalOutcome: input.finalOutcome,
          overriddenBy: verifiedUserId,
          overriddenAt: new Date(),
          overrideReason: input.reason,
        },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: verifiedUserId,
        action: 'BULK_UPDATE_STATUS',
        entityType: 'FilteringResult',
        detailsJson: {
          action: 'BULK_OVERRIDE',
          count: input.ids.length,
          finalOutcome: input.finalOutcome,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { updated: input.ids.length }
    }),

  /**
   * Finalize filtering results — apply outcomes to project statuses
   * PASSED → mark as ELIGIBLE
   * FILTERED_OUT → mark as REJECTED (data preserved)
   */
  finalizeResults: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        categoryTargets: z.record(z.number().int().min(0)).optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id)

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

      const nextRound = await ctx.prisma.round.findFirst({
        where: {
          competitionId: currentRound.competitionId,
          sortOrder: { gt: currentRound.sortOrder },
        },
        orderBy: { sortOrder: 'asc' },
        select: { id: true, name: true },
      })

      const results = await ctx.prisma.filteringResult.findMany({
        where: { roundId: input.roundId },
        include: {
          project: {
            select: { competitionCategory: true },
          },
        },
      })

      const filteredOutIds = results
        .filter((r) => (r.finalOutcome || r.outcome) === 'FILTERED_OUT')
        .map((r) => r.projectId)

      let passedResults = results
        .filter((r) => (r.finalOutcome || r.outcome) === 'PASSED')

      // Apply category targets if provided
      const categoryWarnings: string[] = []
      const categoryCounts: Record<string, number> = {}
      const demotedIds: string[] = []

      if (input.categoryTargets && Object.keys(input.categoryTargets).length > 0) {
        // Group passing projects by category
        const passedByCategory = new Map<string, typeof passedResults>()
        for (const r of passedResults) {
          const cat = r.project.competitionCategory?.toLowerCase().trim() || '_uncategorized'
          const existing = passedByCategory.get(cat) || []
          existing.push(r)
          passedByCategory.set(cat, existing)
        }

        // Check each category against its target
        for (const [cat, target] of Object.entries(input.categoryTargets)) {
          const normalizedCat = cat.toLowerCase().trim()
          const catResults = passedByCategory.get(normalizedCat) || []
          categoryCounts[cat] = catResults.length

          if (catResults.length > target) {
            // Sort by AI confidence score (descending) to keep the best
            const sorted = catResults.sort((a, b) => {
              const scoreA = getAIConfidenceScore(a.aiScreeningJson)
              const scoreB = getAIConfidenceScore(b.aiScreeningJson)
              return scoreB - scoreA
            })

            // Demote the lowest-ranked excess to FLAGGED
            const excess = sorted.slice(target)
            for (const r of excess) {
              demotedIds.push(r.id)
            }
          } else if (catResults.length < target) {
            categoryWarnings.push(
              `Category "${cat}" is below target: ${catResults.length}/${target}`
            )
          }
        }

        // Also count categories not in targets
        for (const [cat, catResults] of passedByCategory) {
          if (!Object.keys(input.categoryTargets).some((k) => k.toLowerCase().trim() === cat)) {
            categoryCounts[cat] = catResults.length
          }
        }

        // Remove demoted from passedResults
        const demotedIdSet = new Set(demotedIds)
        passedResults = passedResults.filter((r) => !demotedIdSet.has(r.id))
      } else {
        // Build category counts even without targets
        for (const r of passedResults) {
          const cat = r.project.competitionCategory || '_uncategorized'
          categoryCounts[cat] = (categoryCounts[cat] || 0) + 1
        }
      }

      const passedIds = passedResults.map((r) => r.projectId)

      // Exclude projects confirmed for SEPARATE_POOL award tracks from main pool
      const awardRouted = await ctx.prisma.awardEligibility.findMany({
        where: {
          projectId: { in: passedIds },
          shortlisted: true,
          confirmedAt: { not: null },
          award: { eligibilityMode: 'SEPARATE_POOL' },
        },
        select: { projectId: true },
      })
      const awardRoutedIds = new Set(awardRouted.map((a) => a.projectId))
      const mainPoolPassedIds = passedIds.filter((id) => !awardRoutedIds.has(id))

      const operations: Prisma.PrismaPromise<unknown>[] = []

      if (filteredOutIds.length > 0) {
        operations.push(
          ctx.prisma.project.updateMany({
            where: { id: { in: filteredOutIds } },
            data: { status: 'REJECTED' },
          })
        )
      }

      if (mainPoolPassedIds.length > 0) {
        operations.push(
          ctx.prisma.project.updateMany({
            where: { id: { in: mainPoolPassedIds } },
            data: { status: 'ELIGIBLE' },
          })
        )
      }

      // Update demoted results to FLAGGED outcome
      if (demotedIds.length > 0) {
        operations.push(
          ctx.prisma.filteringResult.updateMany({
            where: { id: { in: demotedIds } },
            data: {
              finalOutcome: 'FLAGGED',
              overriddenBy: verifiedUserId,
              overriddenAt: new Date(),
              overrideReason: 'Demoted by category target enforcement',
            },
          })
        )
      }

      // Advance passed projects to the next round
      if (nextRound && mainPoolPassedIds.length > 0) {
        operations.push(
          ctx.prisma.projectRoundState.createMany({
            data: mainPoolPassedIds.map((projectId) => ({
              projectId,
              roundId: nextRound.id,
            })),
            skipDuplicates: true,
          })
        )
      }

      await ctx.prisma.$transaction(operations)

      await logAudit({
        prisma: ctx.prisma,
        userId: verifiedUserId,
        action: 'UPDATE',
        entityType: 'Stage',
        entityId: input.roundId,
        detailsJson: {
          action: 'FINALIZE_FILTERING',
          passed: mainPoolPassedIds.length,
          routedToAwards: awardRoutedIds.size,
          filteredOut: filteredOutIds.length,
          demotedToFlagged: demotedIds.length,
          categoryTargets: input.categoryTargets || null,
          categoryWarnings,
          advancedToStage: nextRound?.name || null,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return {
        passed: mainPoolPassedIds.length,
        routedToAwards: awardRoutedIds.size,
        filteredOut: filteredOutIds.length,
        demotedToFlagged: demotedIds.length,
        categoryCounts,
        categoryWarnings,
        advancedToStageId: nextRound?.id || null,
        advancedToStageName: nextRound?.name || null,
      }
    }),

  /**
   * Reinstate a filtered-out project back into the active pool
   */
  reinstateProject: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        projectId: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id)

      await ctx.prisma.filteringResult.update({
        where: {
          roundId_projectId: {
            roundId: input.roundId,
            projectId: input.projectId,
          },
        },
        data: {
          finalOutcome: 'PASSED',
          overriddenByUser: { connect: { id: verifiedUserId } },
          overriddenAt: new Date(),
          overrideReason: 'Reinstated by admin',
        },
      })

      await ctx.prisma.project.update({
        where: { id: input.projectId },
        data: { status: 'ELIGIBLE' },
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: verifiedUserId,
        action: 'UPDATE',
        entityType: 'FilteringResult',
        detailsJson: {
          action: 'REINSTATE',
          roundId: input.roundId,
          projectId: input.projectId,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })
    }),

  /**
   * Bulk reinstate filtered-out projects
   */
  bulkReinstate: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        projectIds: z.array(z.string()),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id)

      await ctx.prisma.$transaction([
        ...input.projectIds.map((projectId) =>
          ctx.prisma.filteringResult.update({
            where: {
              roundId_projectId: {
                roundId: input.roundId,
                projectId,
              },
            },
            data: {
              finalOutcome: 'PASSED',
              overriddenByUser: { connect: { id: verifiedUserId } },
              overriddenAt: new Date(),
              overrideReason: 'Bulk reinstated by admin',
            },
          })
        ),
        ctx.prisma.project.updateMany({
          where: { id: { in: input.projectIds } },
          data: { status: 'ELIGIBLE' },
        }),
      ])

      await logAudit({
        prisma: ctx.prisma,
        userId: verifiedUserId,
        action: 'BULK_UPDATE_STATUS',
        entityType: 'FilteringResult',
        detailsJson: {
          action: 'BULK_REINSTATE',
          roundId: input.roundId,
          count: input.projectIds.length,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { reinstated: input.projectIds.length }
    }),
})
