/**
 * Round Engine Service
 *
 * State machine for round lifecycle transitions, operating on Round +
 * ProjectRoundState. Parallels stage-engine.ts but for the Competition/Round
 * architecture.
 *
 * Key invariants:
 * - Round transitions follow: ROUND_DRAFT → ROUND_ACTIVE → ROUND_CLOSED → ROUND_ARCHIVED
 * - Project transitions within an active round only
 * - All mutations are transactional with dual audit trail
 */

import type { PrismaClient, ProjectRoundStateValue, Prisma } from '@prisma/client'
import { logAudit } from '@/server/utils/audit'
import { safeValidateRoundConfig } from '@/types/competition-configs'
import { expireIntentsForRound } from './assignment-intent'

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

export type RoundTransitionResult = {
  success: boolean
  round?: { id: string; status: string }
  errors?: string[]
}

export type ProjectRoundTransitionResult = {
  success: boolean
  projectRoundState?: {
    id: string
    projectId: string
    roundId: string
    state: ProjectRoundStateValue
  }
  errors?: string[]
}

export type BatchProjectTransitionResult = {
  succeeded: string[]
  failed: Array<{ projectId: string; errors: string[] }>
  total: number
}

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

const BATCH_SIZE = 50

// ─── Valid Transition Maps ──────────────────────────────────────────────────

const VALID_ROUND_TRANSITIONS: Record<string, string[]> = {
  ROUND_DRAFT: ['ROUND_ACTIVE'],
  ROUND_ACTIVE: ['ROUND_CLOSED'],
  ROUND_CLOSED: ['ROUND_ACTIVE', 'ROUND_ARCHIVED'],
  ROUND_ARCHIVED: [],
}

// ─── Round-Level Transitions ────────────────────────────────────────────────

/**
 * Activate a round: ROUND_DRAFT → ROUND_ACTIVE
 * Guards: configJson is valid, competition is not ARCHIVED
 * Side effects: expire pending intents from previous round (if any)
 */
export async function activateRound(
  roundId: string,
  actorId: string,
  prisma: PrismaClient | any,
): Promise<RoundTransitionResult> {
  try {
    const round = await prisma.round.findUnique({
      where: { id: roundId },
      include: { competition: true },
    })

    if (!round) {
      return { success: false, errors: [`Round ${roundId} not found`] }
    }

    // Check valid transition
    if (round.status !== 'ROUND_DRAFT') {
      return {
        success: false,
        errors: [`Cannot activate round: current status is ${round.status}, expected ROUND_DRAFT`],
      }
    }

    // Guard: competition must not be ARCHIVED
    if (round.competition.status === 'ARCHIVED') {
      return {
        success: false,
        errors: ['Cannot activate round: competition is ARCHIVED'],
      }
    }

    // Guard: configJson must be valid
    if (round.configJson) {
      const validation = safeValidateRoundConfig(
        round.roundType,
        round.configJson as Record<string, unknown>,
      )
      if (!validation.success) {
        return {
          success: false,
          errors: [`Invalid round config: ${validation.error.message}`],
        }
      }
    }

    const updated = await prisma.$transaction(async (tx: any) => {
      const result = await tx.round.update({
        where: { id: roundId },
        data: { status: 'ROUND_ACTIVE' },
      })

      await tx.decisionAuditLog.create({
        data: {
          eventType: 'round.activated',
          entityType: 'Round',
          entityId: roundId,
          actorId,
          detailsJson: {
            roundName: round.name,
            roundType: round.roundType,
            competitionId: round.competitionId,
            previousStatus: 'ROUND_DRAFT',
          },
          snapshotJson: {
            timestamp: new Date().toISOString(),
            emittedBy: 'round-engine',
          },
        },
      })

      return result
    })

    // Audit log outside transaction to avoid FK violations poisoning the tx
    await logAudit({
      userId: actorId,
      action: 'ROUND_ACTIVATE',
      entityType: 'Round',
      entityId: roundId,
      detailsJson: { name: round.name, roundType: round.roundType },
    })

    return {
      success: true,
      round: { id: updated.id, status: updated.status },
    }
  } catch (error) {
    console.error('[RoundEngine] activateRound failed:', error)
    return {
      success: false,
      errors: [error instanceof Error ? error.message : 'Unknown error during round activation'],
    }
  }
}

