import { TRPCError } from '@trpc/server'
import { prisma } from '@/lib/prisma'
import {
  createNotification,
  notifyAdmins,
  NotificationTypes,
} from './in-app-notification'
import { logAudit } from '@/server/utils/audit'

/**
 * Reassign a project after a juror declares COI.
 * Deletes the old assignment, finds an eligible replacement juror, and creates a new assignment.
 * Returns the new juror info or null if no eligible juror found.
 */
export async function reassignAfterCOI(params: {
  assignmentId: string
  auditUserId?: string
  auditIp?: string
  auditUserAgent?: string
}): Promise<{ newJurorId: string; newJurorName: string; newAssignmentId: string } | null> {
  const assignment = await prisma.assignment.findUnique({
    where: { id: params.assignmentId },
    include: {
      round: { select: { id: true, name: true, configJson: true, juryGroupId: true } },
      project: { select: { id: true, title: true } },
      user: { select: { id: true, name: true, email: true } },
    },
  })

  if (!assignment) return null

  const { roundId, projectId } = assignment
  const config = (assignment.round.configJson ?? {}) as Record<string, unknown>
  const maxAssignmentsPerJuror =
    (config.maxLoadPerJuror as number) ??
    (config.maxAssignmentsPerJuror as number) ??
    20

  // ── Build exclusion set: jurors who must NEVER get this project ──────────

  // 1. Currently assigned to this project in ANY round (not just current)
  const allProjectAssignments = await prisma.assignment.findMany({
    where: { projectId },
    select: { userId: true },
  })
  const excludedUserIds = new Set(allProjectAssignments.map((a) => a.userId))

  // 2. COI records for this project (any juror who declared conflict, ever)
  const coiRecords = await prisma.conflictOfInterest.findMany({
    where: { projectId, hasConflict: true },
    select: { userId: true },
  })
  for (const c of coiRecords) excludedUserIds.add(c.userId)

  // 3. Historical: jurors who previously had this project but were removed
  //    (via COI reassignment or admin transfer — tracked in audit logs)
  const historicalAuditLogs = await prisma.decisionAuditLog.findMany({
    where: {
      eventType: { in: ['COI_REASSIGNMENT', 'ASSIGNMENT_TRANSFER'] },
      detailsJson: { path: ['projectId'], equals: projectId },
    },
    select: { detailsJson: true },
  })
  for (const log of historicalAuditLogs) {
    const details = log.detailsJson as Record<string, unknown> | null
    if (!details) continue
    // COI_REASSIGNMENT logs: oldJurorId had the project, newJurorId got it
    if (details.oldJurorId) excludedUserIds.add(details.oldJurorId as string)
    // ASSIGNMENT_TRANSFER logs: sourceJurorId lost the project
    if (details.sourceJurorId) excludedUserIds.add(details.sourceJurorId as string)
    // Transfer logs may have a moves array with per-project details
    if (Array.isArray(details.moves)) {
      for (const move of details.moves as Array<Record<string, unknown>>) {
        if (move.projectId === projectId && move.newJurorId) {
          // The juror who received via past transfer also had it
          excludedUserIds.add(move.newJurorId as string)
        }
      }
    }
  }

  // ── Find candidate jurors ───────────────────────────────────────────────

  let candidateJurors: { id: string; name: string | null; email: string; maxAssignments: number | null }[]

  if (assignment.round.juryGroupId) {
    const members = await prisma.juryGroupMember.findMany({
      where: { juryGroupId: assignment.round.juryGroupId },
      include: { user: { select: { id: true, name: true, email: true, maxAssignments: true, status: true } } },
    })
    candidateJurors = members
      .filter((m) => m.user.status === 'ACTIVE')
      .map((m) => m.user)
  } else {
    // No jury group — scope to jurors already assigned to this round
    const roundJurorIds = await prisma.assignment.findMany({
      where: { roundId },
      select: { userId: true },
      distinct: ['userId'],
    })
    const activeRoundJurorIds = roundJurorIds.map((a) => a.userId)

    candidateJurors = activeRoundJurorIds.length > 0
      ? await prisma.user.findMany({
          where: {
            id: { in: activeRoundJurorIds },
            roles: { has: 'JURY_MEMBER' },
            status: 'ACTIVE',
          },
          select: { id: true, name: true, email: true, maxAssignments: true },
        })
      : []
  }

  // Filter out all excluded jurors (current assignments, COI, historical)
  const eligible = candidateJurors.filter((j) => !excludedUserIds.has(j.id))

  if (eligible.length === 0) return null

  // ── Score eligible jurors: prefer those with incomplete evaluations ──────

  const eligibleIds = eligible.map((j) => j.id)

  // Get assignment counts and evaluation completion for eligible jurors in this round
  const roundAssignments = await prisma.assignment.findMany({
    where: { roundId, userId: { in: eligibleIds } },
    select: { userId: true, evaluation: { select: { status: true } } },
  })

  // Build per-juror stats: total assignments, completed evaluations
  const jurorStats = new Map<string, { total: number; completed: number }>()
  for (const a of roundAssignments) {
    const stats = jurorStats.get(a.userId) || { total: 0, completed: 0 }
    stats.total++
    if (a.evaluation?.status === 'SUBMITTED' || a.evaluation?.status === 'LOCKED') {
      stats.completed++
    }
    jurorStats.set(a.userId, stats)
  }

  // Rank jurors: under cap, then prefer those still working (completed < total)
  const ranked = eligible
    .map((j) => {
      const stats = jurorStats.get(j.id) || { total: 0, completed: 0 }
      const effectiveMax = j.maxAssignments ?? maxAssignmentsPerJuror
      const hasIncomplete = stats.completed < stats.total
      return { ...j, currentCount: stats.total, effectiveMax, hasIncomplete }
    })
    .filter((j) => j.currentCount < j.effectiveMax)
    .sort((a, b) => {
      // 1. Prefer jurors with incomplete evaluations (still active)
      if (a.hasIncomplete !== b.hasIncomplete) return a.hasIncomplete ? -1 : 1
      // 2. Then fewest current assignments (load balancing)
      return a.currentCount - b.currentCount
    })

  if (ranked.length === 0) return null

  const replacement = ranked[0]

  // Delete old assignment and create replacement atomically.
  // Cascade deletes COI record and any draft evaluation.
  const newAssignment = await prisma.$transaction(async (tx) => {
    await tx.assignment.delete({ where: { id: params.assignmentId } })
    return tx.assignment.create({
      data: {
        userId: replacement.id,
        projectId,
        roundId,
        juryGroupId: assignment.juryGroupId ?? assignment.round.juryGroupId ?? undefined,
        isRequired: assignment.isRequired,
        method: 'MANUAL',
      },
    })
  })

  // Notify the replacement juror (COI-specific notification)
  await createNotification({
    userId: replacement.id,
    type: NotificationTypes.COI_REASSIGNED,
    title: 'Project Reassigned to You (COI)',
    message: `The project "${assignment.project.title}" has been reassigned to you for ${assignment.round.name} because the previously assigned juror declared a conflict of interest.`,
    linkUrl: `/jury/competitions`,
    linkLabel: 'View Assignment',
    metadata: { projectId, projectName: assignment.project.title, roundName: assignment.round.name },
  })

  // Notify admins of the reassignment
  await notifyAdmins({
    type: NotificationTypes.EVALUATION_MILESTONE,
    title: 'COI Auto-Reassignment',
    message: `Project "${assignment.project.title}" was reassigned from ${assignment.user.name || assignment.user.email} to ${replacement.name || replacement.email} due to conflict of interest.`,
    linkUrl: `/admin/rounds/${roundId}`,
    linkLabel: 'View Round',
    metadata: {
      projectId,
      oldJurorId: assignment.userId,
      newJurorId: replacement.id,
      reason: 'COI',
    },
  })

  // Audit
  if (params.auditUserId) {
    await logAudit({
      prisma,
      userId: params.auditUserId,
      action: 'COI_REASSIGNMENT',
      entityType: 'Assignment',
      entityId: newAssignment.id,
      detailsJson: {
        oldAssignmentId: params.assignmentId,
        oldJurorId: assignment.userId,
        newJurorId: replacement.id,
        projectId,
        roundId,
      },
      ipAddress: params.auditIp,
      userAgent: params.auditUserAgent,
    })
  }

  return {
    newJurorId: replacement.id,
    newJurorName: replacement.name || replacement.email,
    newAssignmentId: newAssignment.id,
  }
}

