import assert from 'assert';
import { existsSync, readFileSync } from 'fs';
import path, { dirname, resolve } from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import chalk from 'chalk';
import * as _ from 'lodash-es';
import Environment from 'yeoman-environment';
import { QueuedAdapter } from '@yeoman/adapter';
import { CLI_NAME, logger } from './utils.mjs';
import { createJHipsterLogger, packageNameToNamespace } from '../generators/base/support/index.mjs';
import { parseBlueprintInfo, loadBlueprintsFromConfiguration, mergeBlueprints } from '../generators/base/internal/index.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const jhipsterDevBlueprintPath = process.env.JHIPSTER_DEV_BLUEPRINT === 'true' ? path.join(__dirname, '../.blueprint') : undefined;
const devBlueprintNamespace = '@begcode/begcode-dev';
const localBlueprintNamespace = '@begcode/begcode-local';
function loadYoRc(filePath = '.yo-rc.json') {
    if (!existsSync(filePath)) {
        return undefined;
    }
    return JSON.parse(readFileSync(filePath, { encoding: 'utf-8' }));
}
const createEnvironment = (options = {}) => {
    options.adapter = options.adapter ?? new QueuedAdapter({ log: createJHipsterLogger() });
    return new Environment({ newErrorHandler: true, ...options });
};
export default class EnvironmentBuilder {
    devBlueprintPath;
    localBlueprintPath;
    localBlueprintExists;
    static create(options = {}) {
        const env = createEnvironment(options);
        env.setMaxListeners(0);
        return new EnvironmentBuilder(env);
    }
    static async createEnv(...args) {
        const builder = await EnvironmentBuilder.createDefaultBuilder(...args);
        return builder.getEnvironment();
    }
    static async createDefaultBuilder(...args) {
        return EnvironmentBuilder.create(...args).prepare();
    }
    constructor(env) {
        this.env = env;
    }
    async prepare({ blueprints, lookups, devBlueprintPath = jhipsterDevBlueprintPath } = {}) {
        this.devBlueprintPath = existsSync(devBlueprintPath) ? devBlueprintPath : undefined;
        this.localBlueprintPath = path.join(process.cwd(), '.blueprint');
        this.localBlueprintExists = this.localBlueprintPath !== this.devBlueprintPath && existsSync(this.localBlueprintPath);
        await this._lookupJHipster();
        await this._lookupLocalBlueprint();
        await this._lookupDevBlueprint();
        this._loadBlueprints(blueprints);
        await this._lookups(lookups);
        await this._lookupBlueprints();
        await this._loadSharedOptions();
        return this;
    }
    getBlueprintsNamespaces() {
        return [
            ...Object.keys(this._blueprintsWithVersion).map(packageName => packageNameToNamespace(packageName)),
            localBlueprintNamespace,
            ...(this.devBlueprintPath ? [devBlueprintNamespace] : []),
        ];
    }
    getBlueprintsOption() {
        return Object.entries(this._blueprintsWithVersion)
            .map(([packageName, packageVersion]) => (packageVersion ? `${packageName}@${packageVersion}` : packageName))
            .join(',');
    }
    async _lookupJHipster() {
        const sourceRoot = path.basename(path.join(__dirname, '..'));
        let packagePath;
        let lookup;
        if (sourceRoot === 'generator-begcode') {
            packagePath = path.join(__dirname, '..');
            lookup = 'generators';
        }
        else {
            packagePath = path.join(__dirname, '../..');
            lookup = `${sourceRoot}/generators`;
        }
        (await this.env.lookup({ packagePaths: [packagePath], lookups: [lookup] })).forEach(generator => {
            assert(generator.namespace.startsWith(`${CLI_NAME}:`), `Error on the registered namespace ${generator.namespace}, make sure your folder is called generator-begcode.`);
        });
        return this;
    }
    async _lookupLocalBlueprint() {
        if (this.localBlueprintExists) {
            const generators = await this.env.lookup({ packagePaths: [this.localBlueprintPath], lookups: ['.'] });
            if (generators.length > 0) {
                this.env.alias(/^@begcode\/begcode-local(:(.*))?$/, '.blueprint$1');
                this.env.sharedOptions.composeWithLocalBlueprint = true;
            }
        }
        return this;
    }
    async _lookupDevBlueprint() {
        const generators = await this.env.lookup({ packagePaths: [this.devBlueprintPath], lookups: ['.'] });
        if (generators.length > 0) {
            this.env.alias(/^@begcode\/begcode-dev(:(.*))?$/, '.blueprint$1');
        }
        return this;
    }
    async _lookups(lookups = []) {
        lookups = [].concat(lookups);
        for (const lookup of lookups) {
            await this.env.lookup(lookup);
        }
        return this;
    }
    _loadBlueprints(blueprints) {
        this._blueprintsWithVersion = {
            ...this._getAllBlueprintsWithVersion(),
            ...blueprints,
        };
        return this;
    }
    async _lookupBlueprints(options) {
        const missingBlueprints = Object.keys(this._blueprintsWithVersion).filter(blueprint => !this.env.isPackageRegistered(packageNameToNamespace(blueprint)));
        if (missingBlueprints && missingBlueprints.length > 0) {
            await this.env.lookup({ ...options, filterPaths: true, packagePatterns: missingBlueprints });
        }
        return this;
    }
    async lookupGenerators(generators, options = {}) {
        await this.env.lookup({ filterPaths: true, ...options, packagePatterns: generators });
        return this;
    }
    async _loadSharedOptions() {
        const blueprintsPackagePath = await this._getBlueprintPackagePaths();
        if (blueprintsPackagePath) {
            const sharedOptions = (await this._getSharedOptions(blueprintsPackagePath)) ?? {};
            Object.assign(this.env.sharedOptions, sharedOptions);
        }
        return this;
    }
    async getBlueprintCommands() {
        let blueprintsPackagePath = await this._getBlueprintPackagePaths();
        if (this.devBlueprintPath) {
            blueprintsPackagePath = blueprintsPackagePath ?? [];
            blueprintsPackagePath.push([devBlueprintNamespace, this.devBlueprintPath]);
            if (this.localBlueprintExists) {
                blueprintsPackagePath.push([localBlueprintNamespace, this.localBlueprintPath]);
            }
        }
        return this._getBlueprintCommands(blueprintsPackagePath);
    }
    getEnvironment() {
        return this.env;
    }
    _getBlueprintsFromArgv() {
        const blueprintNames = [];
        const indexOfBlueprintArgv = process.argv.indexOf('--blueprint');
        if (indexOfBlueprintArgv > -1) {
            blueprintNames.push(process.argv[indexOfBlueprintArgv + 1]);
        }
        const indexOfBlueprintsArgv = process.argv.indexOf('--blueprints');
        if (indexOfBlueprintsArgv > -1) {
            blueprintNames.push(...process.argv[indexOfBlueprintsArgv + 1].split(','));
        }
        if (!blueprintNames.length) {
            return [];
        }
        return blueprintNames.map(v => parseBlueprintInfo(v));
    }
    _getBlueprintsFromYoRc() {
        const yoRc = loadYoRc();
        if (!yoRc || !yoRc['generator-begcode']) {
            return [];
        }
        return loadBlueprintsFromConfiguration(yoRc['generator-begcode']);
    }
    _getAllBlueprintsWithVersion() {
        return mergeBlueprints(this._getBlueprintsFromArgv(), this._getBlueprintsFromYoRc()).reduce((acc, blueprint) => {
            acc[blueprint.name] = blueprint.version;
            return acc;
        }, {});
    }
    async _getBlueprintPackagePaths() {
        const blueprints = this._blueprintsWithVersion;
        if (!blueprints || Object.keys(blueprints).length === 0) {
            return undefined;
        }
        const blueprintsToInstall = Object.entries(blueprints)
            .filter(([blueprint, _version]) => {
            const namespace = packageNameToNamespace(blueprint);
            if (!this.env.getPackagePath(namespace)) {
                this.env.lookupLocalPackages(blueprint);
            }
            return !this.env.getPackagePath(namespace);
        })
            .reduce((acc, [blueprint, version]) => {
            acc[blueprint] = version;
            return acc;
        }, {});
        if (Object.keys(blueprintsToInstall).length > 0) {
            await this.env.installLocalGenerators(blueprintsToInstall);
        }
        return Object.entries(blueprints).map(([blueprint, _version]) => {
            const namespace = packageNameToNamespace(blueprint);
            const packagePath = this.env.getPackagePath(namespace);
            if (!packagePath) {
                logger.fatal(`The ${chalk.yellow(blueprint)} blueprint provided is not installed. Please install it using command ${chalk.yellow(`npm i -g ${blueprint}`)}`);
            }
            return [blueprint, packagePath];
        });
    }
    async _getBlueprintCommands(blueprintPackagePaths) {
        if (!blueprintPackagePaths) {
            return undefined;
        }
        let result;
        for (const [blueprint, packagePath] of blueprintPackagePaths) {
            let blueprintCommand;
            const blueprintCommandFile = `${packagePath}/cli/commands`;
            const blueprintCommandExtension = ['.js', '.cjs', '.mjs'].find(extension => existsSync(`${blueprintCommandFile}${extension}`));
            if (blueprintCommandExtension) {
                const blueprintCommandsUrl = pathToFileURL(resolve(`${blueprintCommandFile}${blueprintCommandExtension}`));
                try {
                    blueprintCommand = (await import(blueprintCommandsUrl)).default;
                    const blueprintCommands = _.cloneDeep(blueprintCommand);
                    Object.entries(blueprintCommands).forEach(([_command, commandSpec]) => {
                        commandSpec.blueprint = commandSpec.blueprint || blueprint;
                    });
                    result = { ...result, ...blueprintCommands };
                }
                catch (e) {
                    const msg = `Error parsing custom commands found within blueprint: ${blueprint} at ${blueprintCommandsUrl}`;
                    console.info(`${chalk.green.bold('INFO!')} ${msg}`);
                }
            }
            else {
                const msg = `No custom commands found within blueprint: ${blueprint} at ${packagePath}`;
                console.info(`${chalk.green.bold('INFO!')} ${msg}`);
            }
        }
        return result;
    }
    async _getSharedOptions(blueprintPackagePaths) {
        function joiner(objValue, srcValue) {
            if (objValue === undefined) {
                return srcValue;
            }
            if (Array.isArray(objValue) && Array.isArray(srcValue)) {
                return objValue.concat(srcValue);
            }
            if (Array.isArray(objValue)) {
                return [...objValue, srcValue];
            }
            if (Array.isArray(srcValue)) {
                return [objValue, ...srcValue];
            }
            return [objValue, srcValue];
        }
        async function loadSharedOptionsFromFile(sharedOptionsBase, msg, errorMsg) {
            try {
                const baseExtension = ['.js', '.cjs', '.mjs'].find(extension => existsSync(resolve(`${sharedOptionsBase}${extension}`)));
                if (baseExtension) {
                    const { default: opts } = await import(pathToFileURL(resolve(`${sharedOptionsBase}${baseExtension}`)));
                    if (msg) {
                        console.info(`${chalk.green.bold('INFO!')} ${msg}`);
                    }
                    return opts;
                }
            }
            catch (e) {
                if (errorMsg) {
                    console.info(`${chalk.green.bold('INFO!')} ${errorMsg}`, e);
                }
            }
            return {};
        }
        const localPath = './.jhipster/sharedOptions';
        let result = await loadSharedOptionsFromFile(localPath, `SharedOptions found at local config ${localPath}`);
        if (!blueprintPackagePaths) {
            return undefined;
        }
        for (const [blueprint, packagePath] of blueprintPackagePaths) {
            const errorMsg = `No custom sharedOptions found within blueprint: ${blueprint} at ${packagePath}`;
            const opts = await loadSharedOptionsFromFile(`${packagePath}/cli/sharedOptions`, undefined, errorMsg);
            result = _.mergeWith(result, opts, joiner);
        }
        return result;
    }
}
