import { TRPCError } from '@trpc/server'
import type { AssignmentIntent, AssignmentIntentSource, Prisma } from '@prisma/client'
import { prisma } from '@/lib/prisma'

// ============================================================================
// Create Intent
// ============================================================================

/**
 * Creates an assignment intent (pre-assignment signal).
 * Enforces uniqueness: one intent per (member, round, project).
 */
export async function createIntent(params: {
  juryGroupMemberId: string
  roundId: string
  projectId: string
  source: AssignmentIntentSource
  actorId?: string
}): Promise<AssignmentIntent> {
  const { juryGroupMemberId, roundId, projectId, source, actorId } = params

  const intent = await prisma.$transaction(async (tx) => {
    // Check for existing pending intent
    const existing = await tx.assignmentIntent.findUnique({
      where: {
        juryGroupMemberId_roundId_projectId: {
          juryGroupMemberId,
          roundId,
          projectId,
        },
      },
    })

    if (existing) {
      if (existing.status === 'INTENT_PENDING') {
        throw new TRPCError({
          code: 'CONFLICT',
          message: 'A pending intent already exists for this member/round/project',
        })
      }
      // If previous intent was terminal (HONORED, OVERRIDDEN, EXPIRED, CANCELLED),
      // allow creating a new one by updating it back to PENDING
      const updated = await tx.assignmentIntent.update({
        where: { id: existing.id },
        data: { status: 'INTENT_PENDING', source },
      })

      await logIntentEvent(tx, 'intent.recreated', updated, actorId, {
        previousStatus: existing.status,
        source,
      })

      return updated
    }

    const created = await tx.assignmentIntent.create({
      data: {
        juryGroupMemberId,
        roundId,
        projectId,
        source,
        status: 'INTENT_PENDING',
      },
    })

    await logIntentEvent(tx, 'intent.created', created, actorId, { source })

    return created
  })

  return intent
}

// ============================================================================
// Honor Intent (PENDING → HONORED)
// ============================================================================

/**
 * Marks an intent as HONORED when the corresponding assignment is created.
 * Only INTENT_PENDING intents can be honored.
 */
export async function honorIntent(
  intentId: string,
  assignmentId: string,
  actorId?: string,
): Promise<AssignmentIntent> {
  return prisma.$transaction(async (tx) => {
    const intent = await tx.assignmentIntent.findUniqueOrThrow({
      where: { id: intentId },
    })

    assertPending(intent)

    const updated = await tx.assignmentIntent.update({
      where: { id: intentId },
      data: { status: 'HONORED' },
    })

    await logIntentEvent(tx, 'intent.honored', updated, actorId, {
      assignmentId,
    })

    return updated
  })
}

// ============================================================================
// Override Intent (PENDING → OVERRIDDEN)
// ============================================================================

/**
 * Marks an intent as OVERRIDDEN when an admin overrides the pre-assignment.
 * Only INTENT_PENDING intents can be overridden.
 */
export async function overrideIntent(
  intentId: string,
  reason: string,
  actorId?: string,
): Promise<AssignmentIntent> {
  return prisma.$transaction(async (tx) => {
    const intent = await tx.assignmentIntent.findUniqueOrThrow({
      where: { id: intentId },
    })

    assertPending(intent)

    const updated = await tx.assignmentIntent.update({
      where: { id: intentId },
      data: { status: 'OVERRIDDEN' },
    })

    await logIntentEvent(tx, 'intent.overridden', updated, actorId, { reason })

    return updated
  })
}

// ============================================================================
// Cancel Intent (PENDING → CANCELLED)
// ============================================================================

/**
 * Marks an intent as CANCELLED (e.g. admin removes it before it is honored).
 * Only INTENT_PENDING intents can be cancelled.
 */
