"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addPlugin = addPlugin;
exports.addPluginV1 = addPluginV1;
exports.generateCombinations = generateCombinations;
const yargs = require("yargs-parser");
const devkit_exports_1 = require("nx/src/devkit-exports");
const devkit_internals_1 = require("nx/src/devkit-internals");
/**
 * Iterates through various forms of plugin options to find the one which does not conflict with the current graph

 */
async function addPlugin(tree, graph, pluginName, createNodesTuple, options, shouldUpdatePackageJsonScripts) {
    return _addPluginInternal(tree, graph, pluginName, (pluginOptions) => new devkit_internals_1.LoadedNxPlugin({
        name: pluginName,
        createNodesV2: createNodesTuple,
    }, {
        plugin: pluginName,
        options: pluginOptions,
    }), options, shouldUpdatePackageJsonScripts);
}
/**
 * @deprecated Use `addPlugin` instead
 * Iterates through various forms of plugin options to find the one which does not conflict with the current graph

 */
async function addPluginV1(tree, graph, pluginName, createNodesTuple, options, shouldUpdatePackageJsonScripts) {
    return _addPluginInternal(tree, graph, pluginName, (pluginOptions) => new devkit_internals_1.LoadedNxPlugin({
        name: pluginName,
        createNodes: createNodesTuple,
    }, {
        plugin: pluginName,
        options: pluginOptions,
    }), options, shouldUpdatePackageJsonScripts);
}
async function _addPluginInternal(tree, graph, pluginName, pluginFactory, options, shouldUpdatePackageJsonScripts) {
    const graphNodes = Object.values(graph.nodes);
    const nxJson = (0, devkit_exports_1.readNxJson)(tree);
    let pluginOptions;
    let projConfigs;
    if (Object.keys(options).length > 0) {
        const combinations = generateCombinations(options);
        optionsLoop: for (const _pluginOptions of combinations) {
            pluginOptions = _pluginOptions;
            nxJson.plugins ??= [];
            if (nxJson.plugins.some((p) => typeof p === 'string'
                ? p === pluginName
                : p.plugin === pluginName && !p.include)) {
                // Plugin has already been added
                return;
            }
            global.NX_GRAPH_CREATION = true;
            try {
                projConfigs = await (0, devkit_internals_1.retrieveProjectConfigurations)([pluginFactory(pluginOptions)], tree.root, nxJson);
            }
            catch (e) {
                // Errors are okay for this because we're only running 1 plugin
                if ((0, devkit_internals_1.isProjectConfigurationsError)(e)) {
                    projConfigs = e.partialProjectConfigurationsResult;
                    // ignore errors from projects with no name
                    if (!e.errors.every(devkit_internals_1.isProjectsWithNoNameError)) {
                        throw e;
                    }
                }
                else {
                    throw e;
                }
            }
            global.NX_GRAPH_CREATION = false;
            for (const projConfig of Object.values(projConfigs.projects)) {
                const node = graphNodes.find((node) => node.data.root === projConfig.root);
                if (!node) {
                    continue;
                }
                for (const targetName in projConfig.targets) {
                    if (node.data.targets[targetName]) {
                        // Conflicting Target Name, check the next one
                        pluginOptions = null;
                        continue optionsLoop;
                    }
                }
            }
            break;
        }
    }
    else {
        // If the plugin does not take in options, we add the plugin with empty options.
        nxJson.plugins ??= [];
        pluginOptions = {};
        global.NX_GRAPH_CREATION = true;
        try {
            projConfigs = await (0, devkit_internals_1.retrieveProjectConfigurations)([pluginFactory(pluginOptions)], tree.root, nxJson);
        }
        catch (e) {
            // Errors are okay for this because we're only running 1 plugin
            if ((0, devkit_internals_1.isProjectConfigurationsError)(e)) {
                projConfigs = e.partialProjectConfigurationsResult;
                // ignore errors from projects with no name
                if (!e.errors.every(devkit_internals_1.isProjectsWithNoNameError)) {
                    throw e;
                }
            }
            else {
                throw e;
            }
        }
        global.NX_GRAPH_CREATION = false;
    }
    if (!pluginOptions) {
        throw new Error('Could not add the plugin in a way which does not conflict with existing targets. Please report this error at: https://github.com/nrwl/nx/issues/new/choose');
    }
    nxJson.plugins.push({
        plugin: pluginName,
        options: pluginOptions,
    });
    (0, devkit_exports_1.updateNxJson)(tree, nxJson);
    if (shouldUpdatePackageJsonScripts) {
        updatePackageScripts(tree, projConfigs);
    }
}
function updatePackageScripts(tree, projectConfigurations) {
    for (const projectConfig of Object.values(projectConfigurations.projects)) {
        const projectRoot = projectConfig.root;
        processProject(tree, projectRoot, projectConfig);
    }
}
function processProject(tree, projectRoot, projectConfiguration) {
    const packageJsonPath = `${projectRoot}/package.json`;
    if (!tree.exists(packageJsonPath)) {
        return;
    }
    const packageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
    if (!packageJson.scripts || !Object.keys(packageJson.scripts).length) {
        return;
    }
    const targetCommands = getInferredTargetCommands(projectConfiguration);
    if (!targetCommands.length) {
        return;
    }
    let hasChanges = false;
    targetCommands.sort((a, b) => b.command.split(/\s/).length - a.command.split(/\s/).length);
    for (const targetCommand of targetCommands) {
        const { command, target, configuration } = targetCommand;
        const targetCommandRegex = new RegExp(`(?<=^|&)((?: )*(?:[^&\\r\\n\\s]+ )*)(${command})((?: [^&\\r\\n\\s]+)*(?: )*)(?=$|&)`, 'g');
        for (const scriptName of Object.keys(packageJson.scripts)) {
            const script = packageJson.scripts[scriptName];
            // quick check for exact match within the script
            if (targetCommandRegex.test(script)) {
                packageJson.scripts[scriptName] = script.replace(targetCommandRegex, configuration
                    ? `$1nx ${target} --configuration=${configuration}$3`
                    : `$1nx ${target}$3`);
                hasChanges = true;
            }
            else {
                /**
                 * Parse script and command to handle the following:
                 * - if command doesn't match script                  => don't replace
                 * - if command has more args                         => don't replace
                 * - if command has same args, regardless of order    => replace removing args
                 * - if command has less args or with different value => replace leaving args
                 */
                const parsedCommand = yargs(command, {
                    configuration: { 'strip-dashed': true },
                });
                // this assumes there are no positional args in the command, everything is a command or subcommand
                const commandCommand = parsedCommand._.join(' ');
                const commandRegex = new RegExp(`(?<=^|&)((?: )*(?:[^&\\r\\n\\s]+ )*)(${commandCommand})((?: [^&\\r\\n\\s]+)*( )*)(?=$|&)`, 'g');
                const matches = script.match(commandRegex);
                if (!matches) {
                    // the command doesn't match the script, don't replace
                    continue;
                }
                for (const match of matches) {
                    // parse the matched command within the script
                    const parsedScript = yargs(match, {
                        configuration: { 'strip-dashed': true },
                    });
                    let hasArgsWithDifferentValues = false;
                    let scriptHasExtraArgs = false;
                    let commandHasExtraArgs = false;
                    for (const [key, value] of Object.entries(parsedCommand)) {
                        if (key === '_') {
                            continue;
                        }
                        if (parsedScript[key] === undefined) {
                            commandHasExtraArgs = true;
                            break;
                        }
                        if (parsedScript[key] !== value) {
                            hasArgsWithDifferentValues = true;
                        }
                    }
                    if (commandHasExtraArgs) {
                        // the command has extra args, don't replace
                        continue;
                    }
                    for (const key of Object.keys(parsedScript)) {
                        if (key === '_') {
                            continue;
                        }
                        if (!parsedCommand[key]) {
                            scriptHasExtraArgs = true;
                            break;
                        }
                    }
                    if (!hasArgsWithDifferentValues && !scriptHasExtraArgs) {
                        // they are the same, replace with the command removing the args
                        const script = packageJson.scripts[scriptName];
                        packageJson.scripts[scriptName] = script.replace(match, match.replace(commandRegex, configuration
                            ? `$1nx ${target} --configuration=${configuration}$4`
                            : `$1nx ${target}$4`));
                        hasChanges = true;
                    }
                    else {
                        // there are different args or the script has extra args, replace with the command leaving the args
                        packageJson.scripts[scriptName] = packageJson.scripts[scriptName].replace(match, match.replace(commandRegex, configuration
                            ? `$1nx ${target} --configuration=${configuration}$3`
                            : `$1nx ${target}$3`));
                        hasChanges = true;
                    }
                }
            }
        }
    }
    if (hasChanges) {
        (0, devkit_exports_1.writeJson)(tree, packageJsonPath, packageJson);
    }
}
function getInferredTargetCommands(project) {
    const targetCommands = [];
    for (const [targetName, target] of Object.entries(project.targets ?? {})) {
        if (target.command) {
            targetCommands.push({ command: target.command, target: targetName });
        }
        else if (target.executor === 'nx:run-commands' &&
            target.options?.command) {
            targetCommands.push({
                command: target.options.command,
                target: targetName,
            });
        }
        if (!target.configurations) {
            continue;
        }
        for (const [configurationName, configuration] of Object.entries(target.configurations)) {
            if (configuration.command) {
                targetCommands.push({
                    command: configuration.command,
                    target: targetName,
                    configuration: configurationName,
                });
            }
            else if (target.executor === 'nx:run-commands' &&
                configuration.options?.command) {
                targetCommands.push({
                    command: configuration.options.command,
                    target: targetName,
                    configuration: configurationName,
                });
            }
        }
    }
    return targetCommands;
}
function generateCombinations(input) {
    // This is reversed so that combinations have the first defined property updated first
    const keys = Object.keys(input).reverse();
    return _generateCombinations(Object.values(input).reverse()).map((combination) => {
        const result = {};
        combination.reverse().forEach((combo, i) => {
            result[keys[keys.length - i - 1]] = combo;
        });
        return result;
    });
}
/**
 * Generate all possible combinations of a 2-dimensional array.
 *
 * Useful for generating all possible combinations of options for a plugin
 */
function _generateCombinations(input) {
    if (input.length === 0) {
        return [[]];
    }
    else {
        const [first, ...rest] = input;
        const partialCombinations = _generateCombinations(rest);
        return first.flatMap((value) => partialCombinations.map((combination) => [value, ...combination]));
    }
}
