import { randomUUID } from 'node:crypto';

import { prisma } from '@hexahost/database';
import { canStartOnNode } from '@hexahost/scheduler';
import { Queue } from 'bullmq';

import { logger } from '../logger';
import { loadNodeSnapshot } from '../node-snapshots';
import { transitionServerStatus } from '../server-state';
import { QUEUE_SERVER_LIFECYCLE } from '../queues';
import { getRedisPublisher } from '../redis';

const START_QUEUE_EXPIRY_MS = 30 * 60 * 1000;

let lifecycleQueue: Queue | null = null;

function getLifecycleQueue(): Queue {
  if (!lifecycleQueue) {
    lifecycleQueue = new Queue(QUEUE_SERVER_LIFECYCLE, {
      connection: getRedisPublisher(),
    });
  }

  return lifecycleQueue;
}

export async function processStartQueueTick(): Promise<void> {
  const entries = await prisma.serverStartQueueEntry.findMany({
    where: { status: 'QUEUED' },
    orderBy: [{ priority: 'desc' }, { requestedAt: 'asc' }],
    include: {
      server: {
        include: { plan: true },
      },
    },
    take: 25,
  });

  if (entries.length === 0) {
    return;
  }

  const now = Date.now();

  for (const entry of entries) {
    if (entry.expiresAt && entry.expiresAt.getTime() < now) {
      await expireQueueEntry(entry.serverId);
      continue;
    }

    const requiredRamMb = entry.server.plan?.maxRamMb ?? entry.server.ramMb;
    const nodeSnapshot = await loadNodeSnapshot(entry.nodeId);

    if (!nodeSnapshot || !canStartOnNode(nodeSnapshot, requiredRamMb)) {
      continue;
    }

    const correlationId = entry.correlationId ?? randomUUID();

    await prisma.$transaction(async (tx) => {
      const current = await tx.serverStartQueueEntry.findUnique({
        where: { id: entry.id },
      });

      if (!current || current.status !== 'QUEUED') {
        return;
      }

      await tx.serverStartQueueEntry.update({
        where: { id: entry.id },
        data: { status: 'DISPATCHED', correlationId },
      });
    });

    await getLifecycleQueue().add(
      'start-server',
      { serverId: entry.serverId, correlationId },
      { jobId: correlationId },
    );

    logger.info(
      { serverId: entry.serverId, nodeId: entry.nodeId },
      'Dispatched queued server start',
    );
  }
}

async function expireQueueEntry(serverId: string): Promise<void> {
  await prisma.$transaction(async (tx) => {
    const entry = await tx.serverStartQueueEntry.findUnique({
      where: { serverId },
    });

    if (!entry || entry.status !== 'QUEUED') {
      return;
    }

    await tx.serverStartQueueEntry.update({
      where: { serverId },
      data: { status: 'EXPIRED' },
    });

    const server = await tx.gameServer.findUnique({ where: { id: serverId } });
    if (server?.status === 'QUEUED') {
      await tx.gameServer.update({
        where: { id: serverId },
        data: { status: 'STOPPED', version: { increment: 1 } },
      });

      await tx.gameServerStateTransition.create({
        data: {
          gameServerId: serverId,
          fromStatus: 'QUEUED',
          toStatus: 'STOPPED',
          reason: 'Start queue entry expired',
        },
      });
    }
  });

  logger.info({ serverId }, 'Expired queued start request');
}

export function defaultQueueExpiry(): Date {
  return new Date(Date.now() + START_QUEUE_EXPIRY_MS);
}
