/**
 * Enhanced Assignment Service (Round-Aware)
 *
 * Builds on existing smart-assignment scoring and integrates with the
 * Phase 2 policy engine for cap/mode/bias resolution.
 */

import type { PrismaClient, Prisma } from '@prisma/client'
import { logAudit } from '@/server/utils/audit'
import { resolveCompetitionContext, resolveMemberContext } from './competition-context'
import { evaluateAssignmentPolicy } from './assignment-policy'
import {
  calculateTagOverlapScore,
  calculateBioMatchScore,
  calculateWorkloadScore,
  calculateAvailabilityPenalty,
  calculateCategoryQuotaPenalty,
  type ProjectTagData,
  type ScoreBreakdown,
} from './smart-assignment'

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

export type AssignmentPreview = {
  assignments: AssignmentSuggestion[]
  warnings: string[]
  stats: {
    totalProjects: number
    totalJurors: number
    assignmentsGenerated: number
    unassignedProjects: number
  }
}

export type AssignmentSuggestion = {
  userId: string
  userName: string
  projectId: string
  projectTitle: string
  score: number
  breakdown: ScoreBreakdown
  reasoning: string[]
  matchingTags: string[]
  policyViolations: string[]
  fromIntent: boolean
}

export type CoverageReport = {
  totalProjects: number
  fullyAssigned: number
  partiallyAssigned: number
  unassigned: number
  avgReviewsPerProject: number
  requiredReviews: number
  byCategory: Record<string, { total: number; assigned: number; coverage: number }>
}

// ─── Constants ──────────────────────────────────────────────────────────────

const GEO_DIVERSITY_THRESHOLD = 2
const GEO_DIVERSITY_PENALTY = -15
const PREVIOUS_ROUND_FAMILIARITY_BONUS = 10

// ─── Preview Assignment ─────────────────────────────────────────────────────

/**
 * Preview round assignments without committing them.
 * 1. Load round + juryGroup + members via competition-context
 * 2. Evaluate policy for each member via evaluateAssignmentPolicy()
 * 3. Honor pending AssignmentIntents first
 * 4. Run scoring algorithm
 * 5. Enforce hard/soft caps per member
 * 6. Return preview with policy violation warnings
 */