/**
 * Close a round: ROUND_ACTIVE → ROUND_CLOSED
 * Guards: all submission windows closed (if submission/mentoring round)
 * Side effects: expire all INTENT_PENDING for this round
 */
export async function closeRound(
  roundId: string,
  actorId: string,
  prisma: PrismaClient | any,
): Promise<RoundTransitionResult> {
  try {
    const round = await prisma.round.findUnique({
      where: { id: roundId },
      include: { submissionWindow: true },
    })

    if (!round) {
      return { success: false, errors: [`Round ${roundId} not found`] }
    }

    if (round.status !== 'ROUND_ACTIVE') {
      return {
        success: false,
        errors: [`Cannot close round: current status is ${round.status}, expected ROUND_ACTIVE`],
      }
    }

    // Guard: submission window must be closed/locked for submission/mentoring rounds
    if (
      (round.roundType === 'SUBMISSION' || round.roundType === 'MENTORING') &&
      round.submissionWindow
    ) {
      const sw = round.submissionWindow
      if (sw.windowCloseAt && new Date() < sw.windowCloseAt && !sw.isLocked) {
        return {
          success: false,
          errors: ['Cannot close round: linked submission window is still open'],
        }
      }
    }

    const updated = await prisma.$transaction(async (tx: any) => {
      const result = await tx.round.update({
        where: { id: roundId },
        data: { status: 'ROUND_CLOSED' },
      })

      // Expire pending intents
      await expireIntentsForRound(roundId, actorId)

      await tx.decisionAuditLog.create({
        data: {
          eventType: 'round.closed',
          entityType: 'Round',
          entityId: roundId,
          actorId,
          detailsJson: {
            roundName: round.name,
            roundType: round.roundType,
            previousStatus: 'ROUND_ACTIVE',
          },
          snapshotJson: {
            timestamp: new Date().toISOString(),
            emittedBy: 'round-engine',
          },
        },
      })

      return result
    })

    // Audit log outside transaction to avoid FK violations poisoning the tx
    await logAudit({
      userId: actorId,
      action: 'ROUND_CLOSE',
      entityType: 'Round',
      entityId: roundId,
      detailsJson: { name: round.name, roundType: round.roundType },
    })

    return {
      success: true,
      round: { id: updated.id, status: updated.status },
    }
  } catch (error) {
    console.error('[RoundEngine] closeRound failed:', error)
    return {
      success: false,
      errors: [error instanceof Error ? error.message : 'Unknown error during round close'],
    }
  }
}

/**
 * Archive a round: ROUND_CLOSED → ROUND_ARCHIVED
 * No guards.
 */
export async function archiveRound(
  roundId: string,
  actorId: string,
  prisma: PrismaClient | any,
): Promise<RoundTransitionResult> {
  try {
    const round = await prisma.round.findUnique({ where: { id: roundId } })

    if (!round) {
      return { success: false, errors: [`Round ${roundId} not found`] }
    }

    if (round.status !== 'ROUND_CLOSED') {
      return {
        success: false,
        errors: [`Cannot archive round: current status is ${round.status}, expected ROUND_CLOSED`],
      }
    }

    const updated = await prisma.$transaction(async (tx: any) => {
      const result = await tx.round.update({
        where: { id: roundId },
        data: { status: 'ROUND_ARCHIVED' },
      })

      await tx.decisionAuditLog.create({
        data: {
          eventType: 'round.archived',
          entityType: 'Round',
          entityId: roundId,
          actorId,
          detailsJson: {
            roundName: round.name,
            previousStatus: 'ROUND_CLOSED',
          },
          snapshotJson: {
            timestamp: new Date().toISOString(),
            emittedBy: 'round-engine',
          },
        },
      })

      return result
    })

    // Audit log outside transaction to avoid FK violations poisoning the tx
    await logAudit({
      userId: actorId,
      action: 'ROUND_ARCHIVE',
      entityType: 'Round',
      entityId: roundId,
      detailsJson: { name: round.name },
    })

    return {
      success: true,
      round: { id: updated.id, status: updated.status },
    }
  } catch (error) {
    console.error('[RoundEngine] archiveRound failed:', error)
    return {
      success: false,
      errors: [error instanceof Error ? error.message : 'Unknown error during round archive'],
    }
  }
}

