/**
 * Port Lock Manager
 * Provides atomic port allocation using file-based locks to prevent race conditions
 * during concurrent container deployments
 */

const fs = require('fs');
const path = require('path');
const lockfile = require('proper-lockfile');

const LOCK_DIR = path.join(__dirname, '.port-locks');
const LOCK_TIMEOUT = 120000; // 2 minutes
const LOCK_STALE_THRESHOLD = 120000; // 2 minutes
const LOCK_RETRY_OPTIONS = {
  retries: {
    retries: 10,
    minTimeout: 100,
    maxTimeout: 1000,
    randomize: true
  },
  stale: LOCK_STALE_THRESHOLD,
  realpath: false
};

class PortLockManager {
  constructor() {
    this.activeLocks = new Map(); // Map of lockId -> { ports: [], release: fn }
    this.ensureLockDirectory();
  }

  /**
   * Ensure lock directory exists
   */
  ensureLockDirectory() {
    if (!fs.existsSync(LOCK_DIR)) {
      fs.mkdirSync(LOCK_DIR, { recursive: true });
      console.log('[PortLockManager] Created lock directory:', LOCK_DIR);
    }
  }

  /**
   * Get lock file path for a port
   */
  getLockFilePath(port) {
    return path.join(LOCK_DIR, `port-${port}.lock`);
  }

  /**
   * Acquire locks for multiple ports atomically
   * Ports are sorted to prevent deadlocks
   * @param {string[]} ports - Array of port numbers as strings
   * @returns {Promise<string>} Lock ID for releasing locks later
   */
  async acquirePorts(ports) {
    if (!Array.isArray(ports) || ports.length === 0) {
      throw new Error('Ports must be a non-empty array');
    }

    const lockId = `lock-${Date.now()}-${Math.random().toString(36).substring(7)}`;
    const sortedPorts = [...new Set(ports)].sort((a, b) => parseInt(a) - parseInt(b));
    const acquiredLocks = [];
    const releaseFunctions = [];

    try {
      console.log(`[PortLockManager] Acquiring locks for ports: ${sortedPorts.join(', ')}`);

      // Acquire locks in sorted order to prevent deadlocks
      for (const port of sortedPorts) {
        const lockFilePath = this.getLockFilePath(port);

        // Create lock file if it doesn't exist
        if (!fs.existsSync(lockFilePath)) {
          fs.writeFileSync(lockFilePath, JSON.stringify({
            created: new Date().toISOString(),
            port
          }));
        }

        // Acquire lock with retry
        const release = await lockfile.lock(lockFilePath, LOCK_RETRY_OPTIONS);

        acquiredLocks.push(port);
        releaseFunctions.push(release);

        console.log(`[PortLockManager] Locked port ${port}`);
      }

      // Store lock information
      this.activeLocks.set(lockId, {
        ports: sortedPorts,
        releases: releaseFunctions,
        timestamp: Date.now()
      });

      console.log(`[PortLockManager] Successfully acquired all locks (ID: ${lockId})`);
      return lockId;

    } catch (error) {
      // Release any locks we managed to acquire
      console.error(`[PortLockManager] Failed to acquire all locks:`, error.message);

      for (const release of releaseFunctions) {
        try {
          await release();
        } catch (releaseError) {
          console.error(`[PortLockManager] Error releasing lock during cleanup:`, releaseError.message);
        }
      }

      throw new Error(`Failed to acquire port locks: ${error.message}`);
    }
  }

  /**
   * Release locks for a lock ID
   * @param {string} lockId - Lock ID returned from acquirePorts
   */
  async releasePorts(lockId) {
    const lockInfo = this.activeLocks.get(lockId);

    if (!lockInfo) {
      console.warn(`[PortLockManager] Lock ID ${lockId} not found (may have been released already)`);
      return;
    }

    console.log(`[PortLockManager] Releasing locks for ports: ${lockInfo.ports.join(', ')}`);

    const errors = [];

    for (const release of lockInfo.releases) {
      try {
        await release();
      } catch (error) {
        errors.push(error.message);
        console.error(`[PortLockManager] Error releasing lock:`, error.message);
      }
    }

    this.activeLocks.delete(lockId);

    if (errors.length > 0) {
      console.warn(`[PortLockManager] Released locks with ${errors.length} errors`);
    } else {
      console.log(`[PortLockManager] Successfully released all locks (ID: ${lockId})`);
    }
  }

  /**
   * Clean up stale lock files
   * Removes locks older than LOCK_STALE_THRESHOLD
   */
  async cleanupStaleLocks() {
    console.log('[PortLockManager] Cleaning up stale locks...');

    this.ensureLockDirectory();

    let cleaned = 0;
    let errors = 0;

    try {
      const files = fs.readdirSync(LOCK_DIR);

      for (const file of files) {
        if (!file.endsWith('.lock')) continue;

        const lockFilePath = path.join(LOCK_DIR, file);

        try {
          // Check if lock is stale using proper-lockfile's built-in check
          const isLocked = await lockfile.check(lockFilePath, { realpath: false, stale: LOCK_STALE_THRESHOLD });

          if (!isLocked) {
            // Lock is stale or not locked, safe to remove
            fs.unlinkSync(lockFilePath);
            cleaned++;
            console.log(`[PortLockManager] Removed stale lock: ${file}`);
          }
        } catch (error) {
          // File might not exist or might have been removed by another process
          if (error.code !== 'ENOENT') {
            errors++;
            console.warn(`[PortLockManager] Error checking lock ${file}:`, error.message);
          }
        }
      }

      console.log(`[PortLockManager] Cleanup complete: ${cleaned} stale locks removed, ${errors} errors`);
    } catch (error) {
      console.error('[PortLockManager] Error during cleanup:', error.message);
    }
  }

  /**
   * Get current lock status
   */
  getStatus() {
    const activeLocks = Array.from(this.activeLocks.entries()).map(([lockId, info]) => ({
      lockId,
      ports: info.ports,
      age: Date.now() - info.timestamp,
      timestamp: new Date(info.timestamp).toISOString()
    }));

    return {
      activeLocks: activeLocks.length,
      locks: activeLocks,
      lockDirectory: LOCK_DIR
    };
  }

  /**
   * Check if a port is currently locked
   * @param {string} port - Port number as string
   * @returns {Promise<boolean>}
   */
  async isPortLocked(port) {
    const lockFilePath = this.getLockFilePath(port);

    if (!fs.existsSync(lockFilePath)) {
      return false;
    }

    try {
      return await lockfile.check(lockFilePath, { realpath: false, stale: LOCK_STALE_THRESHOLD });
    } catch (error) {
      // If we can't check, assume it's not locked
      return false;
    }
  }
}

// Singleton instance
const portLockManager = new PortLockManager();

module.exports = portLockManager;