export async function previewRoundAssignment(
  roundId: string,
  config?: { honorIntents?: boolean; requiredReviews?: number },
  prisma?: PrismaClient,
): Promise<AssignmentPreview> {
  const db = prisma ?? (await import('@/lib/prisma')).prisma
  const honorIntents = config?.honorIntents ?? true
  const requiredReviews = config?.requiredReviews ?? 3

  const ctx = await resolveCompetitionContext(roundId)
  const warnings: string[] = []

  if (!ctx.juryGroup) {
    return {
      assignments: [],
      warnings: ['Round has no linked jury group'],
      stats: { totalProjects: 0, totalJurors: 0, assignmentsGenerated: 0, unassignedProjects: 0 },
    }
  }

  // Load jury group members
  const members = await db.juryGroupMember.findMany({
    where: { juryGroupId: ctx.juryGroup.id },
    include: {
      user: { select: { id: true, name: true, email: true, role: true, bio: true, expertiseTags: true, country: true, availabilityJson: true } },
    },
  })

  // Load projects in this round (with active ProjectRoundState)
  const projectStates = await db.projectRoundState.findMany({
    where: { roundId, state: { in: ['PENDING', 'IN_PROGRESS'] } },
    include: {
      project: {
        select: {
          id: true,
          title: true,
          description: true,
          country: true,
          competitionCategory: true,
          projectTags: { include: { tag: true } },
        },
      },
    },
  })

  const projects = projectStates.map((ps: any) => ps.project)
  if (projects.length === 0) {
    return {
      assignments: [],
      warnings: ['No active projects in this round'],
      stats: { totalProjects: 0, totalJurors: members.length, assignmentsGenerated: 0, unassignedProjects: 0 },
    }
  }

  // Load existing assignments for this round
  const existingAssignments = await db.assignment.findMany({
    where: { roundId },
    select: { userId: true, projectId: true },
  })
  const assignedPairs = new Set(existingAssignments.map((a: any) => `${a.userId}:${a.projectId}`))

  // Track assignment counts per juror for policy evaluation
  const jurorAssignmentCounts = new Map<string, number>()
  for (const a of existingAssignments) {
    jurorAssignmentCounts.set(a.userId, (jurorAssignmentCounts.get(a.userId) ?? 0) + 1)
  }

  // Load pending intents
  let pendingIntents: any[] = []
  if (honorIntents) {
    pendingIntents = await db.assignmentIntent.findMany({
      where: { roundId, status: 'INTENT_PENDING' },
      include: {
        juryGroupMember: { include: { user: { select: { id: true } } } },
      },
    })
  }

  // Load COI records
  const coiRecords = await db.conflictOfInterest.findMany({
    where: {
      assignment: { roundId },
      hasConflict: true,
    },
    select: { userId: true, projectId: true },
  })
  const coiPairs = new Set(coiRecords.map((c: any) => `${c.userId}:${c.projectId}`))

  // Build assignment suggestions
  const suggestions: AssignmentSuggestion[] = []
  const projectAssignmentCounts = new Map<string, number>()

  // Count existing coverage
  for (const a of existingAssignments) {
    projectAssignmentCounts.set(a.projectId, (projectAssignmentCounts.get(a.projectId) ?? 0) + 1)
  }

  // First: honor pending intents
  const intentAssignments = new Set<string>()
  for (const intent of pendingIntents) {
    const userId = intent.juryGroupMember.user.id
    const projectId = intent.projectId
    const pairKey = `${userId}:${projectId}`

    if (assignedPairs.has(pairKey) || coiPairs.has(pairKey)) continue

    const member = members.find((m: any) => m.userId === userId)
    if (!member) continue

    const project = projects.find((p: any) => p.id === projectId)
    if (!project) continue

    suggestions.push({
      userId,
      userName: member.user.name ?? 'Unknown',
      projectId,
      projectTitle: project.title,
      score: 100, // Intent-based assignments get max priority
      breakdown: { tagOverlap: 0, bioMatch: 0, workloadBalance: 0, countryMatch: 0, geoDiversityPenalty: 0, previousRoundFamiliarity: 0, coiPenalty: 0, availabilityPenalty: 0, categoryQuotaPenalty: 0 },
      reasoning: ['Honoring assignment intent'],
      matchingTags: [],
      policyViolations: [],
      fromIntent: true,
    })

    intentAssignments.add(pairKey)
  }

  // Then: algorithmic matching for remaining needs
  for (const member of members) {
    const userId = member.userId
    const currentCount = (jurorAssignmentCounts.get(userId) ?? 0) +
      suggestions.filter((s) => s.userId === userId).length

    // Build a minimal member context for policy evaluation
    const memberCtx = {
      ...ctx,
      member: member as any,
      user: member.user,
      currentAssignmentCount: currentCount,
      assignmentsByCategory: {},
      pendingIntents: [],
    }

    const policy = evaluateAssignmentPolicy(memberCtx)

    if (!policy.canAssignMore) continue

    for (const project of projects) {
      const pairKey = `${userId}:${project.id}`
      if (assignedPairs.has(pairKey) || intentAssignments.has(pairKey) || coiPairs.has(pairKey)) continue

      // Check project needs more reviews
      const currentProjectReviews = (projectAssignmentCounts.get(project.id) ?? 0) +
        suggestions.filter((s) => s.projectId === project.id).length
      if (currentProjectReviews >= requiredReviews) continue

      // Calculate score
      const projectTags: ProjectTagData[] = project.projectTags.map((pt: any) => ({
        tagId: pt.tagId,
        tagName: pt.tag.name,
        confidence: pt.confidence,
      }))

      const { score: tagScore, matchingTags } = calculateTagOverlapScore(
        member.user.expertiseTags ?? [],
        projectTags,
      )

      const { score: bioScore } = calculateBioMatchScore(
        member.user.bio,
        project.description,
      )

      const workloadScore = calculateWorkloadScore(
        currentCount,
        policy.effectiveCap.value,
      )

      const availabilityPenalty = calculateAvailabilityPenalty(
        member.user.availabilityJson,
        ctx.round.windowOpenAt,
        ctx.round.windowCloseAt,
      )

      const categoryQuotaPenalty = policy.categoryBias.value
        ? calculateCategoryQuotaPenalty(
            Object.fromEntries(
              Object.entries(policy.categoryBias.value).map(([k, v]) => [k, { min: 0, max: Math.round(v * policy.effectiveCap.value) }]),
            ),
            {},
            project.competitionCategory,
          )
        : 0

      const totalScore = tagScore + bioScore + workloadScore + availabilityPenalty + categoryQuotaPenalty
      const policyViolations: string[] = []
      if (policy.isOverCap) {
        policyViolations.push(`Over cap by ${policy.overCapBy}`)
      }

      const reasoning: string[] = []
      if (matchingTags.length > 0) reasoning.push(`${matchingTags.length} matching tag(s)`)
      if (bioScore > 0) reasoning.push('Bio match')
      if (availabilityPenalty < 0) reasoning.push('Availability concern')

      suggestions.push({
        userId,
        userName: member.user.name ?? 'Unknown',
        projectId: project.id,
        projectTitle: project.title,
        score: totalScore,
        breakdown: {
          tagOverlap: tagScore,
          bioMatch: bioScore,
          workloadBalance: workloadScore,
          countryMatch: 0,
          geoDiversityPenalty: 0,
          previousRoundFamiliarity: 0,
          coiPenalty: 0,
          availabilityPenalty,
          categoryQuotaPenalty,
        },
        reasoning,
        matchingTags,
        policyViolations,
        fromIntent: false,
      })
    }
  }

  // Balance-aware greedy assignment: at each step, pick the best suggestion
  // while dynamically penalizing jurors who already have more assignments
  // to distribute work as evenly as possible.
  const BALANCE_PENALTY_PER_ASSIGNMENT = 25

  const finalAssignments: AssignmentSuggestion[] = []
  const finalJurorCounts = new Map(jurorAssignmentCounts)
  const finalProjectCounts = new Map(projectAssignmentCounts)
  const consumed = new Set<number>()

  while (consumed.size < suggestions.length) {
    let bestIdx = -1
    let bestAdjustedScore = -Infinity

    for (let i = 0; i < suggestions.length; i++) {
      if (consumed.has(i)) continue
      const s = suggestions[i]

      const projectCount = finalProjectCounts.get(s.projectId) ?? 0
      if (projectCount >= requiredReviews) {
        consumed.add(i)
        continue
      }

      const jurorCount = finalJurorCounts.get(s.userId) ?? 0
      // Dynamic balance penalty: penalize jurors who already have more assignments
      const balancePenalty = jurorCount * BALANCE_PENALTY_PER_ASSIGNMENT
      const adjustedScore = s.score - balancePenalty

      if (adjustedScore > bestAdjustedScore) {
        bestAdjustedScore = adjustedScore
        bestIdx = i
      }
    }

    if (bestIdx === -1) break

    const suggestion = suggestions[bestIdx]
    consumed.add(bestIdx)

    const jurorCount = finalJurorCounts.get(suggestion.userId) ?? 0

    // Re-check cap
    const member = members.find((m: any) => m.userId === suggestion.userId)
    if (member) {
      const memberCtx = {
        ...ctx,
        member: member as any,
        user: member.user,
        currentAssignmentCount: jurorCount,
        assignmentsByCategory: {},
        pendingIntents: [],
      }
      const policy = evaluateAssignmentPolicy(memberCtx)
      if (!policy.canAssignMore && !suggestion.fromIntent) continue
    }

    finalAssignments.push(suggestion)
    finalJurorCounts.set(suggestion.userId, jurorCount + 1)
    finalProjectCounts.set(suggestion.projectId, (finalProjectCounts.get(suggestion.projectId) ?? 0) + 1)
  }

  const unassignedProjects = projects.filter(
    (p: any) => (finalProjectCounts.get(p.id) ?? 0) < requiredReviews,
  ).length

  return {
    assignments: finalAssignments,
    warnings,
    stats: {
      totalProjects: projects.length,
      totalJurors: members.length,
      assignmentsGenerated: finalAssignments.length,
      unassignedProjects,
    },
  }
}