/**
 * Reopen a round: ROUND_CLOSED → ROUND_ACTIVE
 * Side effects: any subsequent rounds in the same competition that are
 * ROUND_ACTIVE will be paused (set to ROUND_CLOSED) to prevent two
 * active rounds overlapping.
 */
export async function reopenRound(
  roundId: string,
  actorId: string,
  prisma: PrismaClient | any,
): Promise<RoundTransitionResult & { pausedRounds?: string[] }> {
  try {
    const round = await prisma.round.findUnique({
      where: { id: roundId },
      include: { competition: true },
    })

    if (!round) {
      return { success: false, errors: [`Round ${roundId} not found`] }
    }

    if (round.status !== 'ROUND_CLOSED') {
      return {
        success: false,
        errors: [`Cannot reopen round: current status is ${round.status}, expected ROUND_CLOSED`],
      }
    }

    const result = await prisma.$transaction(async (tx: any) => {
      // Pause any subsequent active rounds in the same competition
      const subsequentActiveRounds = await tx.round.findMany({
        where: {
          competitionId: round.competitionId,
          sortOrder: { gt: round.sortOrder },
          status: 'ROUND_ACTIVE',
        },
        select: { id: true, name: true },
      })

      if (subsequentActiveRounds.length > 0) {
        await tx.round.updateMany({
          where: { id: { in: subsequentActiveRounds.map((r: any) => r.id) } },
          data: { status: 'ROUND_CLOSED' },
        })

        // Audit each paused round
        for (const paused of subsequentActiveRounds) {
          await tx.decisionAuditLog.create({
            data: {
              eventType: 'round.paused',
              entityType: 'Round',
              entityId: paused.id,
              actorId,
              detailsJson: {
                roundName: paused.name,
                reason: `Paused because prior round "${round.name}" was reopened`,
                previousStatus: 'ROUND_ACTIVE',
              },
              snapshotJson: {
                timestamp: new Date().toISOString(),
                emittedBy: 'round-engine',
              },
            },
          })
        }
      }

      // Reopen this round
      const updated = await tx.round.update({
        where: { id: roundId },
        data: { status: 'ROUND_ACTIVE' },
      })

      await tx.decisionAuditLog.create({
        data: {
          eventType: 'round.reopened',
          entityType: 'Round',
          entityId: roundId,
          actorId,
          detailsJson: {
            roundName: round.name,
            previousStatus: 'ROUND_CLOSED',
            pausedRounds: subsequentActiveRounds.map((r: any) => r.name),
          },
          snapshotJson: {
            timestamp: new Date().toISOString(),
            emittedBy: 'round-engine',
          },
        },
      })

      return {
        updated,
        pausedRounds: subsequentActiveRounds.map((r: any) => r.name),
      }
    })

    // Audit log outside transaction to avoid FK violations poisoning the tx
    await logAudit({
      userId: actorId,
      action: 'ROUND_REOPEN',
      entityType: 'Round',
      entityId: roundId,
      detailsJson: {
        name: round.name,
        pausedRounds: result.pausedRounds,
      },
    })

    return {
      success: true,
      round: { id: result.updated.id, status: result.updated.status },
      pausedRounds: result.pausedRounds,
    }
  } catch (error) {
    console.error('[RoundEngine] reopenRound failed:', error)
    return {
      success: false,
      errors: [error instanceof Error ? error.message : 'Unknown error during round reopen'],
    }
  }
}

// ─── Project-Level Transitions ──────────────────────────────────────────────

/**
 * Transition a project within a round.
 * Upserts ProjectRoundState: create if not exists, update if exists.
 * Validate: round must be ROUND_ACTIVE.
 * Dual audit trail (DecisionAuditLog + logAudit).
 */
