import { randomUUID } from 'node:crypto';

import { prisma } from '@hexahost/database';
import { assignJoinAddress } from '@hexahost/dns';
import { selectNodeForAllocation } from '@hexahost/scheduler';
import { Job } from 'bullmq';
import { z } from 'zod';

import { releaseServerAllocation } from '../allocation';
import { logger } from '../logger';
import { loadNodeSnapshots } from '../node-snapshots';
import { publishNodeCommand, waitForNodeResponse } from '../redis';
import { transitionServerStatus } from '../server-state';

const PROVISION_TIMEOUT_MS = 5 * 60 * 1000;

export const provisionServerJobSchema = z.object({
  serverId: z.string().uuid(),
});

function buildDataPath(serverId: string): string {
  return `/var/lib/hgc/servers/${serverId}`;
}

async function findAvailableHostPort(nodeId: string): Promise<number> {
  const node = await prisma.gameNode.findUniqueOrThrow({
    where: { id: nodeId },
    select: { portRangeStart: true, portRangeEnd: true },
  });

  const [allocations, servers] = await Promise.all([
    prisma.gameServerAllocation.findMany({
      where: { nodeId, status: 'ACTIVE' },
      select: { hostPort: true },
    }),
    prisma.gameServer.findMany({
      where: { nodeId, hostPort: { not: null } },
      select: { hostPort: true },
    }),
  ]);

  const usedPorts = new Set<number>();
  for (const allocation of allocations) {
    usedPorts.add(allocation.hostPort);
  }
  for (const server of servers) {
    if (server.hostPort !== null) {
      usedPorts.add(server.hostPort);
    }
  }

  let port = node.portRangeStart;
  while (usedPorts.has(port)) {
    port += 1;
    if (port > node.portRangeEnd) {
      throw new Error(`No free host port in range ${node.portRangeStart}-${node.portRangeEnd}`);
    }
  }

  return port;
}

export async function processProvisionServerJob(job: Job): Promise<{ status: string }> {
  if (job.name !== 'provision-server') {
    logger.warn({ jobName: job.name }, 'Unknown provisioning job');
    return { status: 'ignored' };
  }

  const { serverId } = provisionServerJobSchema.parse(job.data);
  const server = await prisma.gameServer.findUnique({
    where: { id: serverId },
    include: { plan: true },
  });

  if (!server) {
    throw new Error(`Server not found: ${serverId}`);
  }

  const nodeSnapshots = await loadNodeSnapshots();
  const ramMb = server.plan?.maxRamMb ?? server.ramMb;
  const node = selectNodeForAllocation(nodeSnapshots, ramMb);

  if (!node) {
    await transitionServerStatus(serverId, 'ERROR', {
      reason: 'No online game node available',
      errorCode: 'NO_NODE_AVAILABLE',
      errorMessage: 'No online game node is available for provisioning',
    });
    throw new Error('No online game node available');
  }

  const hostPort = await findAvailableHostPort(node.id);
  const dataPath = buildDataPath(serverId);
  const messageId = randomUUID();

  await prisma.$transaction(async (tx) => {
    await tx.gameServerAllocation.create({
      data: {
        serverId,
        nodeId: node.id,
        hostPort,
        ramMbReserved: ramMb,
        status: 'ACTIVE',
      },
    });

    await tx.gameNode.update({
      where: { id: node.id },
      data: { activeServers: { increment: 1 } },
    });

    await tx.gameServer.update({
      where: { id: serverId },
      data: {
        nodeId: node.id,
        hostPort,
        ramMb,
        dataPath,
        status: 'PROVISIONING',
      },
    });

    await tx.gameServerStateTransition.create({
      data: {
        gameServerId: serverId,
        fromStatus: server.status,
        toStatus: 'PROVISIONING',
        reason: 'Worker allocated node resources',
        correlationId: messageId,
      },
    });
  });

  await publishNodeCommand({
    nodeId: node.id,
    envelope: {
      protocolVersion: 1,
      messageId,
      type: 'server.provision',
      timestamp: new Date().toISOString(),
      payload: {
        serverId,
        generation: server.version,
        edition: server.edition,
        softwareFamily: server.softwareFamily,
        minecraftVersion: server.minecraftVersion,
        hostPort,
        ramMb,
        dataPath,
        eulaAccepted: server.eulaAccepted,
      },
    },
  });

  const response = await waitForNodeResponse(messageId, PROVISION_TIMEOUT_MS);

  if (response === null) {
    await transitionServerStatus(serverId, 'INSTALLING', {
      reason: 'Provision command sent; awaiting agent completion',
      correlationId: messageId,
    });
    logger.info({ serverId, messageId }, 'Provision timed out; marked INSTALLING');
    return { status: 'installing' };
  }

  if (!response.success) {
    await releaseServerAllocation(serverId);
    await transitionServerStatus(serverId, 'ERROR', {
      reason: 'Provision failed',
      correlationId: messageId,
      errorCode: response.errorCode ?? response.resultCode ?? 'PROVISION_FAILED',
      errorMessage: response.errorMessage ?? 'Server provisioning failed',
    });
    throw new Error(response.errorMessage ?? 'Server provisioning failed');
  }

  await transitionServerStatus(serverId, 'STOPPED', {
    reason: 'Provision completed',
    correlationId: messageId,
  });

  await assignJoinAddress(serverId);

  logger.info({ serverId, nodeId: node.id, hostPort }, 'Server provisioned');
  return { status: 'stopped' };
}
