/**
 * Deliberation Service
 *
 * Full deliberation lifecycle: session management, voting, aggregation,
 * tie-breaking, and finalization.
 *
 * Session transitions: DELIB_OPEN → VOTING → TALLYING → DELIB_LOCKED
 *                                                     → RUNOFF → TALLYING (max 3 runoff rounds)
 */

import type {
  PrismaClient,
  DeliberationMode,
  DeliberationStatus,
  TieBreakMethod,
  CompetitionCategory,
  DeliberationParticipantStatus,
  Prisma,
} from '@prisma/client'
import { logAudit } from '@/server/utils/audit'
import { triggerInProgressOnActivity } from './round-engine'

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

export type SessionTransitionResult = {
  success: boolean
  session?: { id: string; status: DeliberationStatus }
  errors?: string[]
}

export type AggregationResult = {
  rankings: Array<{
    projectId: string
    rank: number
    voteCount: number
    score: number
  }>
  hasTies: boolean
  tiedProjectIds: string[]
}

const MAX_RUNOFF_ROUNDS = 3

// ─── Valid Transitions ──────────────────────────────────────────────────────

const VALID_SESSION_TRANSITIONS: Record<string, string[]> = {
  DELIB_OPEN: ['VOTING'],
  VOTING: ['TALLYING'],
  TALLYING: ['DELIB_LOCKED', 'RUNOFF'],
  RUNOFF: ['TALLYING'],
  DELIB_LOCKED: [],
}

// ─── Session Lifecycle ──────────────────────────────────────────────────────

/**
 * Create a new deliberation session with participants.
 */
export async function createSession(
  params: {
    competitionId: string
    roundId: string
    category: CompetitionCategory
    mode: DeliberationMode
    tieBreakMethod: TieBreakMethod
    showCollectiveRankings?: boolean
    showPriorJuryData?: boolean
    participantUserIds: string[] // JuryGroupMember IDs
  },
  prisma: PrismaClient,
) {
  return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
    const session = await tx.deliberationSession.create({
      data: {
        competitionId: params.competitionId,
        roundId: params.roundId,
        category: params.category,
        mode: params.mode,
        tieBreakMethod: params.tieBreakMethod,
        showCollectiveRankings: params.showCollectiveRankings ?? false,
        showPriorJuryData: params.showPriorJuryData ?? false,
        status: 'DELIB_OPEN',
      },
    })

    // Create participant records
    for (const userId of params.participantUserIds) {
      await tx.deliberationParticipant.create({
        data: {
          sessionId: session.id,
          userId,
          status: 'REQUIRED',
        },
      })
    }

    await tx.decisionAuditLog.create({
      data: {
        eventType: 'deliberation.created',
        entityType: 'DeliberationSession',
        entityId: session.id,
        actorId: null,
        detailsJson: {
          competitionId: params.competitionId,
          roundId: params.roundId,
          category: params.category,
          mode: params.mode,
          participantCount: params.participantUserIds.length,
        } as Prisma.InputJsonValue,
        snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
      },
    })

    return session
  })
}

/**
 * Open voting: DELIB_OPEN → VOTING
 */
export async function openVoting(
  sessionId: string,
  actorId: string,
  prisma: PrismaClient,
): Promise<SessionTransitionResult> {
  return transitionSession(sessionId, 'DELIB_OPEN', 'VOTING', actorId, prisma)
}

/**
 * Close voting: VOTING → TALLYING
 * Triggers vote aggregation.
 */
export async function closeVoting(
  sessionId: string,
  actorId: string,
  prisma: PrismaClient,
): Promise<SessionTransitionResult> {
  return transitionSession(sessionId, 'VOTING', 'TALLYING', actorId, prisma)
}

// ─── Vote Submission ────────────────────────────────────────────────────────

/**
 * Submit a vote in a deliberation session.
 * Validates: session is VOTING (or RUNOFF), juryMember is active participant.
 */
