/**
 * Copyright 2013-2026 the original author or authors from the JHipster project.
 *
 * This file is part of the JHipster project, see https://www.jhipster.tech/
 * for more information.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import assert from 'node:assert';
import { existsSync } from 'node:fs';
import pathjs from 'node:path';
import chalk from 'chalk';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { monitoringTypes, serviceDiscoveryTypes } from "../../lib/jhipster/index.js";
import { createBase64Secret, stringHashCode } from "../../lib/utils/index.js";
import { normalizePath as normalize } from "../../lib/utils/path.js";
import { createFaker } from "../base-application/support/index.js";
import BaseWorkspacesGenerator, {} from "../base-workspaces/index.js";
import { checkDocker } from "../base-workspaces/internal/docker-base.js";
import { askForClustersModeWorkspace, askForMonitoring, askForServiceDiscoveryWorkspace, } from "../base-workspaces/internal/docker-prompts.js";
import { loadDockerDependenciesTask, loadDockerElasticsearchVersion } from "../base-workspaces/internal/index.js";
import { askForDirectoryPath } from "../base-workspaces/prompts.js";
import cleanupOldFilesTask from "./cleanup.js";
import { writeFiles } from "./files.js";
const { PROMETHEUS } = monitoringTypes;
const { EUREKA, NO: NO_SERVICE_DISCOVERY } = serviceDiscoveryTypes;
export default class DockerComposeGenerator extends BaseWorkspacesGenerator {
    async beforeQueue() {
        if (!this.fromBlueprint) {
            this.jhipsterConfig.deploymentType ??= 'docker-compose';
            assert.equal(this.jhipsterConfig.deploymentType, 'docker-compose', 'Deployment type must be docker-compose');
            await this.composeWithBlueprints();
        }
        await this.dependsOnBootstrap('base-workspaces');
    }
    get initializing() {
        return this.asInitializingTaskGroup({
            sayHello() {
                this.log.log(chalk.white(`${chalk.bold('🐳')}  Welcome to the JHipster Docker Compose Sub-Generator ${chalk.bold('🐳')}`));
                this.log.log(chalk.white(`Files will be generated in folder: ${chalk.yellow(this.destinationRoot())}`));
            },
            checkDocker,
            async checkDockerCompose({ control }) {
                if (this.skipChecks)
                    return;
                if (!control.environmentHasDockerCompose) {
                    throw new Error(`Docker Compose V2 is not installed on your computer.
         Read https://docs.docker.com/compose/install/
`);
                }
            },
        });
    }
    get [BaseWorkspacesGenerator.INITIALIZING]() {
        return this.delegateTasksToBlueprint(() => this.initializing);
    }
    get prompting() {
        return this.asPromptingTaskGroup({
            askForDirectoryPath,
        });
    }
    get [BaseWorkspacesGenerator.PROMPTING]() {
        return this.delegateTasksToBlueprint(() => this.prompting);
    }
    get preparing() {
        return this.asPreparingTaskGroup({
            setWorkspacesRoot() {
                this.setWorkspacesRoot(this.destinationPath(this.jhipsterConfig.directoryPath));
            },
        });
    }
    get [BaseWorkspacesGenerator.PREPARING]() {
        return this.delegateTasksToBlueprint(() => this.preparing);
    }
    get promptingWorkspaces() {
        return this.asPromptingWorkspacesTaskGroup({
            askForMonitoring,
            askForClustersModeWorkspace,
            askForServiceDiscoveryWorkspace,
        });
    }
    get [BaseWorkspacesGenerator.PROMPTING_WORKSPACES]() {
        return this.delegateTasksToBlueprint(() => this.promptingWorkspaces);
    }
    get configuringWorkspaces() {
        return this.asConfiguringWorkspacesTaskGroup({
            configureBaseDeployment({ applications }) {
                this.jhipsterConfig.jwtSecretKey ??= createBase64Secret(this.options.reproducibleTests);
                if (applications.some(app => app.serviceDiscoveryTypeEureka)) {
                    this.jhipsterConfig.adminPassword = this.jhipsterConfig.adminPassword ?? 'admin';
                }
            },
        });
    }
    get [BaseWorkspacesGenerator.CONFIGURING_WORKSPACES]() {
        return this.delegateTasksToBlueprint(() => this.configuringWorkspaces);
    }
    get loadingWorkspaces() {
        return this.asLoadingWorkspacesTaskGroup({
            loadBaseDeployment({ deployment, applications }) {
                deployment.jwtSecretKey = this.jhipsterConfig.jwtSecretKey;
                loadDockerDependenciesTask.call(this, { context: deployment });
                loadDockerElasticsearchVersion.call(this, {
                    springBoot4: applications.some(app => app.springBoot4),
                    dockerContainers: deployment.dockerContainers,
                });
            },
        });
    }
    get [BaseWorkspacesGenerator.LOADING_WORKSPACES]() {
        return this.delegateTasksToBlueprint(() => this.loadingWorkspaces);
    }
    get preparingWorkspaces() {
        return this.asPreparingWorkspacesTaskGroup({
            prepareDeployment({ deployment, applications }) {
                this.prepareDeploymentDerivedProperties({ deployment, applications });
            },
        });
    }
    get [BaseWorkspacesGenerator.PREPARING_WORKSPACES]() {
        return this.delegateTasksToBlueprint(() => this.preparingWorkspaces);
    }
    get default() {
        return this.asDefaultTaskGroup({
            async setAppsYaml({ deployment, applications }) {
                const faker = await createFaker();
                deployment.keycloakRedirectUris = '';
                deployment.appsYaml = applications.map(appConfig => {
                    const lowercaseBaseName = appConfig.baseName.toLowerCase();
                    const parentConfiguration = {};
                    const path = this.workspacePath(appConfig.appFolder);
                    // Add application configuration
                    const yaml = parseYaml(this.fs.read(`${path}/src/main/docker/app.yml`));
                    const yamlConfig = yaml.services.app;
                    if (yamlConfig.depends_on) {
                        yamlConfig.depends_on = Object.fromEntries(Object.entries(yamlConfig.depends_on).map(([serviceName, config]) => {
                            if (['keycloak', 'jhipster-registry', 'consul'].includes(serviceName)) {
                                return [serviceName, config];
                            }
                            return [`${lowercaseBaseName}-${serviceName}`, config];
                        }));
                    }
                    if (appConfig.applicationTypeGateway || appConfig.applicationTypeMonolith) {
                        if (deployment.keycloakSecrets === undefined && appConfig.authenticationTypeOauth2) {
                            faker.seed(stringHashCode(appConfig.baseName));
                            deployment.keycloakSecrets = Array.from(new Array(6), () => faker.string.uuid());
                        }
                        deployment.keycloakRedirectUris += `"http://localhost:${appConfig.composePort}/*", "https://localhost:${appConfig.composePort}/*", `;
                        if (appConfig.devServerPort !== undefined) {
                            deployment.keycloakRedirectUris += `"http://localhost:${appConfig.devServerPort}/*", `;
                        }
                        if (appConfig.devServerPortProxy !== undefined) {
                            deployment.keycloakRedirectUris += `"http://localhost:${appConfig.devServerPortProxy}/*", `;
                        }
                        // Split ports by ":" and take last 2 elements to skip the hostname/IP if present
                        const ports = yamlConfig.ports[0].split(':').slice(-2);
                        ports[0] = appConfig.composePort;
                        yamlConfig.ports[0] = ports.join(':');
                    }
                    if (yamlConfig.environment) {
                        yamlConfig.environment = yamlConfig.environment.map((envOption) => {
                            // Doesn't applies to keycloak, jhipster-registry and consul.
                            // docker-compose changes the container name to `${lowercaseBaseName}-${databaseType}`.
                            // we need to update the environment urls to the new container host.
                            [
                                'SPRING_R2DBC_URL',
                                'SPRING_DATASOURCE_URL',
                                'SPRING_LIQUIBASE_URL',
                                'SPRING_NEO4J_URI',
                                'SPRING_MONGODB_URI',
                                'JHIPSTER_CACHE_REDIS_SERVER',
                                'SPRING_ELASTICSEARCH_URIS',
                            ].forEach(varName => {
                                if (envOption.startsWith(varName)) {
                                    envOption = envOption
                                        .replace('://', `://${lowercaseBaseName}-`)
                                        .replace('oracle:thin:@', `oracle:thin:@${lowercaseBaseName}-`);
                                }
                            });
                            ['JHIPSTER_CACHE_MEMCACHED_SERVERS', 'SPRING_COUCHBASE_CONNECTION_STRING', 'SPRING_CASSANDRA_CONTACTPOINTS'].forEach(varName => {
                                if (envOption.startsWith(varName)) {
                                    envOption = envOption.replace(`${varName}=`, `${varName}=${lowercaseBaseName}-`);
                                }
                            });
                            return envOption;
                        });
                    }
                    if (appConfig.applicationTypeMonolith && deployment.monitoring === PROMETHEUS) {
                        yamlConfig.environment.push('JHIPSTER_LOGGING_LOGSTASH_ENABLED=false', 'MANAGEMENT_METRICS_EXPORT_PROMETHEUS_ENABLED=true');
                    }
                    if (deployment.serviceDiscoveryType === EUREKA) {
                        // Set the JHipster Registry password
                        yamlConfig.environment.push(`JHIPSTER_REGISTRY_PASSWORD=${deployment.adminPassword}`);
                    }
                    const hasNoServiceDiscovery = !deployment.serviceDiscoveryType && deployment.serviceDiscoveryType !== NO_SERVICE_DISCOVERY;
                    if (hasNoServiceDiscovery && appConfig.skipClient) {
                        yamlConfig.environment.push('SERVER_PORT=80'); // to simplify service resolution in docker/k8s
                    }
                    parentConfiguration[lowercaseBaseName] = yamlConfig;
                    // Add database configuration
                    if (appConfig.databaseTypeAny && !appConfig.prodDatabaseTypeOracle) {
                        const database = appConfig.databaseTypeSql ? appConfig.prodDatabaseType : appConfig.databaseType;
                        const relativePath = normalize(pathjs.relative(this.destinationRoot(), `${path}/src/main/docker`));
                        const databaseYaml = parseYaml(this.fs.read(`${path}/src/main/docker/${database}.yml`));
                        const databaseServiceName = `${lowercaseBaseName}-${database}`;
                        let databaseYamlConfig = databaseYaml.services[database];
                        // Don't export database ports
                        delete databaseYamlConfig.ports;
                        if (appConfig.databaseTypeCassandra) {
                            // migration service config
                            const cassandraMigrationYaml = parseYaml(this.fs.read(`${path}/src/main/docker/cassandra-migration.yml`));
                            const cassandraMigrationConfig = cassandraMigrationYaml.services[`${database}-migration`];
                            cassandraMigrationConfig.build.context = relativePath;
                            const cqlFilesRelativePath = normalize(pathjs.relative(this.destinationRoot(), `${path}/src/main/resources/config/cql`));
                            cassandraMigrationConfig.volumes[0] = `${cqlFilesRelativePath}:/cql:ro`;
                            parentConfiguration[`${databaseServiceName}-migration`] = cassandraMigrationConfig;
                        }
                        if (appConfig.databaseTypeCouchbase) {
                            databaseYamlConfig.build.context = relativePath;
                        }
                        if (deployment.clusteredDbApps?.includes(appConfig.appFolder)) {
                            const clusterDbYaml = parseYaml(this.fs.read(`${path}/src/main/docker/${database}-cluster.yml`));
                            const dbNodeConfig = clusterDbYaml.services[`${database}-node`];
                            dbNodeConfig.build.context = relativePath;
                            databaseYamlConfig = clusterDbYaml.services[database];
                            delete databaseYamlConfig.ports;
                            if (appConfig.databaseTypeCouchbase) {
                                databaseYamlConfig.build.context = relativePath;
                            }
                            parentConfiguration[`${databaseServiceName}-node`] = dbNodeConfig;
                            if (appConfig.databaseTypeMongodb) {
                                parentConfiguration[`${databaseServiceName}-config`] = clusterDbYaml.services[`${database}-config`];
                            }
                        }
                        parentConfiguration[databaseServiceName] = databaseYamlConfig;
                    }
                    if (appConfig.searchEngineElasticsearch) {
                        // Add search engine configuration
                        const { searchEngine } = appConfig;
                        const searchEngineYaml = parseYaml(this.fs.read(`${path}/src/main/docker/${searchEngine}.yml`));
                        const searchEngineConfig = searchEngineYaml.services[searchEngine];
                        delete searchEngineConfig.ports;
                        parentConfiguration[`${lowercaseBaseName}-${searchEngine}`] = searchEngineConfig;
                    }
                    // Add Memcached support
                    if (appConfig.cacheProviderMemcached) {
                        const memcachedYaml = parseYaml(this.readDestination(`${path}/src/main/docker/memcached.yml`));
                        const memcachedConfig = memcachedYaml.services.memcached;
                        delete memcachedConfig.ports;
                        parentConfiguration[`${lowercaseBaseName}-memcached`] = memcachedConfig;
                    }
                    // Add Redis support
                    if (appConfig.cacheProviderRedis) {
                        const redisYaml = parseYaml(this.readDestination(`${path}/src/main/docker/redis.yml`));
                        const redisConfig = redisYaml.services.redis;
                        delete redisConfig.ports;
                        parentConfiguration[`${lowercaseBaseName}-redis`] = redisConfig;
                    }
                    // Expose authenticationType
                    deployment.authenticationType = appConfig.authenticationType;
                    // Dump the file
                    let yamlString = stringifyYaml(parentConfiguration, { indent: 2, lineWidth: 0 });
                    // Add extra indentation for each lines
                    const yamlArray = yamlString.split('\n');
                    for (let j = 0; j < yamlArray.length; j++) {
                        yamlArray[j] = `  ${yamlArray[j]}`;
                    }
                    yamlString = yamlArray.join('\n');
                    return yamlString;
                });
            },
        });
    }
    get [BaseWorkspacesGenerator.DEFAULT]() {
        return this.delegateTasksToBlueprint(() => this.default);
    }
    get writing() {
        return this.asWritingTaskGroup({
            cleanupOldFilesTask,
            writeFiles,
        });
    }
    get [BaseWorkspacesGenerator.WRITING]() {
        return this.delegateTasksToBlueprint(() => this.writing);
    }
    get end() {
        return this.asEndTaskGroup({
            end({ applications }) {
                this.checkApplicationsDockerImages({ applications });
                this.log.verboseInfo(`You can launch all your infrastructure by running : ${chalk.cyan('docker compose up -d')}`);
                const uiApplications = applications.filter(app => (app.applicationTypeGateway || app.applicationTypeMonolith) && app.clientFrameworkAny);
                if (uiApplications.length > 0) {
                    this.log.log('\nYour applications will be accessible on these URLs:');
                    for (const application of uiApplications) {
                        this.log.verboseInfo(`\t- ${application.baseName}: http://localhost:${application.composePort}`);
                    }
                    this.log.log('\n');
                }
            },
        });
    }
    get [BaseWorkspacesGenerator.END]() {
        return this.delegateTasksToBlueprint(() => this.end);
    }
    checkApplicationsDockerImages({ applications }) {
        this.log.log('\nChecking Docker images in applications directories...');
        let imagePath = '';
        let runCommand = '';
        let hasWarning = false;
        let warningMessage = 'To generate the missing Docker image(s), please run:\n';
        applications.forEach(application => {
            if (application.buildToolGradle) {
                imagePath = this.workspacePath(application.appFolder, 'build/jib-cache');
                runCommand = `./gradlew bootJar -Pprod jibDockerBuild${process.arch === 'arm64' ? ' -PjibArchitecture=arm64' : ''}`;
            }
            else if (application.buildToolMaven) {
                imagePath = this.workspacePath(application.appFolder, '/target/jib-cache');
                runCommand = `./mvnw -ntp -Pprod verify jib:dockerBuild${process.arch === 'arm64' ? ' -Djib-maven-plugin.architecture=arm64' : ''}`;
            }
            if (!existsSync(imagePath)) {
                hasWarning = true;
                warningMessage += `  ${chalk.cyan(runCommand)} in ${this.workspacePath(application.appFolder)}\n`;
            }
        });
        if (hasWarning) {
            this.log.warn('Docker Compose configuration generated, but no Jib cache found');
            this.log.warn('If you forgot to generate the Docker image for this application, please run:');
            this.log.log(chalk.red(warningMessage));
        }
        else {
            this.log.verboseInfo(chalk.bold.green('Docker Compose configuration successfully generated!'));
        }
    }
    prepareDeploymentDerivedProperties({ deployment, applications }) {
        deployment.usesOauth2 = applications.some(appConfig => appConfig.authenticationTypeOauth2);
        deployment.useKafka = applications.some(appConfig => appConfig.messageBrokerKafka);
        deployment.usePulsar = applications.some(appConfig => appConfig.messageBrokerPulsar);
        deployment.useMemcached = applications.some(appConfig => appConfig.cacheProviderMemcached);
        deployment.useRedis = applications.some(appConfig => appConfig.cacheProviderRedis);
        deployment.includesApplicationTypeGateway = applications.some(appConfig => appConfig.applicationTypeGateway);
        deployment.entryPort = 8080;
        deployment.appConfigs = applications;
    }
}
