import { prisma } from '../lib/prisma';
import { env } from '../config/env';
import { AppError } from '../middleware/error-handler';

interface PortRangeConfig {
  service: string;
  start: number;
  end: number;
  blockSize?: number;
}

// Nginx embed proxies use a block of consecutive ports (embed+0 through embed+14).
// Allocate in blocks of 16 to prevent overlapping port ranges between instances.
const EMBED_BLOCK_SIZE = 20;

function getPortRanges(): PortRangeConfig[] {
  return [
    { service: 'api', start: env.PORT_RANGE_API_START, end: env.PORT_RANGE_API_END },
    { service: 'admin', start: env.PORT_RANGE_ADMIN_START, end: env.PORT_RANGE_ADMIN_END },
    { service: 'postgres', start: env.PORT_RANGE_POSTGRES_START, end: env.PORT_RANGE_POSTGRES_END },
    { service: 'nginx', start: env.PORT_RANGE_NGINX_START, end: env.PORT_RANGE_NGINX_END },
    { service: 'embed', start: env.PORT_RANGE_EMBED_START, end: env.PORT_RANGE_EMBED_END, blockSize: EMBED_BLOCK_SIZE },
  ];
}

async function findNextAvailablePort(
  start: number,
  end: number,
  tx?: Parameters<Parameters<typeof prisma.$transaction>[0]>[0],
  blockSize: number = 1
): Promise<number> {
  const client = tx || prisma;
  const allocated = await client.portAllocation.findMany({
    where: { port: { gte: start, lte: end } },
    select: { port: true },
    orderBy: { port: 'asc' },
  });

  const usedPorts = new Set(allocated.map((a) => a.port));

  for (let port = start; port + blockSize - 1 <= end; port += blockSize) {
    if (!usedPorts.has(port)) {
      return port;
    }
  }

  throw new AppError(503, `No available ports in range ${start}-${end}`, 'PORT_EXHAUSTED');
}

export interface AllocatedPorts {
  config: Record<string, number>;
  allocations: Array<{ port: number; service: string }>;
}

/**
 * Allocate ports using a serializable transaction to prevent race conditions.
 * Port allocation records are created immediately to act as a DB-level lock.
 * They are linked to the instance later via instances.service.ts.
 */
export async function allocatePorts(): Promise<AllocatedPorts> {
  return prisma.$transaction(async (tx) => {
    const ranges = getPortRanges();
    const config: Record<string, number> = {};
    const allocations: Array<{ port: number; service: string }> = [];

    for (const range of ranges) {
      const port = await findNextAvailablePort(range.start, range.end, tx, range.blockSize ?? 1);
      config[range.service] = port;
      allocations.push({ port, service: range.service });
    }

    return { config, allocations };
  });
}

export async function releasePorts(instanceId: string): Promise<void> {
  await prisma.portAllocation.deleteMany({ where: { instanceId } });
}

export async function getPortUsage(): Promise<{
  ranges: Array<{ service: string; start: number; end: number; used: number; total: number }>;
}> {
  const ranges = getPortRanges();
  const result = [];

  for (const range of ranges) {
    const used = await prisma.portAllocation.count({
      where: { port: { gte: range.start, lte: range.end } },
    });
    result.push({
      service: range.service,
      start: range.start,
      end: range.end,
      used,
      total: range.end - range.start + 1,
    });
  }

  return { ranges: result };
}
