/**
 * @fileoverview OrdoJS CLI - Port Manager
 *
 * Handles port allocation, detection of conflicts, and finding available ports.
 */

import net from 'net';
import { logger } from '../utils/index.js';

/**
 * Port range for automatic allocation
 */
const PORT_RANGE = {
  MIN: 3000,
  MAX: 3999
};

/**
 * PortManager class for handling port allocation and conflict resolution
 */
export class PortManager {
  private usedPorts: Set<number>;
  private basePort: number;

  /**
   * Create a new PortManager instance
   *
   * @param basePort - The preferred base port to use (default: 3000)
   */
  constructor(basePort = PORT_RANGE.MIN) {
    this.usedPorts = new Set();
    this.basePort = basePort;
  }

  /**
   * Check if a port is available
   *
   * @param port - The port to check
   * @returns Promise resolving to true if the port is available, false otherwise
   */
  async isPortAvailable(port: number): Promise<boolean> {
    return new Promise((resolve) => {
      const server = net.createServer();

      server.once('error', (err) => {
        const error = err as NodeJS.ErrnoException;
        if (error.code === 'EADDRINUSE') {
          resolve(false);
        } else {
          // Other errors also indicate the port is not usable
          resolve(false);
        }
      });

      server.once('listening', () => {
        // Close the server and resolve with true (port is available)
        server.close(() => {
          resolve(true);
        });
      });

      // Try to listen on the port
      server.listen(port, '127.0.0.1');
    });
  }

  /**
   * Find an available port starting from the specified port
   *
   * @param startPort - The port to start checking from
   * @returns Promise resolving to an available port number
   */
  async findAvailablePort(startPort: number = this.basePort): Promise<number> {
    // Start from the requested port
    let port = startPort;

    // Try ports in sequence until we find an available one
    while (port <= PORT_RANGE.MAX) {
      if (await this.isPortAvailable(port)) {
        return port;
      }
      port++;
    }

    // If we've exhausted the range, throw an error
    throw new Error(`No available ports found in range ${startPort}-${PORT_RANGE.MAX}`);
  }

  /**
   * Allocate a port for a specific service
   *
   * @param serviceName - Name of the service requesting a port
   * @param preferredPort - Preferred port to use if available
   * @returns Promise resolving to the allocated port number
   */
  async allocatePort(serviceName: string, preferredPort?: number): Promise<number> {
    const startPort = preferredPort || this.basePort;

    try {
      // Check if the preferred port is available
      if (preferredPort && await this.isPortAvailable(preferredPort)) {
        this.usedPorts.add(preferredPort);
        logger.debug(`Allocated port ${preferredPort} for ${serviceName}`);
        return preferredPort;
      }

      // Find an available port
      const port = await this.findAvailablePort(startPort);
      this.usedPorts.add(port);

      if (preferredPort && port !== preferredPort) {
        logger.warn(`Preferred port ${preferredPort} for ${serviceName} was not available, using ${port} instead`);
      } else {
        logger.debug(`Allocated port ${port} for ${serviceName}`);
      }

      return port;
    } catch (error) {
      logger.error(`Failed to allocate port for ${serviceName}: ${error instanceof Error ? error.message : String(error)}`);
      throw error;
    }
  }

  /**
   * Release a previously allocated port
   *
   * @param port - The port to release
   */
  releasePort(port: number): void {
    if (this.usedPorts.has(port)) {
      this.usedPorts.delete(port);
      logger.debug(`Released port ${port}`);
    }
  }

  /**
   * Get all currently allocated ports
   *
   * @returns Set of allocated port numbers
   */
  getAllocatedPorts(): Set<number> {
    return new Set(this.usedPorts);
  }
}
