/**
 * UDP 端口池：用于为录制 ffmpeg 子进程分配可用 RTP/RTCP 端口对
 *
 * 设计要点：
 *   - 端口范围与 mediasoup RTC 端口范围（默认 40000-40199）必须不相交，否则会与
 *     mediasoup 自身的 PlainTransport 本地绑定端口冲突
 *   - 通过 dgram.createSocket('udp4') 试探绑定来确认端口未被占用，不依赖 lsof / 系统接口
 *   - 一次申请通常成对返回 [rtp, rtcp]，习惯上 rtp = even, rtcp = rtp+1（RFC 3550 §11）
 *   - 失败重试上限 ALLOC_RETRY，超过则抛错；此时通常意味着系统并发录制数过多
 *
 * 释放策略：
 *   - 调用方在录制 stop 时调 release(rtp, rtcp) 主动释放，避免端口长期被占
 *   - 若调用方忘记 release，进程退出时端口自然回收（OS 层），不会泄漏到下次启动
 *
 * 并发：
 *   - reservedPorts 是单进程内存集合，配合 dgram 试探绑定双重防御并发竞态
 */
import dgram from 'node:dgram';

import { childLogger } from './logger.js';

const log = childLogger({ module: 'utils.port-pool' });

const RECORDING_PORT_MIN = 50000;
const RECORDING_PORT_MAX = 59998;
const ALLOC_RETRY = 64;

const reservedPorts = new Set<number>();

function isInRange(port: number): boolean {
  return port >= RECORDING_PORT_MIN && port <= RECORDING_PORT_MAX;
}

/**
 * 试探指定端口是否可用：
 *   - 创建 UDP socket 尝试 bind
 *   - 成功立即 close 释放，返回 true
 *   - 失败（EADDRINUSE / EACCES 等）返回 false
 */
function probeUdpPort(port: number): Promise<boolean> {
  return new Promise<boolean>((resolve) => {
    const socket = dgram.createSocket('udp4');
    let resolved = false;
    const finalize = (ok: boolean) => {
      if (resolved) return;
      resolved = true;
      try {
        socket.close();
      } catch {
        // already closed
      }
      resolve(ok);
    };
    socket.once('error', () => finalize(false));
    try {
      socket.bind(port, '0.0.0.0', () => finalize(true));
    } catch {
      finalize(false);
    }
  });
}

/**
 * 分配一对 (rtp, rtcp) 端口；rtp 为偶数，rtcp = rtp + 1
 *
 * @returns { rtp, rtcp } 已通过试探确认空闲；调用方使用前不再二次试探
 *          注意试探与实际使用之间存在 TOCTOU 窗口，调用方需对 ffmpeg listen 失败做兜底
 * @throws 重试 ALLOC_RETRY 次仍失败时抛错
 */
export async function allocatePortPair(): Promise<{ rtp: number; rtcp: number }> {
  for (let i = 0; i < ALLOC_RETRY; i++) {
    // 在 [MIN, MAX-1] 内随机偶数；保证 +1 后 rtcp 仍 ≤ MAX+1
    // RTP 端口必须偶数，RTCP 必须 RTP+1（mediasoup 默认行为）
    const span = (RECORDING_PORT_MAX - RECORDING_PORT_MIN) >> 1;
    const offset = Math.floor(Math.random() * span) * 2;
    const rtp = RECORDING_PORT_MIN + offset;
    const rtcp = rtp + 1;
    if (!isInRange(rtp) || !isInRange(rtcp)) continue;
    if (reservedPorts.has(rtp) || reservedPorts.has(rtcp)) continue;

    // 双端口都试探
    const [okRtp, okRtcp] = await Promise.all([probeUdpPort(rtp), probeUdpPort(rtcp)]);
    if (!okRtp || !okRtcp) continue;

    reservedPorts.add(rtp);
    reservedPorts.add(rtcp);
    return { rtp, rtcp };
  }
  throw new Error(`port-pool: failed to allocate UDP port pair after ${ALLOC_RETRY} retries`);
}

/**
 * 释放端口对。幂等：未在 reservedPorts 中的端口忽略
 */
export function releasePortPair(rtp: number, rtcp: number): void {
  reservedPorts.delete(rtp);
  reservedPorts.delete(rtcp);
  log.debug({ rtp, rtcp, reservedSize: reservedPorts.size }, 'port pair released');
}

export function getPortPoolStats(): { reserved: number; min: number; max: number } {
  return {
    reserved: reservedPorts.size,
    min: RECORDING_PORT_MIN,
    max: RECORDING_PORT_MAX,
  };
}

/** 测试专用：复位端口集合 */
export function _clearPortPool(): void {
  reservedPorts.clear();
}
