import { kebabCase } from 'lodash-es';
import chalk from 'chalk';
import { glob } from 'glob';
import BaseGenerator from '../base-application/index.mjs';
import statistics from '../statistics.mjs';
import { JAVA_COMPATIBLE_VERSIONS, JAVA_VERSION, SERVER_MAIN_RES_DIR } from '../generator-constants.mjs';
import { GENERATOR_BOOTSTRAP_APPLICATION, GENERATOR_HEROKU } from '../generator-list.mjs';
import { mavenProfileContent } from './templates.mjs';
import { createPomStorage } from '../maven/support/pom-store.mjs';
import { addGradlePluginCallback, applyFromGradleCallback } from '../gradle/internal/needles.mjs';
export default class HerokuGenerator extends BaseGenerator {
    hasHerokuCli;
    herokuAppName;
    herokuDeployType;
    herokuJavaVersion;
    herokuRegion;
    herokuAppExists;
    herokuSkipDeploy;
    herokuSkipBuild;
    dynoSize;
    constructor(args, options, features) {
        super(args, options, features);
        this.option('skip-build', {
            description: 'Skips building the application',
            type: Boolean,
            default: false,
        });
        this.option('skip-deploy', {
            description: 'Skips deployment to Heroku',
            type: Boolean,
            default: false,
        });
        if (this.options.help) {
            return;
        }
        this.herokuSkipBuild = this.options.skipBuild;
        this.herokuSkipDeploy = this.options.skipDeploy || this.options.skipBuild;
    }
    async beforeQueue() {
        if (!this.fromBlueprint) {
            await this.composeWithBlueprints(GENERATOR_HEROKU);
        }
        if (!this.delegateToBlueprint) {
            await this.dependsOnJHipster(GENERATOR_BOOTSTRAP_APPLICATION);
        }
    }
    get initializing() {
        return this.asInitializingTaskGroup({
            async checkInstallation() {
                const { exitCode } = await this.spawnHerokuCommand('--version', { verboseInfo: false });
                this.hasHerokuCli = exitCode === 0;
                if (!this.hasHerokuCli) {
                    const error = "You don't have the Heroku CLI installed. See https://devcenter.heroku.com/articles/heroku-cli#install-the-heroku-cli to learn how to install it.";
                    if (this.skipChecks) {
                        this.log.warn(error);
                        this.log.warn('Generation will continue with limited support');
                    }
                    else {
                        throw new Error(`${error} To ignore this error run 'jhipster heroku --skip-checks'`);
                    }
                }
            },
            async herokuLogin() {
                if (!this.hasHerokuCli)
                    return;
                const { exitCode } = await this.spawnHerokuCommand('whoami', { verboseInfo: false });
                if (exitCode !== 0) {
                    this.log.log(chalk.bold('Log in to Heroku to continue.'));
                    await this.spawnHerokuCommand('login', { reject: true, stdio: 'inherit' });
                }
            },
            initializing() {
                this.log.log(chalk.bold('Heroku configuration is starting'));
                this.dynoSize = 'Basic';
                this.herokuAppExists = Boolean(this.jhipsterConfig.herokuAppName);
            },
        });
    }
    get [BaseGenerator.INITIALIZING]() {
        return this.delegateTasksToBlueprint(() => this.initializing);
    }
    get prompting() {
        return this.asPromptingTaskGroup({
            async askForApp() {
                if (this.hasHerokuCli && this.herokuAppExists) {
                    const { stdout, exitCode } = await this.spawnHeroku(['apps:info', '--json', this.jhipsterConfig.herokuAppName], {
                        verboseInfo: false,
                    });
                    if (exitCode !== 0) {
                        this.log.error(`Could not find application: ${chalk.cyan(this.jhipsterConfig.herokuAppName)}`);
                        this.herokuAppName = null;
                        throw new Error('Run the generator again to create a new application.');
                    }
                    else {
                        const json = JSON.parse(stdout);
                        this.herokuAppName = json.app.name;
                        if (json.dynos.length > 0) {
                            this.dynoSize = json.dynos[0].size;
                        }
                        this.log.verboseInfo(`Deploying as existing application: ${chalk.bold(this.herokuAppName)}`);
                        this.config.set({
                            herokuAppName: this.herokuAppName,
                        });
                    }
                }
                else {
                    await this.prompt([
                        {
                            type: 'input',
                            name: 'herokuAppName',
                            message: 'Name to deploy as:',
                            default: this.baseName,
                        },
                    ], this.config);
                    const answers = await this.prompt([
                        {
                            type: 'list',
                            name: 'herokuRegion',
                            message: 'On which region do you want to deploy?',
                            choices: ['us', 'eu'],
                            default: 0,
                        },
                    ]);
                    this.herokuRegion = answers.herokuRegion;
                }
            },
            async askForHerokuDeployType() {
                await this.prompt([
                    {
                        type: 'list',
                        name: 'herokuDeployType',
                        message: 'Which type of deployment do you want?',
                        choices: [
                            { value: 'git', name: 'Git (compile on Heroku)' },
                            { value: 'jar', name: 'JAR (compile locally)' },
                        ],
                        default: 0,
                    },
                ], this.config);
            },
            async askForHerokuJavaVersion() {
                await this.prompt([
                    {
                        type: 'list',
                        name: 'herokuJavaVersion',
                        message: 'Which Java version would you like to use to build and run your app?',
                        choices: JAVA_COMPATIBLE_VERSIONS.map(version => ({ value: version })),
                        default: JAVA_VERSION,
                    },
                ], this.config);
            },
        });
    }
    get [BaseGenerator.PROMPTING]() {
        return this.delegateTasksToBlueprint(() => this.prompting);
    }
    get loading() {
        return this.asConfiguringTaskGroup({
            saveConfig() {
                this.herokuAppName = kebabCase(this.jhipsterConfig.herokuAppName);
                this.herokuJavaVersion = this.jhipsterConfig.herokuJavaVersion;
                this.herokuDeployType = this.jhipsterConfig.herokuDeployType;
            },
        });
    }
    get [BaseGenerator.LOADING]() {
        return this.delegateTasksToBlueprint(() => this.loading);
    }
    get default() {
        return this.asDefaultTaskGroup({
            insight() {
                statistics.sendSubGenEvent('generator', GENERATOR_HEROKU);
            },
            async gitInit() {
                if (!this.herokuDeployType === 'git')
                    return;
                const git = this.createGit();
                if (await git.checkIsRepo()) {
                    this.log.log(chalk.bold('\nUsing existing Git repository'));
                }
                else {
                    this.log.log(chalk.bold('\nInitializing Git repository'));
                    await git.init();
                }
            },
            async installHerokuDeployPlugin() {
                if (!this.hasHerokuCli)
                    return;
                const cliPlugin = 'heroku-cli-deploy';
                const { stdout, stderr, exitCode } = await this.spawnHerokuCommand('plugins', { stdio: 'pipe' });
                if (exitCode !== 0) {
                    if (stdout.includes(cliPlugin)) {
                        this.log.log('\nHeroku CLI deployment plugin already installed');
                    }
                    else {
                        this.log.log(chalk.bold('\nInstalling Heroku CLI deployment plugin'));
                        const { exitCode } = await this.spawnHerokuCommand(`plugins:install ${cliPlugin}`);
                        if (exitCode !== 0) {
                            throw new Error(stderr);
                        }
                    }
                }
            },
            async herokuCreate() {
                if (!this.hasHerokuCli || this.herokuAppExists)
                    return;
                const regionParams = this.herokuRegion !== 'us' ? ['--region', this.herokuRegion] : [];
                this.log.log(chalk.bold('\nCreating Heroku application and setting up Node environment'));
                const { stdout, stderr, exitCode } = await this.spawnHeroku(['create', this.herokuAppName, ...regionParams]);
                if (stdout.includes('Heroku credentials')) {
                    throw new Error("Error: Not authenticated. Run 'heroku login' to log in to your Heroku account and try again.");
                }
                if (exitCode !== 0) {
                    if (stderr.includes('is already taken')) {
                        const prompts = [
                            {
                                type: 'list',
                                name: 'herokuForceName',
                                message: `The Heroku application "${chalk.cyan(this.herokuAppName)}" already exists! Use it anyways?`,
                                choices: [
                                    {
                                        value: 'Yes',
                                        name: 'Yes, I have access to it',
                                    },
                                    {
                                        value: 'No',
                                        name: 'No, generate a random name',
                                    },
                                ],
                                default: 0,
                            },
                        ];
                        this.log.log('');
                        const props = await this.prompt(prompts);
                        if (props.herokuForceName === 'Yes') {
                            await this.spawnHeroku(['git:remote', '--app', this.herokuAppName], { reject: true });
                        }
                        else {
                            const { stdout } = await this.spawnHeroku(['create', ...regionParams]);
                            this.herokuAppName = stdout.substring(stdout.lastIndexOf('/') + 1, stdout.indexOf('.git'));
                            await this.spawnHeroku(['git:remote', '--app', this.herokuAppName]);
                            this.jhipsterConfig.herokuAppName = this.herokuAppName;
                        }
                    }
                    else if (stderr.includes('Invalid credentials')) {
                        this.log.error("Error: Not authenticated. Run 'heroku login' to log in to your Heroku account and try again.");
                    }
                    else {
                        throw new Error(stderr);
                    }
                }
            },
            async herokuAddonsCreate({ application }) {
                if (!this.hasHerokuCli || this.herokuAppExists)
                    return;
                this.log.log(chalk.bold('\nProvisioning addons'));
                if (application.searchEngineElasticsearch) {
                    this.log.log(chalk.bold('\nProvisioning bonsai elasticsearch addon'));
                    const { stdout, stderr } = await this.spawn('heroku', [
                        'addons:create',
                        'bonsai:sandbox-6',
                        '--as',
                        'BONSAI',
                        '--app',
                        this.herokuAppName,
                    ]);
                    this.checkAddOnReturn({ addOn: 'Elasticsearch', stdout, stderr });
                }
                let dbAddOn;
                if (application.prodDatabaseTypePostgresql) {
                    dbAddOn = 'heroku-postgresql';
                }
                else if (application.prodDatabaseTypeMysql) {
                    dbAddOn = 'jawsdb:kitefin';
                }
                else if (application.prodDatabaseTypeMariadb) {
                    dbAddOn = 'jawsdb-maria:kitefin';
                }
                if (dbAddOn) {
                    this.log.log(chalk.bold(`\nProvisioning database addon ${dbAddOn}`));
                    const { stdout, stderr } = await this.spawn('heroku', [
                        'addons:create',
                        dbAddOn,
                        '--as',
                        'DATABASE',
                        '--app',
                        this.herokuAppName,
                    ]);
                    this.checkAddOnReturn({ addOn: 'Database', stdout, stderr });
                }
                else {
                    this.log.log(chalk.bold(`\nNo suitable database addon for database ${this.prodDatabaseType} available.`));
                }
                let cacheAddOn;
                if (application.cacheProviderMemcached) {
                    cacheAddOn = ['memcachier:dev', '--as', 'MEMCACHIER'];
                }
                else if (application.cacheProviderRedis) {
                    cacheAddOn = ['heroku-redis:hobby-dev', '--as', 'REDIS'];
                }
                if (cacheAddOn) {
                    this.log.log(chalk.bold(`\nProvisioning cache addon '${cacheAddOn}'`));
                    const { stdout, stderr } = await this.spawn('heroku', ['addons:create', ...cacheAddOn, '--app', this.herokuAppName]);
                    this.checkAddOnReturn({ addOn: 'Cache', stdout, stderr });
                }
            },
            async configureJHipsterRegistry({ application }) {
                if (!this.hasHerokuCli || this.herokuAppExists || !application.serviceDiscoveryEureka)
                    return undefined;
                this.log.log('');
                const answers = await this.prompt([
                    {
                        type: 'input',
                        name: 'herokuJHipsterRegistryApp',
                        message: 'What is the name of your JHipster Registry Heroku application?',
                        default: 'jhipster-registry',
                    },
                    {
                        type: 'input',
                        name: 'herokuJHipsterRegistryUsername',
                        message: 'What is your JHipster Registry username?',
                        default: 'admin',
                    },
                    {
                        type: 'input',
                        name: 'herokuJHipsterRegistryPassword',
                        message: 'What is your JHipster Registry password?',
                        default: 'password',
                    },
                ]);
                const herokuJHipsterRegistryUsername = encodeURIComponent(answers.herokuJHipsterRegistryUsername);
                const herokuJHipsterRegistryPassword = encodeURIComponent(answers.herokuJHipsterRegistryPassword);
                const herokuJHipsterRegistry = `https://${herokuJHipsterRegistryUsername}:${herokuJHipsterRegistryPassword}@${answers.herokuJHipsterRegistryApp}.herokuapp.com`;
                const configSetCmd = ['config:set', 'JHIPSTER_REGISTRY_URL', herokuJHipsterRegistry, '--app', this.herokuAppName];
                await this.spawnHeroku(configSetCmd, { stdio: 'pipe' });
            },
        });
    }
    get [BaseGenerator.DEFAULT]() {
        return this.delegateTasksToBlueprint(() => this.default);
    }
    get writing() {
        return this.asWritingTaskGroup({
            copyHerokuFiles({ application }) {
                this.log.log(chalk.bold('\nCreating Heroku deployment files'));
                const context = {
                    ...application,
                    herokuAppName: this.herokuAppName,
                    dynoSize: this.dynoSize,
                    herokuJavaVersion: this.herokuJavaVersion,
                    herokuDeployType: this.herokuDeployType,
                };
                this.writeFile('bootstrap-heroku.yml.ejs', `${SERVER_MAIN_RES_DIR}/config/bootstrap-heroku.yml`, context);
                this.writeFile('application-heroku.yml.ejs', `${SERVER_MAIN_RES_DIR}/config/application-heroku.yml`, context);
                this.writeFile('Procfile.ejs', 'Procfile', context);
                this.writeFile('system.properties.ejs', 'system.properties', context);
                if (application.buildToolGradle) {
                    this.writeFile('heroku.gradle.ejs', 'gradle/heroku.gradle', context);
                }
            },
            addHerokuBuildPlugin({ application }) {
                if (!application.buildToolGradle)
                    return;
                this.editFile('build.gradle', addGradlePluginCallback({ id: 'com.heroku.sdk.heroku-gradle', version: '1.0.4' }));
                this.editFile('build.gradle', applyFromGradleCallback({ script: 'gradle/heroku.gradle' }));
            },
            addHerokuMavenProfile({ application }) {
                if (application.buildToolMaven) {
                    this.addMavenProfile('heroku', mavenProfileContent(application));
                }
            },
        });
    }
    get [BaseGenerator.WRITING]() {
        return this.delegateTasksToBlueprint(() => this.writing);
    }
    get end() {
        return this.asEndTaskGroup({
            async productionBuild() {
                if (this.herokuSkipBuild || this.herokuDeployType === 'git') {
                    this.log.log(chalk.bold('\nSkipping build'));
                    return;
                }
                this.log.log(chalk.bold('\nBuilding application'));
                await this.printChildOutput(this.spawnCommand('npm run java:jar:prod', { stdio: 'pipe' }));
            },
            async productionDeploy({ application }) {
                if (this.herokuSkipDeploy || !this.hasHerokuCli) {
                    this.log.log(chalk.bold('\nSkipping deployment'));
                    return;
                }
                if (this.herokuDeployType === 'git') {
                    try {
                        this.log.log(chalk.bold('\nUpdating Git repository'));
                        const git = this.createGit().outputHandler((_command, stdout, stderr) => this.printChildOutput({ stdout, stderr }));
                        await git.add('.').commit('Deploy to Heroku', { '--allow-empty': null });
                        let buildpack = 'heroku/java';
                        let configName = 'MAVEN_CUSTOM_OPTS';
                        let configValues = '-Pprod,heroku -DskipTests';
                        if (application.buildToolGradle) {
                            buildpack = 'heroku/gradle';
                            configName = 'GRADLE_TASK';
                            configValues = 'stage -Pprod -PnodeInstall';
                        }
                        this.log.log(chalk.bold('\nConfiguring Heroku'));
                        const { stdout: configData } = await this.spawnHeroku(['config:get', configName, '--app', this.herokuAppName]);
                        if (!configData) {
                            await this.spawnHeroku(['config:set', `${configName}=${configValues}`, '--app', this.herokuAppName]);
                        }
                        const { stdout: buildpackData } = await this.spawnHeroku(['buildpacks', '--app', this.herokuAppName]);
                        if (!buildpackData.includes(buildpack)) {
                            await this.spawnHeroku(['buildpacks:add', buildpack, '--app', this.herokuAppName]);
                        }
                        this.log.log(chalk.bold('\nDeploying application...'));
                        await git.push('heroku', 'HEAD:main');
                        this.log.log(chalk.green(`\nYour app should now be live. To view it run\n\t${chalk.bold('heroku open')}`));
                        this.log.log(chalk.yellow(`And you can view the logs with this command\n\t${chalk.bold('heroku logs --tail')}`));
                        this.log.log(chalk.yellow(`After application modification, redeploy it with\n\t${chalk.bold('jhipster heroku')}`));
                    }
                    catch (err) {
                        this.log.error(err);
                    }
                }
                else {
                    this.log.log(chalk.bold('\nDeploying application'));
                    let jarFileWildcard = 'target/*.jar';
                    if (application.buildToolGradle) {
                        jarFileWildcard = 'build/libs/*.jar';
                    }
                    const files = glob.sync(jarFileWildcard, {});
                    const jarFile = files[0];
                    this.log.log(chalk.bold(`\nUploading your application code.\nThis may take ${chalk.cyan('several minutes')} depending on your connection speed...`));
                    try {
                        await this.spawnHeroku(['deploy:jar', jarFile, '--app', this.herokuAppName], { stdio: 'pipe' });
                        await this.spawnHerokuCommand('buildpacks:set heroku/jvm', { stdio: 'pipe' });
                        this.log.log(chalk.green(`\nYour app should now be live. To view it run\n\t${chalk.bold('heroku open')}`));
                        this.log.log(chalk.yellow(`And you can view the logs with this command\n\t${chalk.bold('heroku logs --tail')}`));
                        this.log.log(chalk.yellow(`After application modification, redeploy it with\n\t${chalk.bold('jhipster heroku')}`));
                    }
                    catch (err) {
                        this.log.error(err);
                    }
                }
            },
        });
    }
    get [BaseGenerator.END]() {
        return this.delegateTasksToBlueprint(() => this.end);
    }
    addMavenProfile(profileId, other) {
        createPomStorage(this).addProfile({ id: profileId, content: other });
    }
    spawnHerokuCommand(command, opt) {
        opt = { stdio: 'pipe', reject: false, ...opt };
        const { verboseInfo, ...spawnOptions } = opt;
        const child = this.spawnCommand(`heroku ${command}`, spawnOptions);
        if (opt.stdio !== 'pipe' || verboseInfo === false) {
            return child;
        }
        return this.printChildOutput(child);
    }
    spawnHeroku(args, opt) {
        opt = { stdio: 'pipe', reject: false, ...opt };
        const { verboseInfo, ...spawnOptions } = opt;
        const child = this.spawn('heroku', args, spawnOptions);
        if (spawnOptions.stdio !== 'pipe' || verboseInfo === false) {
            return child;
        }
        return this.printChildOutput(child);
    }
    printChildOutput(child, log = data => this.log.verboseInfo(data)) {
        const { stdout, stderr } = child;
        stdout.on('data', data => {
            data.toString().split(/\r?\n/).filter(Boolean).forEach(log);
        });
        stderr.on('data', data => {
            data.toString().split(/\r?\n/).filter(Boolean).forEach(log);
        });
        return child;
    }
    checkAddOnReturn({ addOn, stdout, stderr }) {
        if (stdout) {
            this.log.ok(`Created ${addOn.valueOf()} add-on`);
            this.log.ok(stdout);
        }
        else if (stderr) {
            const verifyAccountUrl = 'https://heroku.com/verify';
            if (stderr.includes(verifyAccountUrl)) {
                this.log.error(`Account must be verified to use addons. Please go to: ${verifyAccountUrl}`);
                throw new Error(stderr);
            }
            else {
                this.log.verboseInfo(`No new ${addOn.valueOf()} add-on created`);
            }
        }
    }
}
