import { promises as fs, existsSync, mkdirSync } from "fs";
import * as path from "path";
import * as crypto from "crypto";
import { getPluginsDir } from "../utils/configPaths.js";
import { GitService } from "./GitService.js";
/**
 * Marketplace Service
 *
 * Handles local plugin marketplace registration, plugin installation,
 * and state management for installed plugins.
 */
export class MarketplaceService {
    constructor() {
        this.pluginsDir = getPluginsDir();
        this.knownMarketplacesPath = path.join(this.pluginsDir, "known_marketplaces.json");
        this.installedPluginsPath = path.join(this.pluginsDir, "installed_plugins.json");
        this.lockPath = path.join(this.pluginsDir, ".lock");
        this.tmpDir = path.join(this.pluginsDir, "tmp");
        this.cacheDir = path.join(this.pluginsDir, "cache");
        this.marketplacesDir = path.join(this.pluginsDir, "marketplaces");
        this.gitService = new GitService();
        this.ensureDirectoryStructure();
    }
    /**
     * Ensures the required directory structure exists in ~/.wave/plugins
     */
    ensureDirectoryStructure() {
        [this.pluginsDir, this.tmpDir, this.cacheDir, this.marketplacesDir].forEach((dir) => {
            if (!existsSync(dir)) {
                mkdirSync(dir, { recursive: true });
            }
        });
    }
    /**
     * Check if a lock file is stale by reading its PID and checking if the process is alive.
     * Returns true if the lock is stale and safe to remove.
     */
    async isStaleLock() {
        try {
            const content = await fs.readFile(this.lockPath, "utf-8");
            const pid = parseInt(content.trim(), 10);
            if (isNaN(pid))
                return true;
            // Check if the process is still running
            try {
                process.kill(pid, 0);
                return false; // Process exists, lock is valid
            }
            catch {
                return true; // Process doesn't exist, lock is stale
            }
        }
        catch {
            return true; // Can't read lock file, assume stale
        }
    }
    /**
     * Acquires a file-based lock (with PID tracking for stale lock detection) and executes the provided function.
     * Supports re-entrancy within the same process.
     */
    async withLock(fn) {
        if (MarketplaceService.isLockedInProcess) {
            return await fn();
        }
        let lockFd;
        const maxRetries = 600; // 60 seconds total
        const retryDelay = 100;
        for (let i = 0; i < maxRetries; i++) {
            try {
                lockFd = await fs.open(this.lockPath, "wx");
                break;
            }
            catch (error) {
                if (error &&
                    typeof error === "object" &&
                    "code" in error &&
                    error.code === "EEXIST") {
                    // Check for stale lock every 60 retries (every ~6 seconds)
                    if (i > 0 && i % 60 === 0) {
                        const stale = await this.isStaleLock();
                        if (stale) {
                            await fs.unlink(this.lockPath).catch(() => { });
                            continue;
                        }
                    }
                    await new Promise((resolve) => setTimeout(resolve, retryDelay));
                    continue;
                }
                throw error;
            }
        }
        if (!lockFd) {
            throw new Error(`Failed to acquire marketplace lock after ${maxRetries} retries. If no other wave-agent process is running, please delete ${this.lockPath}`);
        }
        // Write PID into the lock file for stale lock detection
        await fs.writeFile(this.lockPath, String(process.pid), "utf-8");
        MarketplaceService.isLockedInProcess = true;
        try {
            return await fn();
        }
        finally {
            MarketplaceService.isLockedInProcess = false;
            await lockFd.close();
            await fs.unlink(this.lockPath).catch(() => { });
        }
    }
    /**
     * Loads the known marketplaces registry
     */
    async getKnownMarketplaces() {
        if (!existsSync(this.knownMarketplacesPath)) {
            return {
                marketplaces: [
                    {
                        ...MarketplaceService.BUILTIN_MARKETPLACE,
                        isBuiltin: true,
                    },
                ],
            };
        }
        try {
            const content = await fs.readFile(this.knownMarketplacesPath, "utf-8");
            if (!content.trim()) {
                return {
                    marketplaces: [
                        {
                            ...MarketplaceService.BUILTIN_MARKETPLACE,
                            isBuiltin: true,
                        },
                    ],
                };
            }
            return JSON.parse(content);
        }
        catch (error) {
            console.error("Failed to load known marketplaces:", error);
            return { marketplaces: [] };
        }
    }
    /**
     * Loads the installed plugins registry
     */
    async getInstalledPlugins() {
        if (!existsSync(this.installedPluginsPath)) {
            return { plugins: [] };
        }
        try {
            const content = await fs.readFile(this.installedPluginsPath, "utf-8");
            if (!content.trim()) {
                return { plugins: [] };
            }
            return JSON.parse(content);
        }
        catch (error) {
            console.error("Failed to load installed plugins:", error);
            return { plugins: [] };
        }
    }
    /**
     * Saves the known marketplaces registry
     */
    async saveKnownMarketplaces(registry) {
        const tmpPath = `${this.knownMarketplacesPath}.tmp`;
        await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
        await fs.rename(tmpPath, this.knownMarketplacesPath);
    }
    /**
     * Saves the installed plugins registry
     */
    async saveInstalledPlugins(registry) {
        const tmpPath = `${this.installedPluginsPath}.tmp`;
        await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
        await fs.rename(tmpPath, this.installedPluginsPath);
    }
    /**
     * Loads a marketplace manifest from a local path
     */
    async loadMarketplaceManifest(marketplacePath) {
        const manifestPath = path.join(marketplacePath, ".wave-plugin", "marketplace.json");
        if (!existsSync(manifestPath)) {
            throw new Error(`Marketplace manifest not found at ${manifestPath}`);
        }
        const content = await fs.readFile(manifestPath, "utf-8");
        const manifest = JSON.parse(content);
        // Basic validation
        if (!manifest.name ||
            !manifest.plugins ||
            !Array.isArray(manifest.plugins)) {
            throw new Error(`Invalid marketplace manifest at ${manifestPath}`);
        }
        return manifest;
    }
    /**
     * Resolves the local path for a marketplace
     */
    getMarketplacePath(marketplace) {
        if (marketplace.source.source === "directory") {
            return marketplace.source.path;
        }
        else if (marketplace.source.source === "github") {
            return path.join(this.marketplacesDir, marketplace.source.repo);
        }
        else {
            // For general git, use a hash of the URL to avoid path issues
            const hash = crypto
                .createHash("md5")
                .update(marketplace.source.url)
                .digest("hex");
            return path.join(this.marketplacesDir, hash);
        }
    }
    /**
     * Adds a new marketplace (local directory, GitHub repo, or Git URL)
     */
    async addMarketplace(input) {
        return this.withLock(async () => {
            let marketplace;
            const isFullUrl = input.startsWith("http://") ||
                input.startsWith("https://") ||
                input.startsWith("git@") ||
                input.startsWith("ssh://");
            if (isFullUrl ||
                (input.includes("/") &&
                    !path.isAbsolute(input) &&
                    !input.startsWith("."))) {
                // Git or GitHub repo
                let urlOrRepo = input;
                let ref;
                if (input.includes("#")) {
                    [urlOrRepo, ref] = input.split("#");
                }
                const tempMarketplace = isFullUrl
                    ? { name: "", source: { source: "git", url: urlOrRepo, ref } }
                    : { name: "", source: { source: "github", repo: urlOrRepo, ref } };
                const targetPath = this.getMarketplacePath(tempMarketplace);
                if (!existsSync(targetPath)) {
                    try {
                        await this.gitService.clone(urlOrRepo, targetPath, ref);
                    }
                    catch (error) {
                        throw new Error(`Failed to add marketplace from Git: ${error instanceof Error ? error.message : String(error)}`);
                    }
                }
                let manifest;
                try {
                    manifest = await this.loadMarketplaceManifest(targetPath);
                }
                catch (error) {
                    throw new Error(`Failed to load manifest from cloned repository: ${error instanceof Error ? error.message : String(error)}`);
                }
                marketplace = {
                    name: manifest.name,
                    source: isFullUrl
                        ? { source: "git", url: urlOrRepo, ref }
                        : { source: "github", repo: urlOrRepo, ref },
                    autoUpdate: false,
                    lastUpdated: new Date().toISOString(),
                };
            }
            else {
                // Local directory format
                const absolutePath = path.resolve(input);
                let manifest;
                try {
                    manifest = await this.loadMarketplaceManifest(absolutePath);
                }
                catch (error) {
                    throw new Error(`Failed to load manifest from directory: ${error instanceof Error ? error.message : String(error)}`);
                }
                marketplace = {
                    name: manifest.name,
                    source: { source: "directory", path: absolutePath },
                    autoUpdate: false,
                    lastUpdated: new Date().toISOString(),
                };
            }
            const registry = await this.getKnownMarketplaces();
            // Check if already exists
            const existingIndex = registry.marketplaces.findIndex((m) => m.name === marketplace.name);
            if (existingIndex >= 0) {
                registry.marketplaces[existingIndex] = marketplace;
            }
            else {
                registry.marketplaces.push(marketplace);
            }
            // Ensure builtin is included if we are creating the file for the first time
            // and it hasn't been explicitly removed yet.
            // (getKnownMarketplaces already handles the default injection)
            await this.saveKnownMarketplaces(registry);
            return marketplace;
        });
    }
    /**
     * Lists all registered marketplaces
     */
    async listMarketplaces() {
        const registry = await this.getKnownMarketplaces();
        return registry.marketplaces.map((m) => ({
            ...m,
            isBuiltin: m.name === MarketplaceService.BUILTIN_MARKETPLACE.name,
        }));
    }
    /**
     * Removes a marketplace by name
     */
    async removeMarketplace(name) {
        return this.withLock(async () => {
            const registry = await this.getKnownMarketplaces();
            const initialCount = registry.marketplaces.length;
            registry.marketplaces = registry.marketplaces.filter((m) => m.name !== name);
            if (registry.marketplaces.length === initialCount) {
                throw new Error(`Marketplace ${name} not found`);
            }
            await this.saveKnownMarketplaces(registry);
        });
    }
    /**
     * Updates a specific marketplace or all marketplaces
     */
    async updateMarketplace(name, options) {
        return this.withLock(async () => {
            const registry = await this.getKnownMarketplaces();
            const toUpdate = name
                ? registry.marketplaces.filter((m) => m.name === name)
                : registry.marketplaces;
            if (name && toUpdate.length === 0) {
                throw new Error(`Marketplace ${name} not found`);
            }
            const isGitAvailable = await this.gitService.isGitAvailable();
            const errors = [];
            for (const marketplace of toUpdate) {
                try {
                    if (marketplace.source.source === "github" ||
                        marketplace.source.source === "git") {
                        if (!isGitAvailable) {
                            console.warn(`Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`);
                            continue;
                        }
                        const targetPath = this.getMarketplacePath(marketplace);
                        if (existsSync(targetPath)) {
                            await this.gitService.pull(targetPath);
                        }
                        else {
                            let url;
                            if (marketplace.source.source === "github") {
                                url = marketplace.source.repo;
                            }
                            else {
                                url = marketplace.source.url;
                            }
                            await this.gitService.clone(url, targetPath, marketplace.source.ref);
                        }
                    }
                    // For directory source, we just re-validate the manifest
                    const manifest = await this.loadMarketplaceManifest(this.getMarketplacePath(marketplace));
                    marketplace.lastUpdated = new Date().toISOString();
                    if (options?.updatePlugins) {
                        const installedRegistry = await this.getInstalledPlugins();
                        const pluginsToUpdate = installedRegistry.plugins.filter((p) => p.marketplace === marketplace.name);
                        for (const plugin of pluginsToUpdate) {
                            const pluginEntry = manifest.plugins.find((p) => p.name === plugin.name);
                            if (!pluginEntry) {
                                console.warn(`Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`);
                                try {
                                    await this.uninstallPlugin(`${plugin.name}@${plugin.marketplace}`, plugin.projectPath);
                                }
                                catch (error) {
                                    console.error(`Failed to uninstall orphaned plugin "${plugin.name}" from marketplace "${marketplace.name}":`, error);
                                }
                                continue;
                            }
                            try {
                                await this.installPlugin(`${plugin.name}@${plugin.marketplace}`, plugin.projectPath);
                            }
                            catch (error) {
                                console.error(`Failed to update plugin "${plugin.name}" from marketplace "${marketplace.name}":`, error);
                            }
                        }
                    }
                }
                catch (error) {
                    const msg = `Failed to update marketplace "${marketplace.name}": ${error instanceof Error ? error.message : String(error)}`;
                    console.error(msg);
                    errors.push(msg);
                }
            }
            if (errors.length > 0) {
                throw new Error(`Some marketplaces failed to update:\n${errors.join("\n")}`);
            }
            await this.saveKnownMarketplaces(registry);
        });
    }
    /**
     * Automatically updates all marketplaces that have auto-update enabled
     */
    async autoUpdateAll() {
        return this.withLock(async () => {
            const registry = await this.getKnownMarketplaces();
            const toAutoUpdate = registry.marketplaces.filter((m) => m.autoUpdate);
            for (const marketplace of toAutoUpdate) {
                try {
                    await this.updateMarketplace(marketplace.name, {
                        updatePlugins: true,
                    });
                }
                catch (error) {
                    console.error(`Auto-update failed for marketplace "${marketplace.name}":`, error);
                }
            }
        });
    }
    /**
     * Toggles auto-update for a marketplace
     */
    async toggleAutoUpdate(name, enabled) {
        return this.withLock(async () => {
            const registry = await this.getKnownMarketplaces();
            const marketplace = registry.marketplaces.find((m) => m.name === name);
            if (!marketplace) {
                throw new Error(`Marketplace ${name} not found`);
            }
            marketplace.autoUpdate = enabled;
            await this.saveKnownMarketplaces(registry);
        });
    }
    /**
     * Installs a plugin from a marketplace
     */
    async installPlugin(pluginAtMarketplace, projectPath) {
        return this.withLock(async () => {
            const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
            if (!pluginName || !marketplaceName) {
                throw new Error("Invalid plugin format. Use name@marketplace");
            }
            const marketplaces = await this.listMarketplaces();
            const marketplace = marketplaces.find((m) => m.name === marketplaceName);
            if (!marketplace) {
                throw new Error(`Marketplace ${marketplaceName} not found`);
            }
            const marketplacePath = this.getMarketplacePath(marketplace);
            const manifest = await this.loadMarketplaceManifest(marketplacePath);
            const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
            if (!pluginEntry) {
                throw new Error(`Plugin ${pluginName} not found in marketplace ${marketplaceName}`);
            }
            const isGitSource = pluginEntry.source.startsWith("http://") ||
                pluginEntry.source.startsWith("https://") ||
                pluginEntry.source.startsWith("git@") ||
                pluginEntry.source.startsWith("ssh://");
            let pluginSrcPath;
            let tempCloneDir;
            try {
                if (isGitSource) {
                    tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
                    let url = pluginEntry.source;
                    let ref;
                    if (url.includes("#")) {
                        [url, ref] = url.split("#");
                    }
                    await this.gitService.clone(url, tempCloneDir, ref);
                    pluginSrcPath = tempCloneDir;
                }
                else {
                    pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
                }
                const pluginManifestPath = path.join(pluginSrcPath, ".wave-plugin", "plugin.json");
                if (!existsSync(pluginManifestPath)) {
                    throw new Error(`Plugin manifest not found at ${pluginManifestPath}`);
                }
                const pluginManifestContent = await fs.readFile(pluginManifestPath, "utf-8");
                const pluginManifest = JSON.parse(pluginManifestContent);
                const version = pluginManifest.version || "1.0.0";
                // Atomic installation
                const tmpPluginDir = path.join(this.tmpDir, `${pluginName}-${Date.now()}`);
                try {
                    if (isGitSource) {
                        await fs.rename(pluginSrcPath, tmpPluginDir);
                        tempCloneDir = undefined; // Already moved
                    }
                    else {
                        await fs.cp(pluginSrcPath, tmpPluginDir, { recursive: true });
                    }
                    const cachePath = path.join(this.cacheDir, marketplaceName, pluginName, version);
                    if (existsSync(cachePath)) {
                        await fs.rm(cachePath, { recursive: true, force: true });
                    }
                    await fs.mkdir(path.dirname(cachePath), { recursive: true });
                    await fs.rename(tmpPluginDir, cachePath);
                    const installedRegistry = await this.getInstalledPlugins();
                    const existingIndex = installedRegistry.plugins.findIndex((p) => p.name === pluginName &&
                        p.marketplace === marketplaceName &&
                        p.projectPath === projectPath);
                    const installedPlugin = {
                        name: pluginName,
                        marketplace: marketplaceName,
                        version,
                        cachePath,
                        projectPath,
                    };
                    if (existingIndex >= 0) {
                        installedRegistry.plugins[existingIndex] = installedPlugin;
                    }
                    else {
                        installedRegistry.plugins.push(installedPlugin);
                    }
                    await this.saveInstalledPlugins(installedRegistry);
                    return installedPlugin;
                }
                catch (error) {
                    // Cleanup tmp dir if it exists
                    if (existsSync(tmpPluginDir)) {
                        await fs.rm(tmpPluginDir, { recursive: true, force: true });
                    }
                    throw error;
                }
            }
            catch (error) {
                // Cleanup temp clone dir if it exists
                if (tempCloneDir && existsSync(tempCloneDir)) {
                    await fs.rm(tempCloneDir, { recursive: true, force: true });
                }
                throw new Error(`Failed to install plugin ${pluginName}: ${error instanceof Error ? error.message : String(error)}`);
            }
        });
    }
    /**
     * Uninstalls a plugin
     */
    async uninstallPlugin(pluginAtMarketplace, projectPath) {
        return this.withLock(async () => {
            const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
            if (!pluginName || !marketplaceName) {
                throw new Error("Invalid plugin format. Use name@marketplace");
            }
            const installedRegistry = await this.getInstalledPlugins();
            const pluginIndex = installedRegistry.plugins.findIndex((p) => p.name === pluginName &&
                p.marketplace === marketplaceName &&
                p.projectPath === projectPath);
            if (pluginIndex === -1) {
                throw new Error(`Plugin ${pluginName}@${marketplaceName} is not installed${projectPath ? ` for project ${projectPath}` : ""}`);
            }
            const pluginToRemove = installedRegistry.plugins[pluginIndex];
            // Remove from registry first
            installedRegistry.plugins.splice(pluginIndex, 1);
            await this.saveInstalledPlugins(installedRegistry);
            // Check if any other project is still using this same cache path
            const isStillReferenced = installedRegistry.plugins.some((p) => p.cachePath === pluginToRemove.cachePath);
            // Only remove cached files if no other references exist
            if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
                await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
            }
        });
    }
    /**
     * Updates a plugin (uninstall followed by install)
     */
    async updatePlugin(pluginAtMarketplace) {
        return this.withLock(async () => {
            await this.uninstallPlugin(pluginAtMarketplace);
            return this.installPlugin(pluginAtMarketplace);
        });
    }
}
MarketplaceService.isLockedInProcess = false;
MarketplaceService.BUILTIN_MARKETPLACE = {
    name: "wave-plugins-official",
    source: {
        source: "github",
        repo: "netease-lcap/wave-plugins-official",
    },
    autoUpdate: true,
};
