import { randomUUID } from 'node:crypto';

import { Injectable } from '@nestjs/common';

import type { StartQueueResponse } from '@hexahost/contracts';
import type { GameServer } from '@hexahost/database';
import { canStartOnNode } from '@hexahost/scheduler';

import { PrismaService } from '../prisma/prisma.service';

import { loadNodeSnapshot } from './node-snapshots';

const START_QUEUE_EXPIRY_MS = 30 * 60 * 1000;

@Injectable()
export class SchedulingService {
  constructor(private readonly prisma: PrismaService) {}

  async canStartServer(server: GameServer): Promise<boolean> {
    if (!server.nodeId) {
      return false;
    }

    const snapshot = await loadNodeSnapshot(server.nodeId);
    return snapshot ? canStartOnNode(snapshot, server.ramMb) : false;
  }

  async enqueueStart(
    server: GameServer,
    actorId: string,
    correlationId: string,
  ): Promise<{ server: GameServer; queuePosition: number }> {
    const fullServer = server.planId
      ? server
      : await this.prisma.gameServer.findUniqueOrThrow({
          where: { id: server.id },
        });

    const plan = fullServer.planId
      ? await this.prisma.plan.findUnique({ where: { id: fullServer.planId } })
      : null;

    const expiresAt = new Date(Date.now() + START_QUEUE_EXPIRY_MS);
    const priority = plan?.queuePriority ?? 0;

    const updated = await this.prisma.$transaction(async (tx) => {
      await tx.serverStartQueueEntry.upsert({
        where: { serverId: server.id },
        create: {
          serverId: server.id,
          nodeId: server.nodeId!,
          status: 'QUEUED',
          priority,
          correlationId,
          expiresAt,
        },
        update: {
          status: 'QUEUED',
          priority,
          correlationId,
          expiresAt,
          requestedAt: new Date(),
        },
      });

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

      await tx.gameServerStateTransition.create({
        data: {
          gameServerId: server.id,
          fromStatus: server.status,
          toStatus: 'QUEUED',
          reason: 'Start queued due to insufficient node capacity',
          actorId,
          correlationId,
        },
      });

      return next;
    });

    const queuePosition = await this.getQueuePosition(server.id);

    return { server: updated, queuePosition };
  }

  async getQueuePosition(serverId: string): Promise<number> {
    const entry = await this.prisma.serverStartQueueEntry.findUnique({
      where: { serverId },
    });

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

    const ahead = await this.prisma.serverStartQueueEntry.count({
      where: {
        status: 'QUEUED',
        OR: [
          { priority: { gt: entry.priority } },
          {
            priority: entry.priority,
            requestedAt: { lt: entry.requestedAt },
          },
        ],
      },
    });

    return ahead + 1;
  }

  async getStartQueue(userId: string, serverId: string): Promise<StartQueueResponse> {
    const server = await this.prisma.gameServer.findFirst({
      where: { id: serverId, userId },
    });

    if (!server) {
      return { entry: null };
    }

    const entry = await this.prisma.serverStartQueueEntry.findUnique({
      where: { serverId },
    });

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

    const position = await this.getQueuePosition(serverId);

    return {
      entry: {
        serverId: entry.serverId,
        nodeId: entry.nodeId,
        status: entry.status,
        priority: entry.priority,
        position,
        requestedAt: entry.requestedAt.toISOString(),
        expiresAt: entry.expiresAt?.toISOString() ?? null,
      },
    };
  }

  async cancelQueuedStart(userId: string, serverId: string): Promise<GameServer> {
    const server = await this.prisma.gameServer.findFirst({
      where: { id: serverId, userId },
    });

    if (!server) {
      throw new Error('Server not found');
    }

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

    return this.prisma.$transaction(async (tx) => {
      await tx.serverStartQueueEntry.updateMany({
        where: { serverId, status: 'QUEUED' },
        data: { status: 'CANCELLED' },
      });

      const updated = 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: 'Queued start cancelled by user',
          actorId: userId,
          correlationId: randomUUID(),
        },
      });

      return updated;
    });
  }
}