// ─── Execute Assignment ─────────────────────────────────────────────────────

/**
 * Execute round assignments by creating Assignment records.
 */
export async function executeRoundAssignment(
  roundId: string,
  assignments: Array<{ userId: string; projectId: string }>,
  actorId: string,
  prisma: PrismaClient,
): Promise<{ created: number; errors: string[] }> {
  const db = prisma ?? (await import('@/lib/prisma')).prisma
  const errors: string[] = []
  let created = 0

  for (const assignment of assignments) {
    try {
      await db.$transaction(async (tx: Prisma.TransactionClient) => {
        // Create assignment record
        await tx.assignment.create({
          data: {
            projectId: assignment.projectId,
            userId: assignment.userId,
            roundId,
            method: 'ALGORITHM',
          },
        })

        // Create or update ProjectRoundState to IN_PROGRESS
        await tx.projectRoundState.upsert({
          where: {
            projectId_roundId: {
              projectId: assignment.projectId,
              roundId,
            },
          },
          create: {
            projectId: assignment.projectId,
            roundId,
            state: 'IN_PROGRESS',
            enteredAt: new Date(),
          },
          update: {
            state: 'IN_PROGRESS',
          },
        })

        // Honor matching intent if exists
        const intent = await tx.assignmentIntent.findFirst({
          where: {
            roundId,
            projectId: assignment.projectId,
            juryGroupMember: { userId: assignment.userId },
            status: 'INTENT_PENDING',
          },
        })
        if (intent) {
          await tx.assignmentIntent.update({
            where: { id: intent.id },
            data: { status: 'HONORED' },
          })
        }

        await logAudit({
          prisma: tx,
          userId: actorId,
          action: 'ROUND_ASSIGNMENT_CREATE',
          entityType: 'Assignment',
          entityId: `${assignment.userId}:${assignment.projectId}`,
          detailsJson: { roundId, userId: assignment.userId, projectId: assignment.projectId },
        })

        created++
      })
    } catch (error) {
      errors.push(
        `Failed to assign ${assignment.userId} to ${assignment.projectId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
      )
    }
  }

  // Overall audit
  await logAudit({
    prisma: db,
    userId: actorId,
    action: 'ROUND_ASSIGNMENT_BATCH',
    entityType: 'Round',
    entityId: roundId,
    detailsJson: { created, failed: errors.length },
  })

  return { created, errors }
}

// ─── Coverage Report ────────────────────────────────────────────────────────

/**
 * Get coverage report for a round's assignments.
 */
export async function getRoundCoverageReport(
  roundId: string,
  requiredReviews: number = 3,
  prisma?: PrismaClient,
): Promise<CoverageReport> {
  const db = prisma ?? (await import('@/lib/prisma')).prisma

  const projectStates = await db.projectRoundState.findMany({
    where: { roundId },
    include: {
      project: { select: { id: true, competitionCategory: true } },
    },
  })

  const assignments = await db.assignment.findMany({
    where: { roundId },
    select: { projectId: true },
  })

  const projectAssignmentCounts = new Map<string, number>()
  for (const a of assignments) {
    projectAssignmentCounts.set(a.projectId, (projectAssignmentCounts.get(a.projectId) ?? 0) + 1)
  }

  let fullyAssigned = 0
  let partiallyAssigned = 0
  let unassigned = 0
  const byCategory: Record<string, { total: number; assigned: number; coverage: number }> = {}

  for (const ps of projectStates) {
    const count = projectAssignmentCounts.get(ps.project.id) ?? 0
    const cat = ps.project.competitionCategory ?? 'UNCATEGORIZED'

    if (!byCategory[cat]) {
      byCategory[cat] = { total: 0, assigned: 0, coverage: 0 }
    }
    byCategory[cat].total++

    if (count >= requiredReviews) {
      fullyAssigned++
      byCategory[cat].assigned++
    } else if (count > 0) {
      partiallyAssigned++
      byCategory[cat].assigned++
    } else {
      unassigned++
    }
  }

  // Calculate coverage percentages
  for (const cat of Object.keys(byCategory)) {
    byCategory[cat].coverage = byCategory[cat].total > 0
      ? Math.round((byCategory[cat].assigned / byCategory[cat].total) * 100)
      : 0
  }

  const totalReviews = assignments.length
  const avgReviews = projectStates.length > 0 ? totalReviews / projectStates.length : 0

  return {
    totalProjects: projectStates.length,
    fullyAssigned,
    partiallyAssigned,
    unassigned,
    avgReviewsPerProject: Math.round(avgReviews * 10) / 10,
    requiredReviews,
    byCategory,
  }
}

// ─── Unassigned Queue ───────────────────────────────────────────────────────

/**
 * Get projects below requiredReviews threshold.
 */
export async function getUnassignedQueue(
  roundId: string,
  requiredReviews: number = 3,
  prisma?: PrismaClient,
) {
  const db = prisma ?? (await import('@/lib/prisma')).prisma

  const projectStates = await db.projectRoundState.findMany({
    where: { roundId, state: { in: ['PENDING', 'IN_PROGRESS'] } },
    include: {
      project: {
        select: {
          id: true,
          title: true,
          teamName: true,
          competitionCategory: true,
          _count: {
            select: {
              assignments: { where: { roundId } },
            },
          },
        },
      },
    },
  })

  return projectStates
    .filter((ps: any) => ps.project._count.assignments < requiredReviews)
    .map((ps: any) => ({
      projectId: ps.project.id,
      title: ps.project.title,
      teamName: ps.project.teamName,
      category: ps.project.competitionCategory,
      currentReviews: ps.project._count.assignments,
      needed: requiredReviews - ps.project._count.assignments,
      state: ps.state,
    }))
    .sort((a: any, b: any) => a.currentReviews - b.currentReviews)
}