export async function submitVote(
  params: {
    sessionId: string
    juryMemberId: string // JuryGroupMember ID
    projectId: string
    rank?: number
    isWinnerPick?: boolean
    runoffRound?: number
  },
  prisma: PrismaClient,
) {
  const session = await prisma.deliberationSession.findUnique({
    where: { id: params.sessionId },
  })

  if (!session) {
    throw new Error('Deliberation session not found')
  }

  if (session.status !== 'VOTING' && session.status !== 'RUNOFF') {
    throw new Error(`Cannot vote: session status is ${session.status}`)
  }

  // Verify participant is active
  const participant = await prisma.deliberationParticipant.findUnique({
    where: {
      sessionId_userId: {
        sessionId: params.sessionId,
        userId: params.juryMemberId,
      },
    },
  })

  if (!participant) {
    throw new Error('Juror is not a participant in this deliberation')
  }

  if (participant.status !== 'REQUIRED' && participant.status !== 'REPLACEMENT_ACTIVE') {
    throw new Error(`Participant status ${participant.status} does not allow voting`)
  }

  const runoffRound = params.runoffRound ?? 0

  const vote = await prisma.deliberationVote.upsert({
    where: {
      sessionId_juryMemberId_projectId_runoffRound: {
        sessionId: params.sessionId,
        juryMemberId: params.juryMemberId,
        projectId: params.projectId,
        runoffRound,
      },
    },
    create: {
      sessionId: params.sessionId,
      juryMemberId: params.juryMemberId,
      projectId: params.projectId,
      rank: params.rank,
      isWinnerPick: params.isWinnerPick ?? false,
      runoffRound,
    },
    update: {
      rank: params.rank,
      isWinnerPick: params.isWinnerPick ?? false,
    },
  })

  // Auto-transition: mark project IN_PROGRESS in the deliberation round
  try {
    if (session.roundId) {
      await triggerInProgressOnActivity(params.projectId, session.roundId, params.juryMemberId, prisma)
    }
  } catch (e) {
    console.error('[Deliberation] triggerInProgressOnActivity failed (non-fatal):', e)
  }

  return vote
}

// ─── Aggregation ────────────────────────────────────────────────────────────

/**
 * Aggregate votes for a session.
 * - SINGLE_WINNER_VOTE: count isWinnerPick=true per project
 * - FULL_RANKING: Borda count (N points for rank 1, N-1 for rank 2, etc.)
 */
export async function aggregateVotes(
  sessionId: string,
  prisma: PrismaClient,
): Promise<AggregationResult> {
  const session = await prisma.deliberationSession.findUnique({
    where: { id: sessionId },
  })

  if (!session) {
    throw new Error('Deliberation session not found')
  }

  // Get the latest runoff round
  const latestVote = await prisma.deliberationVote.findFirst({
    where: { sessionId },
    orderBy: { runoffRound: 'desc' },
    select: { runoffRound: true },
  })
  const currentRound = latestVote?.runoffRound ?? 0

  const votes = await prisma.deliberationVote.findMany({
    where: { sessionId, runoffRound: currentRound },
  })

  const projectScores = new Map<string, number>()
  const projectVoteCounts = new Map<string, number>()

  if (session.mode === 'SINGLE_WINNER_VOTE') {
    // Count isWinnerPick=true per project
    for (const vote of votes) {
      if (vote.isWinnerPick) {
        projectScores.set(vote.projectId, (projectScores.get(vote.projectId) ?? 0) + 1)
        projectVoteCounts.set(vote.projectId, (projectVoteCounts.get(vote.projectId) ?? 0) + 1)
      }
    }
  } else {
    // FULL_RANKING: Borda count
    // First, find N = total unique projects being ranked
    const uniqueProjects = new Set(votes.map((v: any) => v.projectId))
    const n = uniqueProjects.size

    for (const vote of votes) {
      if (vote.rank != null) {
        // Borda: rank 1 gets N points, rank 2 gets N-1, etc.
        const score = Math.max(0, n + 1 - vote.rank)
        projectScores.set(vote.projectId, (projectScores.get(vote.projectId) ?? 0) + score)
        projectVoteCounts.set(vote.projectId, (projectVoteCounts.get(vote.projectId) ?? 0) + 1)
      }
    }
  }

  // Sort by score descending
  const sorted = [...projectScores.entries()]
    .sort(([, a], [, b]) => b - a)
    .map(([projectId, score], index) => ({
      projectId,
      rank: index + 1,
      voteCount: projectVoteCounts.get(projectId) ?? 0,
      score,
    }))

  // Detect ties: projects with same score get same rank
  const rankings: typeof sorted = []
  let currentRank = 1
  for (let i = 0; i < sorted.length; i++) {
    if (i > 0 && sorted[i].score === sorted[i - 1].score) {
      rankings.push({ ...sorted[i], rank: rankings[i - 1].rank })
    } else {
      rankings.push({ ...sorted[i], rank: currentRank })
    }
    currentRank = rankings[i].rank + 1
  }

  // Find tied projects (projects sharing rank 1, or if no clear winner)
  const topScore = rankings.length > 0 ? rankings[0].score : 0
  const tiedProjectIds = rankings.filter((r) => r.score === topScore && topScore > 0).length > 1
    ? rankings.filter((r) => r.score === topScore).map((r) => r.projectId)
    : []

  return {
    rankings,
    hasTies: tiedProjectIds.length > 1,
    tiedProjectIds,
  }
}