export async function transitionProject(
  projectId: string,
  roundId: string,
  newState: ProjectRoundStateValue,
  actorId: string,
  prisma: PrismaClient | any,
): Promise<ProjectRoundTransitionResult> {
  try {
    const round = await prisma.round.findUnique({ where: { id: roundId } })

    if (!round) {
      return { success: false, errors: [`Round ${roundId} not found`] }
    }

    if (round.status !== 'ROUND_ACTIVE') {
      return {
        success: false,
        errors: [`Round is ${round.status}, must be ROUND_ACTIVE to transition projects`],
      }
    }

    // Verify project exists
    const project = await prisma.project.findUnique({ where: { id: projectId } })
    if (!project) {
      return { success: false, errors: [`Project ${projectId} not found`] }
    }

    const result = await prisma.$transaction(async (tx: any) => {
      const now = new Date()

      // Upsert ProjectRoundState
      const existing = await tx.projectRoundState.findUnique({
        where: { projectId_roundId: { projectId, roundId } },
      })

      let prs
      if (existing) {
        prs = await tx.projectRoundState.update({
          where: { id: existing.id },
          data: {
            state: newState,
            exitedAt: isTerminalState(newState) ? now : null,
          },
        })
      } else {
        prs = await tx.projectRoundState.create({
          data: {
            projectId,
            roundId,
            state: newState,
            enteredAt: now,
          },
        })
      }

      // Dual audit trail
      await tx.decisionAuditLog.create({
        data: {
          eventType: 'project_round.transitioned',
          entityType: 'ProjectRoundState',
          entityId: prs.id,
          actorId,
          detailsJson: {
            projectId,
            roundId,
            previousState: existing?.state ?? null,
            newState,
          } as Prisma.InputJsonValue,
          snapshotJson: {
            timestamp: now.toISOString(),
            emittedBy: 'round-engine',
          },
        },
      })

      return { prs, previousState: existing?.state ?? null }
    })

    // Audit log outside transaction to avoid FK violations poisoning the tx
    await logAudit({
      userId: actorId,
      action: 'PROJECT_ROUND_TRANSITION',
      entityType: 'ProjectRoundState',
      entityId: result.prs.id,
      detailsJson: { projectId, roundId, newState, previousState: result.previousState },
    })

    return {
      success: true,
      projectRoundState: {
        id: result.prs.id,
        projectId: result.prs.projectId,
        roundId: result.prs.roundId,
        state: result.prs.state,
      },
    }
  } catch (error) {
    console.error('[RoundEngine] transitionProject failed:', error)
    return {
      success: false,
      errors: [error instanceof Error ? error.message : 'Unknown error during project transition'],
    }
  }
}

/**
 * Batch transition projects in batches of BATCH_SIZE.
 * Each project is processed independently.
 */
export async function batchTransitionProjects(
  projectIds: string[],
  roundId: string,
  newState: ProjectRoundStateValue,
  actorId: string,
  prisma: PrismaClient | any,
): Promise<BatchProjectTransitionResult> {
  const succeeded: string[] = []
  const failed: Array<{ projectId: string; errors: string[] }> = []

  for (let i = 0; i < projectIds.length; i += BATCH_SIZE) {
    const batch = projectIds.slice(i, i + BATCH_SIZE)

    const batchPromises = batch.map(async (projectId) => {
      const result = await transitionProject(projectId, roundId, newState, actorId, prisma)

      if (result.success) {
        succeeded.push(projectId)
      } else {
        failed.push({
          projectId,
          errors: result.errors ?? ['Transition failed'],
        })
      }
    })

    await Promise.all(batchPromises)
  }

  return { succeeded, failed, total: projectIds.length }
}

// ─── Query Helpers ──────────────────────────────────────────────────────────

export async function getProjectRoundStates(
  roundId: string,
  prisma: PrismaClient | any,
) {
  return prisma.projectRoundState.findMany({
    where: { roundId },
    include: {
      project: {
        select: {
          id: true,
          title: true,
          teamName: true,
          competitionCategory: true,
          status: true,
        },
      },
    },
    orderBy: { enteredAt: 'desc' },
  })
}

export async function getProjectRoundState(
  projectId: string,
  roundId: string,
  prisma: PrismaClient | any,
) {
  return prisma.projectRoundState.findUnique({
    where: { projectId_roundId: { projectId, roundId } },
  })
}

// ─── Internals ──────────────────────────────────────────────────────────────

function isTerminalState(state: ProjectRoundStateValue): boolean {
  return ['PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'].includes(state)
}
