import { join } from 'path';
import { promises as fs } from 'fs';
import { basename, resolve } from 'path';
import { createHash } from 'crypto';
import { getGlobalDir, getPermissionErrorHelp } from './global-dir.js';
/**
 * Generate a stable projectId from an absolute path
 * Uses SHA-1 hash encoded as base64url
 */
export function generateProjectId(absolutePath) {
    const hash = createHash('sha1').update(absolutePath).digest('base64url');
    // Take first 16 characters for readability
    return hash.substring(0, 16);
}
/**
 * Build display name for a workspace.
 * - Main repo: "repo"
 * - Worktree: "repo · worktree"
 */
export function generateProjectDisplayName(workspacePath, workflowRootPath) {
    const workspaceName = basename(workspacePath);
    const repoName = basename(workflowRootPath);
    if (workspacePath === workflowRootPath) {
        return repoName;
    }
    return `${repoName} · ${workspaceName}`;
}
export class ProjectRegistry {
    registryPath;
    registryDir;
    needsInitialization = false;
    constructor() {
        this.registryDir = getGlobalDir();
        this.registryPath = join(this.registryDir, 'activeProjects.json');
    }
    /**
     * Ensure the registry directory exists
     */
    async ensureRegistryDir() {
        try {
            await fs.mkdir(this.registryDir, { recursive: true });
        }
        catch (error) {
            // Directory might already exist, ignore EEXIST errors
            if (error.code === 'EEXIST') {
                return;
            }
            // For permission errors, provide helpful guidance
            if (error.code === 'EACCES' || error.code === 'EPERM') {
                console.error(getPermissionErrorHelp('create directory', this.registryDir));
                throw error;
            }
            // Re-throw other errors
            throw error;
        }
    }
    /**
     * Read the registry file with atomic operations
     * Returns a map keyed by projectId
     */
    async readRegistry() {
        await this.ensureRegistryDir();
        try {
            const content = await fs.readFile(this.registryPath, 'utf-8');
            // Handle empty or whitespace-only files
            const trimmedContent = content.trim();
            if (!trimmedContent) {
                console.error(`[ProjectRegistry] Warning: ${this.registryPath} is empty, initializing with empty registry`);
                // Mark that we need to write the file
                this.needsInitialization = true;
                return new Map();
            }
            const data = JSON.parse(trimmedContent);
            const registry = new Map();
            // Ensure backward compatibility with older formats:
            // - instances may be missing
            // - workflowRootPath may be missing
            for (const [projectId, entry] of Object.entries(data)) {
                const normalizedProjectPath = resolve(entry.projectPath);
                const normalizedWorkflowRootPath = resolve(entry.workflowRootPath || entry.projectPath);
                registry.set(projectId, {
                    ...entry,
                    projectPath: normalizedProjectPath,
                    workflowRootPath: normalizedWorkflowRootPath,
                    projectName: entry.projectName || generateProjectDisplayName(normalizedProjectPath, normalizedWorkflowRootPath),
                    instances: Array.isArray(entry.instances) ? entry.instances : []
                });
            }
            return registry;
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // File doesn't exist yet, return empty map
                this.needsInitialization = true;
                return new Map();
            }
            if (error instanceof SyntaxError) {
                // JSON parsing error - file is corrupted or invalid
                console.error(`[ProjectRegistry] Error: Failed to parse ${this.registryPath}: ${error.message}`);
                console.error(`[ProjectRegistry] The file may be corrupted. Initializing with empty registry.`);
                // Back up the corrupted file
                try {
                    const backupPath = `${this.registryPath}.corrupted.${Date.now()}`;
                    await fs.copyFile(this.registryPath, backupPath);
                    console.error(`[ProjectRegistry] Corrupted file backed up to: ${backupPath}`);
                }
                catch (backupError) {
                    // Ignore backup errors
                }
                this.needsInitialization = true;
                return new Map();
            }
            throw error;
        }
    }
    /**
     * Write the registry file atomically
     */
    async writeRegistry(registry) {
        await this.ensureRegistryDir();
        const data = Object.fromEntries(registry);
        const content = JSON.stringify(data, null, 2);
        // Write to temporary file first, then rename for atomic operation
        const tempPath = `${this.registryPath}.tmp`;
        await fs.writeFile(tempPath, content, 'utf-8');
        await fs.rename(tempPath, this.registryPath);
    }
    /**
     * Check if a process is still running
     * Note: When running in Docker with path translation, we can't check host PIDs,
     * so we assume processes are alive if path translation is enabled.
     */
    isProcessAlive(pid) {
        // If path translation is enabled, we're in Docker and can't check host PIDs
        const hostPrefix = process.env.SPEC_WORKFLOW_HOST_PATH_PREFIX;
        const containerPrefix = process.env.SPEC_WORKFLOW_CONTAINER_PATH_PREFIX;
        if (hostPrefix && containerPrefix) {
            // Can't verify host PIDs from inside Docker, assume alive
            return true;
        }
        try {
            // Sending signal 0 checks if process exists without actually sending a signal
            process.kill(pid, 0);
            return true;
        }
        catch (error) {
            return false;
        }
    }
    /**
     * Register a project in the global registry
     * Self-healing: If a project exists with dead PIDs, cleans them up and adds new PID
     * Multi-instance: Allows unlimited MCP server instances per project
     */
    async registerProject(projectPath, pid, options = {}) {
        const registry = await this.readRegistry();
        const workspacePath = resolve(projectPath);
        const workflowRootPath = resolve(options.workflowRootPath || projectPath);
        const projectId = generateProjectId(workspacePath);
        const projectName = options.projectName || generateProjectDisplayName(workspacePath, workflowRootPath);
        const existing = registry.get(projectId);
        if (existing) {
            // Self-healing: Filter out dead PIDs
            const liveInstances = existing.instances.filter(i => this.isProcessAlive(i.pid));
            // Check if this PID is already registered (avoid duplicates)
            if (!liveInstances.some(i => i.pid === pid)) {
                liveInstances.push({ pid, registeredAt: new Date().toISOString() });
            }
            // Update with live instances (no limit on number of instances)
            existing.projectPath = workspacePath;
            existing.workflowRootPath = workflowRootPath;
            existing.projectName = projectName;
            existing.instances = liveInstances;
            registry.set(projectId, existing);
        }
        else {
            // New project
            const entry = {
                projectId,
                projectPath: workspacePath,
                workflowRootPath,
                projectName,
                instances: [{ pid, registeredAt: new Date().toISOString() }]
            };
            registry.set(projectId, entry);
        }
        await this.writeRegistry(registry);
        return projectId;
    }
    /**
     * Unregister a project from the global registry by path
     * If pid is provided, only removes that specific instance
     * If no pid provided, removes the entire project (backwards compat)
     */
    async unregisterProject(projectPath, pid) {
        const registry = await this.readRegistry();
        const absolutePath = resolve(projectPath);
        const projectId = generateProjectId(absolutePath);
        const entry = registry.get(projectId);
        if (!entry)
            return;
        if (pid !== undefined) {
            // Remove only this PID's instance
            entry.instances = entry.instances.filter(i => i.pid !== pid);
            if (entry.instances.length === 0) {
                registry.delete(projectId);
            }
            else {
                registry.set(projectId, entry);
            }
        }
        else {
            // Remove entire project (backwards compat)
            registry.delete(projectId);
        }
        await this.writeRegistry(registry);
    }
    /**
     * Unregister a project by projectId
     */
    async unregisterProjectById(projectId) {
        const registry = await this.readRegistry();
        registry.delete(projectId);
        await this.writeRegistry(registry);
    }
    /**
     * Get all active projects from the registry
     */
    async getAllProjects() {
        const registry = await this.readRegistry();
        return Array.from(registry.values());
    }
    /**
     * Get a specific project by path
     */
    async getProject(projectPath) {
        const registry = await this.readRegistry();
        const absolutePath = resolve(projectPath);
        const projectId = generateProjectId(absolutePath);
        return registry.get(projectId) || null;
    }
    /**
     * Get a specific project by projectId
     */
    async getProjectById(projectId) {
        const registry = await this.readRegistry();
        return registry.get(projectId) || null;
    }
    /**
     * Clean up stale instances (where the process is no longer running)
     * Projects with no live instances are removed entirely
     * Returns the count of removed instances
     */
    async cleanupStaleProjects() {
        const registry = await this.readRegistry();
        let removedInstanceCount = 0;
        let needsWrite = this.needsInitialization; // Write if file needs initialization
        for (const [projectId, entry] of registry.entries()) {
            const liveInstances = entry.instances.filter(i => this.isProcessAlive(i.pid));
            const deadCount = entry.instances.length - liveInstances.length;
            if (deadCount > 0) {
                removedInstanceCount += deadCount;
                needsWrite = true;
                if (liveInstances.length === 0) {
                    // No live instances, remove entire project
                    registry.delete(projectId);
                }
                else {
                    // Keep project with only live instances
                    entry.instances = liveInstances;
                    registry.set(projectId, entry);
                }
            }
        }
        if (needsWrite) {
            await this.writeRegistry(registry);
            this.needsInitialization = false; // Reset flag after successful write
        }
        return removedInstanceCount;
    }
    /**
     * Check if a project is registered by path
     */
    async isProjectRegistered(projectPath) {
        const registry = await this.readRegistry();
        const absolutePath = resolve(projectPath);
        const projectId = generateProjectId(absolutePath);
        return registry.has(projectId);
    }
    /**
     * Get the registry file path for watching
     */
    getRegistryPath() {
        return this.registryPath;
    }
}
//# sourceMappingURL=project-registry.js.map