// ─── Tie-Breaking ───────────────────────────────────────────────────────────

/**
 * Initiate a runoff vote for tied projects.
 * TALLYING → RUNOFF
 */
export async function initRunoff(
  sessionId: string,
  tiedProjectIds: string[],
  actorId: string,
  prisma: PrismaClient,
): Promise<SessionTransitionResult> {
  const session = await prisma.deliberationSession.findUnique({
    where: { id: sessionId },
  })

  if (!session) {
    return { success: false, errors: ['Session not found'] }
  }

  if (session.status !== 'TALLYING') {
    return { success: false, errors: [`Cannot init runoff: status is ${session.status}`] }
  }

  // Check max runoff rounds
  const latestVote = await prisma.deliberationVote.findFirst({
    where: { sessionId },
    orderBy: { runoffRound: 'desc' },
    select: { runoffRound: true },
  })

  const nextRound = (latestVote?.runoffRound ?? 0) + 1
  if (nextRound > MAX_RUNOFF_ROUNDS) {
    return { success: false, errors: [`Maximum runoff rounds (${MAX_RUNOFF_ROUNDS}) exceeded`] }
  }

  return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
    const updated = await tx.deliberationSession.update({
      where: { id: sessionId },
      data: { status: 'RUNOFF' },
    })

    await tx.decisionAuditLog.create({
      data: {
        eventType: 'deliberation.runoff_initiated',
        entityType: 'DeliberationSession',
        entityId: sessionId,
        actorId,
        detailsJson: {
          runoffRound: nextRound,
          tiedProjectIds,
        } as Prisma.InputJsonValue,
        snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
      },
    })

    return {
      success: true,
      session: { id: updated.id, status: updated.status },
    }
  })
}

/**
 * Admin override: directly set final rankings.
 */
export async function adminDecide(
  sessionId: string,
  rankings: Array<{ projectId: string; rank: number }>,
  reason: string,
  actorId: string,
  prisma: PrismaClient,
): Promise<SessionTransitionResult> {
  const session = await prisma.deliberationSession.findUnique({
    where: { id: sessionId },
  })

  if (!session) {
    return { success: false, errors: ['Session not found'] }
  }

  if (session.status !== 'TALLYING') {
    return { success: false, errors: [`Cannot admin-decide: status is ${session.status}`] }
  }

  return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
    const updated = await tx.deliberationSession.update({
      where: { id: sessionId },
      data: {
        adminOverrideResult: {
          rankings,
          reason,
          decidedBy: actorId,
          decidedAt: new Date().toISOString(),
        } as Prisma.InputJsonValue,
      },
    })

    await tx.decisionAuditLog.create({
      data: {
        eventType: 'deliberation.admin_override',
        entityType: 'DeliberationSession',
        entityId: sessionId,
        actorId,
        detailsJson: {
          rankings,
          reason,
        } as Prisma.InputJsonValue,
        snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
      },
    })

    return {
      success: true,
      session: { id: updated.id, status: updated.status },
    }
  })
}