/** Evaluation statuses that are safe to move (not yet finalized). */
const MOVABLE_EVAL_STATUSES = ['NOT_STARTED', 'DRAFT'] as const

export async function reassignDroppedJurorAssignments(params: {
  roundId: string
  droppedJurorId: string
  auditUserId?: string
  auditIp?: string
  auditUserAgent?: string
}) {
  const round = await prisma.round.findUnique({
    where: { id: params.roundId },
    select: { id: true, name: true, configJson: true, juryGroupId: true },
  })

  if (!round) {
    throw new TRPCError({ code: 'NOT_FOUND', message: 'Round not found' })
  }

  const droppedJuror = await prisma.user.findUnique({
    where: { id: params.droppedJurorId },
    select: { id: true, name: true, email: true },
  })

  if (!droppedJuror) {
    throw new TRPCError({ code: 'NOT_FOUND', message: 'Juror not found' })
  }

  const config = (round.configJson ?? {}) as Record<string, unknown>
  const fallbackCap =
    (config.maxLoadPerJuror as number) ??
    (config.maxAssignmentsPerJuror as number) ??
    20

  // Only pick assignments with no evaluation or evaluation still in draft/not-started.
  // Explicitly enumerate movable statuses so SUBMITTED and LOCKED are never touched.
  const assignmentsToMove = await prisma.assignment.findMany({
    where: {
      roundId: params.roundId,
      userId: params.droppedJurorId,
      OR: [
        { evaluation: null },
        { evaluation: { status: { in: [...MOVABLE_EVAL_STATUSES] } } },
      ],
    },
    select: {
      id: true,
      projectId: true,
      juryGroupId: true,
      isRequired: true,
      createdAt: true,
      project: { select: { title: true } },
    },
    orderBy: { createdAt: 'asc' },
  })

  if (assignmentsToMove.length === 0) {
    return {
      movedCount: 0,
      failedCount: 0,
      failedProjects: [] as string[],
      reassignedTo: {} as Record<string, number>,
    }
  }

  let candidateJurors: { id: string; name: string | null; email: string; maxAssignments: number | null }[]

  if (round.juryGroupId) {
    const members = await prisma.juryGroupMember.findMany({
      where: { juryGroupId: round.juryGroupId },
      include: {
        user: {
          select: {
            id: true,
            name: true,
            email: true,
            maxAssignments: true,
            status: true,
          },
        },
      },
    })

    candidateJurors = members
      .filter((m) => m.user.status === 'ACTIVE' && m.user.id !== params.droppedJurorId)
      .map((m) => m.user)
  } else {
    // No jury group configured — scope to jurors already assigned to this round
    // (the de facto jury pool). This prevents assigning to random JURY_MEMBER
    // accounts that aren't part of this round's jury.
    const roundJurorIds = await prisma.assignment.findMany({
      where: { roundId: params.roundId },
      select: { userId: true },
      distinct: ['userId'],
    })
    const activeRoundJurorIds = roundJurorIds
      .map((a) => a.userId)
      .filter((id) => id !== params.droppedJurorId)

    candidateJurors = activeRoundJurorIds.length > 0
      ? await prisma.user.findMany({
          where: {
            id: { in: activeRoundJurorIds },
            roles: { has: 'JURY_MEMBER' },
            status: 'ACTIVE',
          },
          select: { id: true, name: true, email: true, maxAssignments: true },
        })
      : []
  }

  if (candidateJurors.length === 0) {
    throw new TRPCError({ code: 'BAD_REQUEST', message: 'No active replacement jurors available' })
  }

  const candidateIds = candidateJurors.map((j) => j.id)

  const existingAssignments = await prisma.assignment.findMany({
    where: { roundId: params.roundId },
    select: { userId: true, projectId: true },
  })

  const alreadyAssigned = new Set(existingAssignments.map((a) => `${a.userId}:${a.projectId}`))
  const currentLoads = new Map<string, number>()
  for (const a of existingAssignments) {
    currentLoads.set(a.userId, (currentLoads.get(a.userId) ?? 0) + 1)
  }

  const coiRecords = await prisma.conflictOfInterest.findMany({
    where: {
      assignment: { roundId: params.roundId },
      hasConflict: true,
      userId: { in: candidateIds },
    },
    select: { userId: true, projectId: true },
  })
  const coiPairs = new Set(coiRecords.map((c) => `${c.userId}:${c.projectId}`))

  const caps = new Map<string, number>()
  for (const juror of candidateJurors) {
    caps.set(juror.id, juror.maxAssignments ?? fallbackCap)
  }

  const candidateMeta = new Map(candidateJurors.map((j) => [j.id, j]))
  const plannedMoves: {
    assignmentId: string
    projectId: string
    projectTitle: string
    newJurorId: string
    juryGroupId: string | null
    isRequired: boolean
  }[] = []
  const failedProjects: string[] = []

  for (const assignment of assignmentsToMove) {
    const eligible = candidateIds
      .filter((jurorId) => !alreadyAssigned.has(`${jurorId}:${assignment.projectId}`))
      .filter((jurorId) => !coiPairs.has(`${jurorId}:${assignment.projectId}`))
      .filter((jurorId) => (currentLoads.get(jurorId) ?? 0) < (caps.get(jurorId) ?? fallbackCap))
      .sort((a, b) => {
        const loadDiff = (currentLoads.get(a) ?? 0) - (currentLoads.get(b) ?? 0)
        if (loadDiff !== 0) return loadDiff
        return a.localeCompare(b)
      })

    if (eligible.length === 0) {
      failedProjects.push(assignment.project.title)
      continue
    }

    const selectedJurorId = eligible[0]
    plannedMoves.push({
      assignmentId: assignment.id,
      projectId: assignment.projectId,
      projectTitle: assignment.project.title,
      newJurorId: selectedJurorId,
      juryGroupId: assignment.juryGroupId ?? round.juryGroupId,
      isRequired: assignment.isRequired,
    })

    alreadyAssigned.add(`${selectedJurorId}:${assignment.projectId}`)
    currentLoads.set(selectedJurorId, (currentLoads.get(selectedJurorId) ?? 0) + 1)
  }

  // Execute moves inside a transaction with per-move TOCTOU guard.
  // Uses conditional deleteMany so a concurrent evaluation submission
  // (which sets status to SUBMITTED) causes the delete to return count=0
  // instead of cascade-destroying the submitted evaluation.
  const actualMoves: typeof plannedMoves = []
  const skippedProjects: string[] = []

  if (plannedMoves.length > 0) {
    await prisma.$transaction(async (tx) => {
      for (const move of plannedMoves) {
        // Guard: only delete if the assignment still belongs to the dropped juror
        // AND its evaluation (if any) is still in a movable state.
        // If a juror submitted between our read and now, count will be 0.
        const deleted = await tx.assignment.deleteMany({
          where: {
            id: move.assignmentId,
            userId: params.droppedJurorId,
            OR: [
              { evaluation: null },
              { evaluation: { status: { in: [...MOVABLE_EVAL_STATUSES] } } },
            ],
          },
        })

        if (deleted.count === 0) {
          // Assignment was already moved, deleted, or its evaluation was submitted
          skippedProjects.push(move.projectTitle)
          continue
        }

        await tx.assignment.create({
          data: {
            roundId: params.roundId,
            projectId: move.projectId,
            userId: move.newJurorId,
            juryGroupId: move.juryGroupId ?? undefined,
            isRequired: move.isRequired,
            method: 'MANUAL',
            createdBy: params.auditUserId ?? undefined,
          },
        })
        actualMoves.push(move)
      }
    })
  }

  // Add skipped projects to the failed list
  failedProjects.push(...skippedProjects)

  const reassignedTo: Record<string, number> = {}
  for (const move of actualMoves) {
    reassignedTo[move.newJurorId] = (reassignedTo[move.newJurorId] ?? 0) + 1
  }

  if (actualMoves.length > 0) {
    // Build per-juror project name lists for proper emails
    const destProjectNames: Record<string, string[]> = {}
    for (const move of actualMoves) {
      if (!destProjectNames[move.newJurorId]) destProjectNames[move.newJurorId] = []
      destProjectNames[move.newJurorId].push(move.projectTitle)
    }

    const droppedName = droppedJuror.name || droppedJuror.email

    // Fetch round deadline for email
    const roundFull = await prisma.round.findUnique({
      where: { id: params.roundId },
      select: { windowCloseAt: true },
    })
    const deadline = roundFull?.windowCloseAt
      ? new Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'short', timeZone: 'Europe/Paris' }).format(roundFull.windowCloseAt)
      : undefined

    for (const [jurorId, projectNames] of Object.entries(destProjectNames)) {
      const count = projectNames.length
      await createNotification({
        userId: jurorId,
        type: NotificationTypes.DROPOUT_REASSIGNED,
        title: count === 1 ? 'Project Reassigned to You' : `${count} Projects Reassigned to You`,
        message: count === 1
          ? `The project "${projectNames[0]}" has been reassigned to you because ${droppedName} is no longer available in ${round.name}.`
          : `${count} projects have been reassigned to you because ${droppedName} is no longer available in ${round.name}: ${projectNames.join(', ')}.`,
        linkUrl: `/jury/competitions`,
        linkLabel: 'View Assignments',
        metadata: { roundId: round.id, roundName: round.name, projectNames, droppedJurorName: droppedName, deadline, reason: 'juror_drop_reshuffle' },
      })
    }

    const topReceivers = Object.entries(reassignedTo)
      .map(([jurorId, count]) => {
        const juror = candidateMeta.get(jurorId)
        return `${juror?.name || juror?.email || jurorId} (${count})`
      })
      .join(', ')

    await notifyAdmins({
      type: NotificationTypes.EVALUATION_MILESTONE,
      title: 'Juror Dropout Reshuffle',
      message: `Reassigned ${actualMoves.length} project(s) from ${droppedName} to: ${topReceivers}. ${failedProjects.length > 0 ? `${failedProjects.length} project(s) could not be reassigned.` : 'All projects were reassigned successfully.'}`,
      linkUrl: `/admin/rounds/${round.id}`,
      linkLabel: 'View Round',
      metadata: {
        roundId: round.id,
        droppedJurorId: droppedJuror.id,
        movedCount: actualMoves.length,
        failedCount: failedProjects.length,
        topReceivers,
      },
    })
  }

  // Remove the dropped juror from the jury group so they can't be re-assigned
  // in future assignment runs for this round's competition.
  let removedFromGroup = false
  if (round.juryGroupId) {
    const deleted = await prisma.juryGroupMember.deleteMany({
      where: {
        juryGroupId: round.juryGroupId,
        userId: params.droppedJurorId,
      },
    })
    removedFromGroup = deleted.count > 0
  }

  if (params.auditUserId) {
    // Build per-project move detail for audit trail
    const moveDetails = actualMoves.map((move) => {
      const juror = candidateMeta.get(move.newJurorId)
      return {
        projectId: move.projectId,
        projectTitle: move.projectTitle,
        newJurorId: move.newJurorId,
        newJurorName: juror?.name || juror?.email || move.newJurorId,
      }
    })

    await logAudit({
      prisma,
      userId: params.auditUserId,
      action: 'JUROR_DROPOUT_RESHUFFLE',
      entityType: 'Round',
      entityId: round.id,
      detailsJson: {
        droppedJurorId: droppedJuror.id,
        droppedJurorName: droppedJuror.name || droppedJuror.email,
        movedCount: actualMoves.length,
        failedCount: failedProjects.length,
        failedProjects,
        skippedProjects,
        reassignedTo,
        removedFromGroup,
        moves: moveDetails,
      },
      ipAddress: params.auditIp,
      userAgent: params.auditUserAgent,
    })
  }

  return {
    movedCount: actualMoves.length,
    failedCount: failedProjects.length,
    failedProjects,
    reassignedTo,
  }
}
