/**
 * ID Registry - Atomic Feature ID registration with collision detection
 *
 * Provides thread-safe ID registration using file-based locking to prevent
 * race conditions during concurrent imports.
 *
 * Features:
 * - File-based locking for atomic updates
 * - Collision detection (checks both FS-XXX and FS-XXXE)
 * - JSON-based persistent storage
 * - Automatic lock cleanup with timeout
 */
import * as fs from '../utils/fs-native.js';
import path from 'path';
/**
 * ID Registry with atomic file-based locking
 */
export class IDRegistry {
    constructor(projectRoot) {
        this.lockTimeout = 30000; // 30 seconds
        this.retryDelay = 100; // 100ms
        this.registryPath = path.join(projectRoot, '.specweave/.id-registry.json');
        this.lockPath = path.join(projectRoot, '.specweave/.id-registry.lock');
    }
    /**
     * Register a new Feature ID atomically
     *
     * @param fsId - Feature ID to register (e.g., "FS-042E")
     * @param entry - Registry entry metadata
     * @throws Error if collision detected or lock acquisition fails
     */
    async registerID(fsId, entry) {
        // Acquire lock with timeout
        await this.acquireLock();
        try {
            // Read current registry
            const registry = await this.readRegistry();
            // Check for collision (exact match or variant)
            const collision = this.detectCollision(fsId, registry);
            if (collision) {
                throw new Error(`ID collision: ${fsId} conflicts with ${collision}`);
            }
            // Add new entry
            registry.entries[fsId] = {
                id: fsId,
                ...entry
            };
            registry.lastUpdated = new Date().toISOString();
            registry.version++;
            // Write atomically
            await this.writeRegistry(registry);
        }
        finally {
            // Always release lock
            await this.releaseLock();
        }
    }
    /**
     * Check if ID exists in registry
     *
     * @param fsId - Feature ID to check
     * @returns True if ID is registered
     */
    async hasID(fsId) {
        const registry = await this.readRegistry();
        return fsId in registry.entries;
    }
    /**
     * Get registry entry
     *
     * @param fsId - Feature ID to retrieve
     * @returns Registry entry or null if not found
     */
    async getEntry(fsId) {
        const registry = await this.readRegistry();
        return registry.entries[fsId] || null;
    }
    /**
     * Get all registered IDs
     *
     * @returns Array of all registered Feature IDs
     */
    async getAllIDs() {
        const registry = await this.readRegistry();
        return Object.keys(registry.entries);
    }
    /**
     * Acquire lock file with timeout and retry
     *
     * @throws Error if lock cannot be acquired within timeout
     */
    async acquireLock() {
        const startTime = Date.now();
        // Ensure lock directory exists
        await fs.ensureDir(path.dirname(this.lockPath));
        while (true) {
            try {
                // Try to create lock file exclusively
                await fs.writeFile(this.lockPath, Date.now().toString(), { flag: 'wx' });
                return; // Lock acquired
            }
            catch (error) {
                if (error.code !== 'EEXIST') {
                    throw error; // Unexpected error
                }
                // Lock exists, check timeout
                const elapsed = Date.now() - startTime;
                if (elapsed > this.lockTimeout) {
                    // Check if lock is stale
                    const lockAge = await this.getLockAge();
                    if (lockAge > this.lockTimeout) {
                        // Stale lock, force remove
                        await fs.remove(this.lockPath);
                        continue; // Retry
                    }
                    throw new Error(`Lock acquisition timeout after ${elapsed}ms`);
                }
                // Wait and retry
                await this.sleep(this.retryDelay);
            }
        }
    }
    /**
     * Release lock file
     */
    async releaseLock() {
        try {
            await fs.remove(this.lockPath);
        }
        catch (error) {
            // Ignore if lock doesn't exist (already released)
            if (error.code !== 'ENOENT') {
                throw error;
            }
        }
    }
    /**
     * Get lock file age in milliseconds
     *
     * @returns Lock age or Infinity if lock doesn't exist
     */
    async getLockAge() {
        try {
            const lockContent = await fs.readFile(this.lockPath, 'utf-8');
            const lockTime = parseInt(lockContent, 10);
            return Date.now() - lockTime;
        }
        catch (error) {
            return Infinity; // Lock doesn't exist
        }
    }
    /**
     * Read registry from disk
     *
     * @returns Registry data
     */
    async readRegistry() {
        try {
            const content = await fs.readFile(this.registryPath, 'utf-8');
            return JSON.parse(content);
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // Registry doesn't exist, return empty
                return {
                    entries: {},
                    lastUpdated: new Date().toISOString(),
                    version: 1
                };
            }
            throw error;
        }
    }
    /**
     * Write registry to disk atomically
     *
     * @param registry - Registry data to write
     */
    async writeRegistry(registry) {
        await fs.ensureDir(path.dirname(this.registryPath));
        // Write to temp file first
        const tempPath = `${this.registryPath}.tmp`;
        await fs.writeFile(tempPath, JSON.stringify(registry, null, 2));
        // Atomic rename
        await fs.rename(tempPath, this.registryPath);
    }
    /**
     * Detect ID collision (exact match or variant)
     *
     * @param fsId - ID to check (e.g., "FS-042E")
     * @param registry - Current registry
     * @returns Conflicting ID or null if no collision
     */
    detectCollision(fsId, registry) {
        if (fsId in registry.entries) {
            return fsId;
        }
        const match = fsId.match(/^FS-(\d{3,})E?$/);
        if (!match) {
            throw new Error(`Invalid FS-ID format: ${fsId}`);
        }
        const paddedNum = match[1].padStart(3, '0');
        const internalId = `FS-${paddedNum}`;
        const externalId = `FS-${paddedNum}E`;
        if (internalId !== fsId && internalId in registry.entries) {
            return internalId;
        }
        if (externalId !== fsId && externalId in registry.entries) {
            return externalId;
        }
        return null;
    }
    /**
     * Sleep for specified milliseconds
     *
     * @param ms - Milliseconds to sleep
     */
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}
//# sourceMappingURL=id-registry.js.map