// ─── Finalization ───────────────────────────────────────────────────────────

/**
 * Finalize deliberation results: TALLYING → DELIB_LOCKED
 * Creates DeliberationResult records.
 */
export async function finalizeResults(
  sessionId: string,
  actorId: string,
  prisma: PrismaClient,
): Promise<SessionTransitionResult> {
  const session = await prisma.deliberationSession.findUnique({
    where: { id: sessionId },
  })

  if (!session) {
    return { success: false, errors: ['Session not found'] }
  }

  if (session.status !== 'TALLYING') {
    return { success: false, errors: [`Cannot finalize: status is ${session.status}`] }
  }

  // If admin override exists, use those rankings
  const override = session.adminOverrideResult as {
    rankings: Array<{ projectId: string; rank: number }>
  } | null

  let finalRankings: Array<{ projectId: string; rank: number; voteCount: number; isAdminOverridden: boolean }>

  if (override?.rankings) {
    finalRankings = override.rankings.map((r) => ({
      projectId: r.projectId,
      rank: r.rank,
      voteCount: 0,
      isAdminOverridden: true,
    }))
  } else {
    // Use aggregated votes
    const agg = await aggregateVotes(sessionId, prisma)
    finalRankings = agg.rankings.map((r) => ({
      projectId: r.projectId,
      rank: r.rank,
      voteCount: r.voteCount,
      isAdminOverridden: false,
    }))
  }

  return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
    // Create result records
    for (const ranking of finalRankings) {
      await tx.deliberationResult.upsert({
        where: {
          sessionId_projectId: {
            sessionId,
            projectId: ranking.projectId,
          },
        },
        create: {
          sessionId,
          projectId: ranking.projectId,
          finalRank: ranking.rank,
          voteCount: ranking.voteCount,
          isAdminOverridden: ranking.isAdminOverridden,
          overrideReason: ranking.isAdminOverridden
            ? (session.adminOverrideResult as Record<string, unknown> | null)?.reason as string ?? null
            : null,
        },
        update: {
          finalRank: ranking.rank,
          voteCount: ranking.voteCount,
          isAdminOverridden: ranking.isAdminOverridden,
        },
      })
    }

    // Transition to DELIB_LOCKED
    const updated = await tx.deliberationSession.update({
      where: { id: sessionId },
      data: { status: 'DELIB_LOCKED' },
    })

    await tx.decisionAuditLog.create({
      data: {
        eventType: 'deliberation.finalized',
        entityType: 'DeliberationSession',
        entityId: sessionId,
        actorId,
        detailsJson: {
          resultCount: finalRankings.length,
          isAdminOverride: finalRankings.some((r) => r.isAdminOverridden),
        } as Prisma.InputJsonValue,
        snapshotJson: {
          timestamp: new Date().toISOString(),
          emittedBy: 'deliberation',
          rankings: finalRankings,
        },
      },
    })

    await logAudit({
      prisma: tx,
      userId: actorId,
      action: 'DELIBERATION_FINALIZE',
      entityType: 'DeliberationSession',
      entityId: sessionId,
      detailsJson: { resultCount: finalRankings.length },
    })

    return {
      success: true,
      session: { id: updated.id, status: updated.status },
    }
  })
}

// ─── Participant Management ─────────────────────────────────────────────────

/**
 * Update a participant's status (e.g. mark absent, replace).
 */
