/**
 * State Manager - Thread-safe file operations with locking
 *
 * Prevents data corruption when multiple API requests modify state files concurrently.
 * Uses file-based locking with automatic retry and timeout handling.
 *
 * @module state-manager
 */

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

class StateManager {
  /**
   * Create a StateManager instance
   * @param {string} filePath - Path to the state file (e.g., services.json)
   * @param {Object} options - Configuration options
   * @param {number} options.lockTimeout - Max time to wait for lock (ms)
   * @param {number} options.lockRetries - Number of lock acquisition retries
   * @param {number} options.lockRetryInterval - Time between retries (ms)
   */
  constructor(filePath, options = {}) {
    this.filePath = filePath;
    this.lockOptions = {
      retries: {
        retries: options.lockRetries || 10,
        minTimeout: options.lockRetryInterval || 100,
        maxTimeout: (options.lockRetryInterval || 100) * 3
      },
      stale: options.lockTimeout || 30000 // 30 seconds
    };

    // Ensure file exists
    this._ensureFileExists();
  }

  /**
   * Ensure the state file exists, create with empty array if not
   * @private
   */
  _ensureFileExists() {
    if (!fsSync.existsSync(this.filePath)) {
      const dir = path.dirname(this.filePath);
      if (!fsSync.existsSync(dir)) {
        fsSync.mkdirSync(dir, { recursive: true });
      }
      fsSync.writeFileSync(this.filePath, '[]', 'utf8');
    }
  }

  /**
   * Read the state file (no locking required for read-only operations)
   * @returns {Promise<any>} Parsed JSON data
   * @throws {Error} If file doesn't exist or JSON is invalid
   */
  async read() {
    try {
      const content = await fs.readFile(this.filePath, 'utf8');
      return JSON.parse(content);
    } catch (error) {
      if (error.code === 'ENOENT') {
        // File doesn't exist — recreate without locking (no file to lock)
        this._ensureFileExists();
        return [];
      }
      throw new Error(`Failed to read state file: ${error.message}`);
    }
  }

  /**
   * Write data to the state file (with locking)
   * @param {any} data - Data to write (will be JSON.stringify'd)
   * @returns {Promise<void>}
   * @throws {Error} If lock cannot be acquired or write fails
   */
  async write(data) {
    let release;
    try {
      // Acquire lock
      release = await lockfile.lock(this.filePath, this.lockOptions);

      // Write data with pretty formatting
      await fs.writeFile(this.filePath, JSON.stringify(data, null, 2), 'utf8');
    } catch (error) {
      if (error.code === 'ELOCKED') {
        throw new Error('State file is locked by another process. Try again.');
      }
      throw new Error(`Failed to write state file: ${error.message}`);
    } finally {
      // Always release lock
      if (release) {
        try {
          await release();
        } catch (e) {
          // Lock release failure (non-critical, lock will expire via stale timeout)
        }
      }
    }
  }

  /**
   * Update the state file using a callback function (atomic operation)
   * This is the recommended method for most operations.
   *
   * @param {Function} updateFn - Function that receives current data and returns updated data
   * @returns {Promise<any>} The updated data
   * @throws {Error} If lock cannot be acquired or update fails
   *
   * @example
   * // Add a new service
   * await stateManager.update(services => {
   *   services.push({ id: 'new-service', name: 'New Service' });
   *   return services;
   * });
   *
   * @example
   * // Remove a service
   * await stateManager.update(services => {
   *   return services.filter(s => s.id !== 'old-service');
   * });
   */
  async update(updateFn) {
    let release;
    try {
      // Acquire lock
      release = await lockfile.lock(this.filePath, this.lockOptions);

      // Read current data
      const content = await fs.readFile(this.filePath, 'utf8');
      const currentData = JSON.parse(content);

      // Apply update function
      const updatedData = await updateFn(currentData);

      // Write updated data
      await fs.writeFile(this.filePath, JSON.stringify(updatedData, null, 2), 'utf8');

      return updatedData;
    } catch (error) {
      if (error.code === 'ELOCKED') {
        throw new Error('State file is locked by another process. Try again.');
      }
      throw new Error(`Failed to update state file: ${error.message}`);
    } finally {
      // Always release lock
      if (release) {
        try {
          await release();
        } catch (e) {
          // Lock release failure (non-critical, lock will expire via stale timeout)
        }
      }
    }
  }

  /**
   * Check if the state file is currently locked
   * @returns {Promise<boolean>} True if locked, false otherwise
   */
  async isLocked() {
    try {
      return await lockfile.check(this.filePath);
    } catch (error) {
      return false;
    }
  }

  /**
   * Forcefully unlock the state file (use with caution!)
   * Only use this if a lock is stuck due to a crashed process.
   * @returns {Promise<void>}
   */
  async forceUnlock() {
    try {
      await lockfile.unlock(this.filePath);
    } catch (error) {
      // Ignore errors if file wasn't locked
      if (error.code !== 'ENOTACQUIRED') {
        throw error;
      }
    }
  }

  /**
   * Add an item to the state array (convenience method)
   * @param {any} item - Item to add
   * @returns {Promise<any>} Updated array
   */
  async addItem(item) {
    return await this.update(items => {
      items.push(item);
      return items;
    });
  }

  /**
   * Remove an item from the state array by ID (convenience method)
   * @param {string} id - ID of item to remove
   * @returns {Promise<any>} Updated array
   */
  async removeItem(id) {
    return await this.update(items => {
      return items.filter(item => item.id !== id);
    });
  }

  /**
   * Update an item in the state array by ID (convenience method)
   * @param {string} id - ID of item to update
   * @param {Object} updates - Properties to update
   * @returns {Promise<any>} Updated array
   */
  async updateItem(id, updates) {
    return await this.update(items => {
      return items.map(item => {
        if (item.id === id) {
          return { ...item, ...updates };
        }
        return item;
      });
    });
  }

  /**
   * Find an item in the state array by ID (convenience method)
   * @param {string} id - ID of item to find
   * @returns {Promise<any|null>} Found item or null
   */
  async findItem(id) {
    const items = await this.read();
    return items.find(item => item.id === id) || null;
  }
}

module.exports = StateManager;