export async function cancelIntent(
  intentId: string,
  reason: string,
  actorId?: string,
): Promise<AssignmentIntent> {
  return prisma.$transaction(async (tx) => {
    const intent = await tx.assignmentIntent.findUniqueOrThrow({
      where: { id: intentId },
    })

    assertPending(intent)

    const updated = await tx.assignmentIntent.update({
      where: { id: intentId },
      data: { status: 'CANCELLED' },
    })

    await logIntentEvent(tx, 'intent.cancelled', updated, actorId, { reason })

    return updated
  })
}

// ============================================================================
// Expire Intents for Round (batch PENDING → EXPIRED)
// ============================================================================

/**
 * Expires all INTENT_PENDING intents for a given round.
 * Typically called when a round transitions past the assignment phase.
 */
export async function expireIntentsForRound(
  roundId: string,
  actorId?: string,
  txClient?: Prisma.TransactionClient,
): Promise<{ expired: number }> {
  const run = async (tx: Prisma.TransactionClient) => {
    const pending = await tx.assignmentIntent.findMany({
      where: { roundId, status: 'INTENT_PENDING' },
    })

    if (pending.length === 0) return { expired: 0 }

    await tx.assignmentIntent.updateMany({
      where: { roundId, status: 'INTENT_PENDING' },
      data: { status: 'EXPIRED' },
    })

    await tx.decisionAuditLog.create({
      data: {
        eventType: 'intent.batch_expired',
        entityType: 'Round',
        entityId: roundId,
        actorId: actorId ?? null,
        detailsJson: {
          expiredCount: pending.length,
          intentIds: pending.map((i) => i.id),
        } as Prisma.InputJsonValue,
        snapshotJson: {
          timestamp: new Date().toISOString(),
          emittedBy: 'assignment-intent',
        },
      },
    })

    return { expired: pending.length }
  }

  // If a transaction client was provided, use it directly; otherwise open a new one
  if (txClient) {
    return run(txClient)
  }
  return prisma.$transaction(run)
}

// ============================================================================
// Query Helpers
// ============================================================================

export async function getPendingIntentsForRound(
  roundId: string,
): Promise<AssignmentIntent[]> {
  return prisma.assignmentIntent.findMany({
    where: { roundId, status: 'INTENT_PENDING' },
    orderBy: { createdAt: 'asc' },
  })
}

export async function getPendingIntentsForMember(
  juryGroupMemberId: string,
  roundId: string,
): Promise<AssignmentIntent[]> {
  return prisma.assignmentIntent.findMany({
    where: {
      juryGroupMemberId,
      roundId,
      status: 'INTENT_PENDING',
    },
    orderBy: { createdAt: 'asc' },
  })
}

export async function getIntentsForRound(
  roundId: string,
): Promise<AssignmentIntent[]> {
  return prisma.assignmentIntent.findMany({
    where: { roundId },
    orderBy: { createdAt: 'asc' },
  })
}

// ============================================================================
// Internals
// ============================================================================

function assertPending(intent: AssignmentIntent): void {
  if (intent.status !== 'INTENT_PENDING') {
    throw new TRPCError({
      code: 'BAD_REQUEST',
      message: `Intent ${intent.id} is ${intent.status}, only INTENT_PENDING intents can transition`,
    })
  }
}

async function logIntentEvent(
  tx: Prisma.TransactionClient,
  eventType: string,
  intent: AssignmentIntent,
  actorId: string | undefined,
  details: Record<string, unknown>,
): Promise<void> {
  await tx.decisionAuditLog.create({
    data: {
      eventType,
      entityType: 'AssignmentIntent',
      entityId: intent.id,
      actorId: actorId ?? null,
      detailsJson: {
        juryGroupMemberId: intent.juryGroupMemberId,
        roundId: intent.roundId,
        projectId: intent.projectId,
        status: intent.status,
        source: intent.source,
        ...details,
      } as Prisma.InputJsonValue,
      snapshotJson: {
        timestamp: new Date().toISOString(),
        emittedBy: 'assignment-intent',
      },
    },
  })
}