export async function updateParticipantStatus(
  sessionId: string,
  userId: string,
  status: DeliberationParticipantStatus,
  replacedById?: string,
  actorId?: string,
  prisma?: PrismaClient,
) {
  const db = prisma ?? (await import('@/lib/prisma')).prisma

  return db.$transaction(async (tx: Prisma.TransactionClient) => {
    const updated = await tx.deliberationParticipant.update({
      where: { sessionId_userId: { sessionId, userId } },
      data: {
        status,
        replacedById: replacedById ?? null,
      },
    })

    // If replacing, create participant record for replacement
    if (status === 'REPLACED' && replacedById) {
      await tx.deliberationParticipant.upsert({
        where: { sessionId_userId: { sessionId, userId: replacedById } },
        create: {
          sessionId,
          userId: replacedById,
          status: 'REPLACEMENT_ACTIVE',
        },
        update: {
          status: 'REPLACEMENT_ACTIVE',
        },
      })
    }

    if (actorId) {
      await tx.decisionAuditLog.create({
        data: {
          eventType: 'deliberation.participant_updated',
          entityType: 'DeliberationParticipant',
          entityId: updated.id,
          actorId,
          detailsJson: { userId, newStatus: status, replacedById } as Prisma.InputJsonValue,
          snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
        },
      })
    }

    return updated
  })
}

// ─── Queries ────────────────────────────────────────────────────────────────

/**
 * Get a deliberation session with votes, results, and participants.
 */
export async function getSessionWithVotes(
  sessionId: string,
  prisma: PrismaClient,
) {
  return prisma.deliberationSession.findUnique({
    where: { id: sessionId },
    include: {
      votes: {
        include: {
          project: { select: { id: true, title: true, teamName: true } },
          juryMember: {
            include: {
              user: { select: { id: true, name: true, email: true } },
            },
          },
        },
        orderBy: [{ runoffRound: 'desc' }, { rank: 'asc' }],
      },
      results: {
        include: {
          project: { select: { id: true, title: true, teamName: true } },
        },
        orderBy: { finalRank: 'asc' },
      },
      participants: {
        include: {
          user: {
            include: {
              user: { select: { id: true, name: true, email: true } },
            },
          },
        },
      },
      competition: { select: { id: true, name: true } },
      round: { select: { id: true, name: true, roundType: true } },
    },
  })
}

// ─── Internal Helpers ───────────────────────────────────────────────────────

async function transitionSession(
  sessionId: string,
  expectedStatus: DeliberationStatus,
  newStatus: DeliberationStatus,
  actorId: string,
  prisma: PrismaClient,
): Promise<SessionTransitionResult> {
  try {
    const session = await prisma.deliberationSession.findUnique({
      where: { id: sessionId },
    })

    if (!session) {
      return { success: false, errors: ['Session not found'] }
    }

    if (session.status !== expectedStatus) {
      return {
        success: false,
        errors: [`Cannot transition: status is ${session.status}, expected ${expectedStatus}`],
      }
    }

    const valid = VALID_SESSION_TRANSITIONS[expectedStatus] ?? []
    if (!valid.includes(newStatus)) {
      return {
        success: false,
        errors: [`Invalid transition: ${expectedStatus} → ${newStatus}`],
      }
    }

    const updated = await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
      const result = await tx.deliberationSession.update({
        where: { id: sessionId },
        data: { status: newStatus },
      })

      await tx.decisionAuditLog.create({
        data: {
          eventType: `deliberation.${newStatus.toLowerCase()}`,
          entityType: 'DeliberationSession',
          entityId: sessionId,
          actorId,
          detailsJson: {
            previousStatus: expectedStatus,
            newStatus,
          } as Prisma.InputJsonValue,
          snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'deliberation' },
        },
      })

      await logAudit({
        prisma: tx,
        userId: actorId,
        action: `DELIBERATION_${newStatus}`,
        entityType: 'DeliberationSession',
        entityId: sessionId,
      })

      return result
    })

    return {
      success: true,
      session: { id: updated.id, status: updated.status },
    }
  } catch (error) {
    console.error('[Deliberation] Session transition failed:', error)
    return {
      success: false,
      errors: [error instanceof Error ? error.message : 'Unknown error'],
    }
  }
}
