/* eslint-disable @typescript-eslint/require-await */
/**
 * Plugin Manager
 * Handles installation and removal of framework plugins.
 */
import { SpawnUtil } from '../cli/utils/spawn.js';
import { readEnvString } from '../common/ExternalServiceUtils.js';
import { esmDirname, resolvePackageManager } from '../common/index.js';
import { Logger } from '../config/logger.js';
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import { execSync } from '../node-singletons/child-process.js';
import { existsSync, fsPromises as fs } from '../node-singletons/fs.js';
import * as path from '../node-singletons/path.js';
import { PluginRegistry } from './PluginRegistry.js';
const __dirname = esmDirname(import.meta.url);
const MAX_PACKAGE_ROOT_SEARCH_DEPTH = 20;
function findPackageRoot(startDir) {
    let current = startDir;
    for (let i = 0; i < MAX_PACKAGE_ROOT_SEARCH_DEPTH; i++) {
        if (existsSync(path.join(current, 'package.json')))
            return current;
        const parent = path.dirname(current);
        if (parent === current)
            break;
        current = parent;
    }
    // Fallback to a reasonable default if package.json isn't found.
    return path.resolve(startDir, '../..');
}
const getProjectCwd = () => process.cwd();
const getProjectRootEnv = () => readEnvString('ZINTRUST_PROJECT_ROOT');
const getAllowPostInstallEnv = () => readEnvString('ZINTRUST_ALLOW_POSTINSTALL').trim();
function resolveProjectRoot() {
    const projectRootEnv = getProjectRootEnv();
    if (projectRootEnv.trim().length > 0) {
        return projectRootEnv.trim();
    }
    return getProjectCwd();
}
function resolveTemplateRootOrThrow() {
    const packageRoot = findPackageRoot(__dirname);
    const candidates = [
        // Monorepo/dev layout
        path.join(packageRoot, 'src', 'templates'),
        // Packed layout (if templates are shipped without src)
        path.join(packageRoot, 'templates'),
        // Common build output layout
        path.join(packageRoot, 'dist', 'templates'),
    ];
    for (const candidate of candidates) {
        if (existsSync(candidate))
            return candidate;
    }
    throw ErrorFactory.createNotFoundError('Plugin templates directory not found', {
        candidates,
        packageRoot,
    });
}
function isStringRecord(value) {
    if (typeof value !== 'object' || value === null)
        return false;
    for (const v of Object.values(value)) {
        if (typeof v !== 'string')
            return false;
    }
    return true;
}
function isStringArray(value) {
    return Array.isArray(value) && value.every((v) => typeof v === 'string');
}
function parsePackageJsonDeps(text) {
    const parsed = JSON.parse(text);
    if (typeof parsed !== 'object' || parsed === null)
        return {};
    const obj = parsed;
    const dependencies = isStringRecord(obj['dependencies']) ? obj['dependencies'] : undefined;
    const devDependencies = isStringRecord(obj['devDependencies'])
        ? obj['devDependencies']
        : undefined;
    return { dependencies, devDependencies };
}
function getPluginOrThrow(pluginId) {
    const resolvedId = PluginManager.resolveId(pluginId);
    if (resolvedId === null) {
        throw ErrorFactory.createNotFoundError(`Plugin ${pluginId} not found`, { pluginId });
    }
    return { resolvedId, plugin: PluginRegistry[resolvedId] };
}
async function npmInstall(packages, options) {
    if (packages.length === 0)
        return;
    Logger.info(`Installing ${options.label}: ${packages.join(', ')}...`);
    const projectRoot = options.projectRoot ?? resolveProjectRoot();
    const pm = options.packageManager ?? resolvePackageManager();
    // Legacy behavior: npm uses execSync in this framework.
    // This keeps behavior predictable for CI/test environments and matches existing unit tests.
    if (pm === 'npm') {
        const cmd = options.dev
            ? `npm install -D ${packages.join(' ')}`
            : `npm install ${packages.join(' ')}`;
        try {
            execSync(cmd, { cwd: projectRoot, stdio: 'inherit' });
            return;
        }
        catch (error) {
            ErrorFactory.createCliError(`Failed to install ${options.label}`, { error });
            throw error;
        }
    }
    let cmd;
    let args = [];
    switch (pm) {
        case 'pnpm':
            cmd = 'pnpm';
            args = ['add', ...packages, ...(options.dev ? ['-D'] : [])];
            break;
        case 'yarn':
            cmd = 'yarn';
            args = ['add', ...packages, ...(options.dev ? ['--dev'] : [])];
            break;
        case 'npm':
        default:
            cmd = 'npm';
            args = ['install', ...(options.dev ? ['--save-dev'] : []), ...packages];
            break;
    }
    try {
        // Use async spawn for all package managers to avoid blocking event loop
        const exit = await SpawnUtil.spawnAndWait({ command: cmd, args, cwd: projectRoot });
        if (exit !== 0) {
            throw ErrorFactory.createCliError(`Package manager ${pm} failed to install ${options.label}`, {
                exit,
            });
        }
    }
    catch (error) {
        ErrorFactory.createCliError(`Failed to install ${options.label}`, { error });
        throw error;
    }
}
async function copyPluginTemplates(plugin) {
    const templateRoot = resolveTemplateRootOrThrow();
    const projectRoot = resolveProjectRoot();
    // Parallelize file operations using Promise.all
    await Promise.all(plugin.templates.map(async (template) => {
        const sourcePath = path.join(templateRoot, template.source);
        const destPath = path.join(projectRoot, template.destination);
        // Prevent path traversal: resolved destination must be within project root
        const resolvedDest = path.resolve(destPath);
        const resolvedProjectRoot = path.resolve(projectRoot);
        if (!(resolvedDest === resolvedProjectRoot ||
            resolvedDest.startsWith(resolvedProjectRoot + path.sep))) {
            throw ErrorFactory.createCliError(`Invalid template destination: ${template.destination}`, {
                destination: template.destination,
            });
        }
        Logger.info(`Copying ${template.source} to ${template.destination}...`);
        try {
            // Ensure destination directory exists
            await fs.mkdir(path.dirname(destPath), { recursive: true });
            // Read and write file
            const content = await fs.readFile(sourcePath, 'utf-8');
            await fs.writeFile(destPath, content, 'utf-8');
        }
        catch (error) {
            ErrorFactory.createCliError(`Failed to copy template ${template.source}`, { error });
            throw error;
        }
    }));
}
async function ensurePluginAutoImports(plugin) {
    const rawImports = plugin.autoImports;
    if (!isStringArray(rawImports) || rawImports.length === 0)
        return;
    const imports = rawImports;
    const projectRoot = resolveProjectRoot();
    const pluginFile = path.join(projectRoot, 'src', 'zintrust.plugins.ts');
    await fs.mkdir(path.dirname(pluginFile), { recursive: true });
    const header = '/**\n' +
        ' * ZinTrust plugin auto-imports\n' +
        ' *\n' +
        ' * This file is managed by `zin plugin install` and contains side-effect\n' +
        ' * imports that register optional adapters/drivers into core registries.\n' +
        ' */\n\n';
    let current = '';
    try {
        current = await fs.readFile(pluginFile, 'utf-8');
    }
    catch {
        current = '';
    }
    const lines = [];
    if (current.trim().length === 0) {
        lines.push(header.trimEnd());
    }
    else {
        lines.push(current.trimEnd());
    }
    const existing = new Set();
    for (const line of lines.join('\n').split(/\r?\n/)) {
        const m = new RegExp(/^\s*import\s+['"]([^'"]+)['"];\s*$/).exec(line);
        if (m)
            existing.add(m[1]);
    }
    let appended = 0;
    for (const spec of imports) {
        if (typeof spec !== 'string' || spec.trim() === '')
            continue;
        if (existing.has(spec))
            continue;
        lines.push(`import '${spec}';`);
        existing.add(spec);
        appended += 1;
    }
    if (appended === 0)
        return;
    const next = lines.join('\n') + '\n';
    await fs.writeFile(pluginFile, next, 'utf-8');
}
function runPostInstall(plugin) {
    if (!plugin.postInstall)
        return;
    const projectRoot = resolveProjectRoot();
    if (plugin.postInstall.command !== undefined) {
        // Post-install command execution is opt-in. To avoid arbitrary command execution
        // and reduce supply-chain risk, we only execute when ZINTRUST_ALLOW_POSTINSTALL=1
        const allow = getAllowPostInstallEnv() === '1';
        if (allow) {
            Logger.info(`Running post-install command: ${plugin.postInstall.command}...`);
            try {
                execSync(plugin.postInstall.command, {
                    stdio: 'inherit',
                    cwd: projectRoot,
                });
            }
            catch (error) {
                ErrorFactory.createCliError('Post-install command failed', { error });
            }
        }
        else {
            Logger.info(`Post-install command available but not executed (ZINTRUST_ALLOW_POSTINSTALL!=1): ${plugin.postInstall.command}`);
        }
    }
    if (plugin.postInstall.message !== undefined && plugin.postInstall.message.length > 0) {
        Logger.info('----------------------------------------');
        Logger.info(plugin.postInstall.message);
        Logger.info('----------------------------------------');
    }
}
export const PluginManager = Object.freeze({
    /**
     * List all available plugins
     */
    list() {
        return PluginRegistry;
    },
    /**
     * Resolve a plugin ID from an alias or full ID
     */
    resolveId(idOrAlias) {
        if (PluginRegistry[idOrAlias] !== undefined)
            return idOrAlias;
        for (const [id, plugin] of Object.entries(PluginRegistry)) {
            if (plugin.aliases.includes(idOrAlias)) {
                return id;
            }
        }
        return null;
    },
    /**
     * Check if a plugin is currently installed
     */
    async isInstalled(pluginId) {
        const resolvedId = PluginManager.resolveId(pluginId);
        if (resolvedId === null) {
            throw ErrorFactory.createNotFoundError(`Plugin ${pluginId} not found`, { pluginId });
        }
        const plugin = PluginRegistry[resolvedId];
        const projectRoot = resolveProjectRoot();
        // Dependency-only plugins (no templates): consider installed when required deps are present.
        if (plugin.templates.length === 0) {
            try {
                const packageJsonPath = path.join(projectRoot, 'package.json');
                const packageJsonText = await fs.readFile(packageJsonPath, 'utf-8');
                const packageJson = parsePackageJsonDeps(packageJsonText);
                const hasDeps = plugin.dependencies.every((dep) => packageJson.dependencies?.[dep] ?? packageJson.devDependencies?.[dep] ?? '');
                const hasDevDeps = plugin.devDependencies.every((dep) => packageJson.devDependencies?.[dep] ?? packageJson.dependencies?.[dep] ?? '');
                return hasDeps && hasDevDeps;
            }
            catch {
                return false;
            }
        }
        // Check if the main template file exists in the destination
        // We assume if the first template exists, the plugin is "installed"
        if (plugin.templates.length > 0) {
            const destPath = path.join(projectRoot, plugin.templates[0].destination);
            try {
                await fs.access(destPath);
                // Also check if dependencies are in package.json
                const packageJsonPath = path.join(projectRoot, 'package.json');
                const packageJsonText = await fs.readFile(packageJsonPath, 'utf-8');
                const packageJson = parsePackageJsonDeps(packageJsonText);
                const hasDeps = plugin.dependencies.every((dep) => packageJson.dependencies?.[dep] ?? packageJson.devDependencies?.[dep] ?? '');
                return hasDeps;
            }
            catch {
                ErrorFactory.createCliError(`Plugin ${plugin.name} not installed (missing files or dependencies)`);
                return false;
            }
        }
        return false;
    },
    /**
     * Install a plugin
     */
    async install(pluginId, options) {
        const { plugin } = getPluginOrThrow(pluginId);
        Logger.info(`Installing plugin: ${plugin.name}...`);
        // 1. Install dependencies (use SpawnUtil and support multiple package managers)
        await npmInstall(plugin.dependencies, {
            dev: false,
            label: 'dependencies',
            packageManager: options?.packageManager,
        });
        await npmInstall(plugin.devDependencies, {
            dev: true,
            label: 'dev dependencies',
            packageManager: options?.packageManager,
        });
        // 2. Copy templates
        await copyPluginTemplates(plugin);
        // 2b. Ensure plugin-side effects are imported by the project
        await ensurePluginAutoImports(plugin);
        // 3. Post-Install (still executed via execSync - opt-in)
        runPostInstall(plugin);
        Logger.info(`✓ Plugin ${plugin.name} installed successfully`);
    },
    /**
     * Uninstall a plugin
     * Note: This does NOT uninstall dependencies to avoid breaking other things,
     * but it does revert the code to the "stub" state if possible, or delete the file.
     */
    async uninstall(pluginId) {
        const resolvedId = PluginManager.resolveId(pluginId);
        if (resolvedId === null) {
            throw ErrorFactory.createNotFoundError(`Plugin ${pluginId} not found`, { pluginId });
        }
        const plugin = PluginRegistry[resolvedId];
        Logger.info(`Uninstalling plugin: ${plugin.name}...`);
        // Revert templates to stubs
        // For now, we'll just delete the file if it's an adapter,
        // BUT wait - we need the stubs to exist for the framework to compile if they are referenced.
        // However, in the "Template" architecture, the stubs ARE the default state.
        // So "uninstalling" might mean "restoring the stub".
        // Since we don't have a "stub repository" easily accessible here without reading from git or a backup,
        // and the user might have modified the file, "uninstall" is tricky.
        // Strategy:
        // 1. Warn the user that this will delete the file.
        // 2. If it's a core adapter, we should probably restore a basic stub.
        // For this iteration, we will just warn and delete, assuming the user knows what they are doing.
        // OR, better: We can have a "stubs" folder in templates too?
        // Actually, the "stubs" are what is currently in the codebase.
        // When we "install", we overwrite the stub.
        // When we "uninstall", we should ideally restore the stub.
        // Let's check if we have a stub backup. If not, maybe we just leave the file but warn?
        // Or maybe we don't support "uninstall" fully yet, just "install".
        Logger.warn('Uninstalling plugins is not fully automated yet. You may need to manually revert file changes.');
        // We can at least try to remove the dependencies if they are not used elsewhere?
        // Too risky.
        Logger.info(`✓ Plugin ${plugin.name} uninstalled (files preserved, please revert manually if needed)`);
    },
});
