/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { lock } from 'proper-lockfile';
import { debugLogger } from '../utils/debugLogger.js';
const PROJECT_ROOT_FILE = '.project_root';
const LOCK_TIMEOUT_MS = 10000;
const LOCK_RETRY_DELAY_MS = 100;
/**
 * Manages a mapping between absolute project paths and short, human-readable identifiers.
 * This helps reduce context bloat and makes temporary directories easier to work with.
 */
export class ProjectRegistry {
    registryPath;
    baseDirs;
    data;
    initPromise;
    constructor(registryPath, baseDirs = []) {
        this.registryPath = registryPath;
        this.baseDirs = baseDirs;
    }
    /**
     * Initializes the registry by loading data from disk.
     */
    async initialize() {
        if (this.initPromise) {
            return this.initPromise;
        }
        this.initPromise = (async () => {
            if (this.data) {
                return;
            }
            this.data = await this.loadData();
        })();
        return this.initPromise;
    }
    async loadData() {
        if (!fs.existsSync(this.registryPath)) {
            return { projects: {} };
        }
        try {
            const content = await fs.promises.readFile(this.registryPath, 'utf8');
            // eslint-disable-next-line @typescript-eslint/no-unsafe-return
            return JSON.parse(content);
        }
        catch (e) {
            debugLogger.debug('Failed to load registry: ', e);
            // If the registry is corrupted, we'll start fresh to avoid blocking the CLI
            return { projects: {} };
        }
    }
    normalizePath(projectPath) {
        let resolved = path.resolve(projectPath);
        if (os.platform() === 'win32') {
            resolved = resolved.toLowerCase();
        }
        return resolved;
    }
    async save(data) {
        const dir = path.dirname(this.registryPath);
        if (!fs.existsSync(dir)) {
            await fs.promises.mkdir(dir, { recursive: true });
        }
        try {
            const content = JSON.stringify(data, null, 2);
            // Use a randomized tmp path to avoid ENOENT crashes when save() is called concurrently
            const tmpPath = this.registryPath + '.' + randomUUID() + '.tmp';
            await fs.promises.writeFile(tmpPath, content, 'utf8');
            await fs.promises.rename(tmpPath, this.registryPath);
        }
        catch (error) {
            debugLogger.error(`Failed to save project registry to ${this.registryPath}:`, error);
        }
    }
    /**
     * Returns a short identifier for the given project path.
     * If the project is not already in the registry, a new identifier is generated and saved.
     */
    async getShortId(projectPath) {
        if (!this.data) {
            throw new Error('ProjectRegistry must be initialized before use');
        }
        const normalizedPath = this.normalizePath(projectPath);
        // Ensure directory exists so we can create a lock file
        const dir = path.dirname(this.registryPath);
        if (!fs.existsSync(dir)) {
            await fs.promises.mkdir(dir, { recursive: true });
        }
        // Ensure the registry file exists so proper-lockfile can lock it
        if (!fs.existsSync(this.registryPath)) {
            await this.save({ projects: {} });
        }
        // Use proper-lockfile to prevent racy updates
        const release = await lock(this.registryPath, {
            retries: {
                retries: Math.floor(LOCK_TIMEOUT_MS / LOCK_RETRY_DELAY_MS),
                minTimeout: LOCK_RETRY_DELAY_MS,
            },
        });
        try {
            // Re-load data under lock to get the latest state
            const currentData = await this.loadData();
            this.data = currentData;
            let shortId = currentData.projects[normalizedPath];
            // If we have a mapping, verify it against the folders on disk
            if (shortId) {
                if (await this.verifySlugOwnership(shortId, normalizedPath)) {
                    // HEAL: If it passed verification but markers are missing (e.g. new base dir or deleted marker), recreate them.
                    await this.ensureOwnershipMarkers(shortId, normalizedPath);
                    return shortId;
                }
                // If verification fails, it means the registry is out of sync or someone else took it.
                // We'll remove the mapping and find/generate a new one.
                delete currentData.projects[normalizedPath];
            }
            // Try to find if this project already has folders assigned that we didn't know about
            shortId = await this.findExistingSlugForPath(normalizedPath);
            if (!shortId) {
                // Generate a new one
                shortId = await this.claimNewSlug(normalizedPath, currentData.projects);
            }
            currentData.projects[normalizedPath] = shortId;
            await this.save(currentData);
            return shortId;
        }
        finally {
            await release();
        }
    }
    async verifySlugOwnership(slug, projectPath) {
        if (this.baseDirs.length === 0) {
            return true; // Nothing to verify against
        }
        for (const baseDir of this.baseDirs) {
            const markerPath = path.join(baseDir, slug, PROJECT_ROOT_FILE);
            if (fs.existsSync(markerPath)) {
                try {
                    const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
                    if (this.normalizePath(owner) !== this.normalizePath(projectPath)) {
                        return false;
                    }
                }
                catch (e) {
                    debugLogger.debug(`Failed to read ownership marker ${markerPath}:`, e);
                    // If we can't read it, assume it's not ours or corrupted.
                    return false;
                }
            }
        }
        return true;
    }
    async findExistingSlugForPath(projectPath) {
        if (this.baseDirs.length === 0) {
            return undefined;
        }
        const normalizedTarget = this.normalizePath(projectPath);
        // Scan all base dirs to see if any slug already belongs to this project
        for (const baseDir of this.baseDirs) {
            if (!fs.existsSync(baseDir)) {
                continue;
            }
            try {
                const candidates = await fs.promises.readdir(baseDir);
                for (const candidate of candidates) {
                    const markerPath = path.join(baseDir, candidate, PROJECT_ROOT_FILE);
                    if (fs.existsSync(markerPath)) {
                        const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
                        if (this.normalizePath(owner) === normalizedTarget) {
                            // Found it! Ensure all base dirs have the marker
                            await this.ensureOwnershipMarkers(candidate, normalizedTarget);
                            return candidate;
                        }
                    }
                }
            }
            catch (e) {
                debugLogger.debug(`Failed to scan base dir ${baseDir}:`, e);
            }
        }
        return undefined;
    }
    async claimNewSlug(projectPath, existingMappings) {
        const baseName = path.basename(projectPath) || 'project';
        const slug = this.slugify(baseName);
        let counter = 0;
        const existingIds = new Set(Object.values(existingMappings));
        while (true) {
            const candidate = counter === 0 ? slug : `${slug}-${counter}`;
            counter++;
            // Check if taken in registry
            if (existingIds.has(candidate)) {
                continue;
            }
            // Check if taken on disk
            let diskCollision = false;
            for (const baseDir of this.baseDirs) {
                const markerPath = path.join(baseDir, candidate, PROJECT_ROOT_FILE);
                if (fs.existsSync(markerPath)) {
                    try {
                        const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
                        if (this.normalizePath(owner) !== this.normalizePath(projectPath)) {
                            diskCollision = true;
                            break;
                        }
                    }
                    catch {
                        // If we can't read it, assume it's someone else's to be safe
                        diskCollision = true;
                        break;
                    }
                }
            }
            if (diskCollision) {
                continue;
            }
            // Try to claim it
            try {
                await this.ensureOwnershipMarkers(candidate, projectPath);
                return candidate;
            }
            catch {
                // Someone might have claimed it between our check and our write.
                // Try next candidate.
                continue;
            }
        }
    }
    async ensureOwnershipMarkers(slug, projectPath) {
        const normalizedProject = this.normalizePath(projectPath);
        for (const baseDir of this.baseDirs) {
            const slugDir = path.join(baseDir, slug);
            if (!fs.existsSync(slugDir)) {
                await fs.promises.mkdir(slugDir, { recursive: true });
            }
            const markerPath = path.join(slugDir, PROJECT_ROOT_FILE);
            if (fs.existsSync(markerPath)) {
                const owner = (await fs.promises.readFile(markerPath, 'utf8')).trim();
                if (this.normalizePath(owner) === normalizedProject) {
                    continue;
                }
                // Collision!
                throw new Error(`Slug ${slug} is already owned by ${owner}`);
            }
            // Use flag: 'wx' to ensure atomic creation
            await fs.promises.writeFile(markerPath, normalizedProject, {
                encoding: 'utf8',
                flag: 'wx',
            });
        }
    }
    slugify(text) {
        return (text
            .toLowerCase()
            .replace(/[^a-z0-9]/g, '-')
            .replace(/-+/g, '-')
            .replace(/^-|-$/g, '') || 'project');
    }
}
//# sourceMappingURL=projectRegistry.js.map