"use strict";
/*
 * Copyright 2022 Fernando Boucquez
 *
 * 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
 *
 *     http://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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComposeService = void 0;
const fs_1 = require("fs");
const _ = require("lodash");
const path_1 = require("path");
const BootstrapUtils_1 = require("./BootstrapUtils");
const ConfigLoader_1 = require("./ConfigLoader");
const RuntimeService_1 = require("./RuntimeService");
const targetNodesFolder = BootstrapUtils_1.BootstrapUtils.targetNodesFolder;
const targetDatabasesFolder = BootstrapUtils_1.BootstrapUtils.targetDatabasesFolder;
const targetGatewaysFolder = BootstrapUtils_1.BootstrapUtils.targetGatewaysFolder;
const targetExplorersFolder = BootstrapUtils_1.BootstrapUtils.targetExplorersFolder;
class ComposeService {
    constructor(logger, params) {
        this.logger = logger;
        this.params = params;
        this.configLoader = new ConfigLoader_1.ConfigLoader(logger);
    }
    resolveDebugOptions(dockerComposeDebugMode, dockerComposeServiceDebugMode) {
        if (dockerComposeServiceDebugMode == false) {
            return {};
        }
        if (dockerComposeServiceDebugMode || dockerComposeDebugMode) {
            return ComposeService.DEBUG_SERVICE_PARAMS;
        }
        return {};
    }
    async run(passedPresetData, passedAddresses) {
        const presetData = passedPresetData !== null && passedPresetData !== void 0 ? passedPresetData : this.configLoader.loadExistingPresetData(this.params.target, this.params.password || false);
        const currentDir = process.cwd();
        const target = path_1.join(currentDir, this.params.target);
        const targetDocker = path_1.join(target, `docker`);
        if (this.params.upgrade) {
            BootstrapUtils_1.BootstrapUtils.deleteFolder(this.logger, targetDocker);
        }
        const dockerFile = path_1.join(targetDocker, 'docker-compose.yml');
        if (fs_1.existsSync(dockerFile)) {
            this.logger.info(dockerFile + ' already exist. Reusing. (run --upgrade to drop and upgrade)');
            return BootstrapUtils_1.BootstrapUtils.loadYaml(dockerFile, false);
        }
        await BootstrapUtils_1.BootstrapUtils.mkdir(targetDocker);
        await BootstrapUtils_1.BootstrapUtils.generateConfiguration(presetData, path_1.join(BootstrapUtils_1.BootstrapUtils.ROOT_FOLDER, 'config', 'docker'), targetDocker);
        await BootstrapUtils_1.BootstrapUtils.chmodRecursive(path_1.join(targetDocker, 'mongo'), 0o666);
        const user = await new RuntimeService_1.RuntimeService(this.logger).resolveDockerUserFromParam(this.params.user);
        const vol = (hostFolder, imageFolder, readOnly) => {
            return `${hostFolder}:${imageFolder}:${readOnly ? 'ro' : 'rw'}`;
        };
        this.logger.info(`Creating docker-compose.yml from last used profile.`);
        const services = [];
        const resolvePorts = (portConfigurations) => {
            return portConfigurations
                .filter((c) => c.openPort)
                .map(({ openPort, internalPort }) => {
                if (openPort === true || openPort === 'true') {
                    return `${internalPort}:${internalPort}`;
                }
                return `${openPort}:${internalPort}`;
            });
        };
        const resolveHttpsProxyDomains = (fromDomain, toDomain) => {
            return `${fromDomain} -> ${toDomain}`;
        };
        const resolveService = async (servicePreset, rawService) => {
            const service = Object.assign({}, rawService);
            if (servicePreset.host || servicePreset.ipv4_address) {
                service.networks = { default: {} };
            }
            if (servicePreset.host) {
                service.hostname = servicePreset.host;
                service.networks.default.aliases = [servicePreset.host];
            }
            if (servicePreset.environment) {
                service.environment = Object.assign(Object.assign({}, servicePreset.environment), rawService.environment);
            }
            if (servicePreset.ipv4_address) {
                service.networks.default.ipv4_address = servicePreset.ipv4_address;
            }
            return service;
        };
        await Promise.all((presetData.databases || [])
            .filter((d) => !d.excludeDockerService)
            .map(async (n) => {
            const databaseName = n.databaseName || presetData.databaseName;
            const databasePort = 27017;
            services.push(await resolveService(n, Object.assign(Object.assign({ user, environment: { MONGO_INITDB_DATABASE: databaseName }, container_name: n.name, image: presetData.mongoImage, command: `mongod --dbpath=/dbdata --bind_ip=${n.name} ${presetData.mongoComposeRunParam}`, stop_signal: 'SIGINT', working_dir: '/docker-entrypoint-initdb.d', ports: resolvePorts([{ internalPort: databasePort, openPort: n.openPort }]), volumes: [
                    vol(`./mongo`, `/docker-entrypoint-initdb.d`, true),
                    vol(`../${targetDatabasesFolder}/${n.name}`, '/dbdata', false),
                ] }, this.resolveDebugOptions(presetData.dockerComposeDebugMode, n.dockerComposeDebugMode)), n.compose)));
        }));
        const nodeWorkingDirectory = '/symbol-workdir';
        const nodeCommandsDirectory = '/symbol-commands';
        const restart = presetData.dockerComposeServiceRestart;
        await Promise.all((presetData.nodes || [])
            .filter((d) => !d.excludeDockerService)
            .map(async (n) => {
            const debugFlag = 'DEBUG';
            const serverDebugMode = presetData.dockerComposeDebugMode || n.dockerComposeDebugMode ? debugFlag : 'NORMAL';
            const brokerDebugMode = presetData.dockerComposeDebugMode || n.brokerDockerComposeDebugMode ? debugFlag : 'NORMAL';
            const serverCommand = `/bin/bash ${nodeCommandsDirectory}/start.sh ${presetData.catapultAppFolder} ${presetData.dataDirectory} server broker ${n.name} ${serverDebugMode} ${!!n.brokerName}`;
            const brokerCommand = `/bin/bash ${nodeCommandsDirectory}/start.sh ${presetData.catapultAppFolder} ${presetData.dataDirectory} broker server ${n.brokerName || 'broker'} ${brokerDebugMode}`;
            const portConfigurations = [{ internalPort: 7900, openPort: n.openPort }];
            const serverDependsOn = [];
            const brokerDependsOn = [];
            if (n.databaseHost) {
                serverDependsOn.push(n.databaseHost);
                brokerDependsOn.push(n.databaseHost);
            }
            if (n.brokerName) {
                serverDependsOn.push(n.brokerName);
            }
            const volumes = [
                vol(`../${targetNodesFolder}/${n.name}`, nodeWorkingDirectory, false),
                vol(`./server`, nodeCommandsDirectory, true),
            ];
            const nodeService = await resolveService(n, Object.assign(Object.assign({ user: serverDebugMode === debugFlag ? undefined : user, container_name: n.name, image: presetData.symbolServerImage, command: serverCommand, stop_signal: 'SIGINT', working_dir: nodeWorkingDirectory, restart: restart, ports: resolvePorts(portConfigurations), volumes: volumes, depends_on: serverDependsOn }, this.resolveDebugOptions(presetData.dockerComposeDebugMode, n.dockerComposeDebugMode)), n.compose));
            services.push(nodeService);
            if (n.brokerName) {
                services.push(await resolveService({
                    ipv4_address: n.brokerIpv4_address,
                    openPort: n.brokerOpenPort,
                    excludeDockerService: n.brokerExcludeDockerService,
                    host: n.brokerHost,
                }, Object.assign(Object.assign({ user: brokerDebugMode === debugFlag ? undefined : user, container_name: n.brokerName, image: nodeService.image, working_dir: nodeWorkingDirectory, command: brokerCommand, ports: resolvePorts([{ internalPort: 7902, openPort: n.brokerOpenPort }]), stop_signal: 'SIGINT', restart: restart, volumes: nodeService.volumes, depends_on: brokerDependsOn }, this.resolveDebugOptions(presetData.dockerComposeDebugMode, n.brokerDockerComposeDebugMode)), n.brokerCompose)));
            }
        }));
        const restInternalPort = 3000; // Move to shared?
        await Promise.all((presetData.gateways || [])
            .filter((d) => !d.excludeDockerService)
            .map(async (n) => {
            const volumes = [vol(`../${targetGatewaysFolder}/${n.name}`, nodeWorkingDirectory, false)];
            services.push(await resolveService(n, Object.assign(Object.assign({ container_name: n.name, user, image: presetData.symbolRestImage, command: 'npm start --prefix /app/catapult-rest/rest /symbol-workdir/rest.json', stop_signal: 'SIGINT', working_dir: nodeWorkingDirectory, ports: resolvePorts([{ internalPort: restInternalPort, openPort: n.openPort }]), restart: restart, volumes: volumes, depends_on: [n.databaseHost] }, this.resolveDebugOptions(presetData.dockerComposeDebugMode, n.dockerComposeDebugMode)), n.compose)));
        }));
        await Promise.all((presetData.httpsProxies || [])
            .filter((d) => !d.excludeDockerService)
            .map(async (n) => {
            var _a, _b, _c;
            const internalPort = 443;
            const host = n.host || ((_b = (_a = presetData.nodes) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.host);
            if (!host) {
                throw new Error(`HTTPS Proxy ${n.name} is invalid, 'host' property could not be resolved. It must be set to a valid DNS record.`);
            }
            const domains = n.domains || ((_c = presetData.gateways) === null || _c === void 0 ? void 0 : _c.map((g) => resolveHttpsProxyDomains(host, `http://${g.name}:${restInternalPort}`))[0]);
            if (!domains) {
                throw new Error(`HTTPS Proxy ${n.name} is invalid, 'domains' property could not be resolved!`);
            }
            services.push(await resolveService(n, Object.assign(Object.assign({ container_name: n.name, image: presetData.httpsPortalImage, stop_signal: 'SIGINT', ports: resolvePorts([
                    { internalPort: 80, openPort: true },
                    { internalPort: internalPort, openPort: n.openPort },
                ]), environment: {
                    DOMAINS: domains,
                    WEBSOCKET: n.webSocket,
                    STAGE: n.stage,
                    SERVER_NAMES_HASH_BUCKET_SIZE: n.serverNamesHashBucketSize,
                }, restart: restart, depends_on: [presetData.gateways[0].name] }, this.resolveDebugOptions(presetData.dockerComposeDebugMode, n.dockerComposeDebugMode)), n.compose)));
        }));
        await Promise.all((presetData.explorers || [])
            .filter((d) => !d.excludeDockerService)
            .map(async (n) => {
            const volumes = [
                vol(`../${targetExplorersFolder}/${n.name}`, nodeWorkingDirectory, true),
                vol(`./explorer`, nodeCommandsDirectory, true),
            ];
            const entrypoint = `ash -c "/bin/ash ${nodeCommandsDirectory}/run.sh ${n.name}"`;
            services.push(await resolveService(n, Object.assign(Object.assign({ container_name: n.name, image: presetData.symbolExplorerImage, entrypoint: entrypoint, stop_signal: 'SIGINT', working_dir: nodeWorkingDirectory, ports: resolvePorts([{ internalPort: 4000, openPort: n.openPort }]), restart: restart, volumes: volumes }, this.resolveDebugOptions(presetData.dockerComposeDebugMode, n.dockerComposeDebugMode)), n.compose)));
        }));
        await Promise.all((presetData.faucets || [])
            .filter((d) => !d.excludeDockerService)
            .map(async (n) => {
            var _a, _b, _c;
            const mosaicPreset = presetData.nemesis.mosaics[0];
            const fullName = `${presetData.baseNamespace}.${mosaicPreset.name}`;
            // const nemesisPrivateKey = addresses?.mosaics?[0]?/;
            services.push(await resolveService(n, Object.assign(Object.assign({ container_name: n.name, image: presetData.symbolFaucetImage, stop_signal: 'SIGINT', environment: {
                    NATIVE_CURRENCY_NAME: ((_a = n.environment) === null || _a === void 0 ? void 0 : _a.NATIVE_CURRENCY_NAME) || fullName,
                    FAUCET_PRIVATE_KEY: ((_b = n.environment) === null || _b === void 0 ? void 0 : _b.FAUCET_PRIVATE_KEY) || this.getMainAccountPrivateKey(passedAddresses) || '',
                    NATIVE_CURRENCY_ID: BootstrapUtils_1.BootstrapUtils.toSimpleHex(((_c = n.environment) === null || _c === void 0 ? void 0 : _c.NATIVE_CURRENCY_ID) || presetData.currencyMosaicId || ''),
                }, restart: restart, ports: resolvePorts([{ internalPort: 4000, openPort: n.openPort }]), depends_on: [n.gateway] }, this.resolveDebugOptions(presetData.dockerComposeDebugMode, n.dockerComposeDebugMode)), n.compose)));
        }));
        const validServices = services.filter((s) => s).map((s) => s);
        const servicesMap = _.keyBy(validServices, 'container_name');
        let dockerCompose = {
            version: presetData.dockerComposeVersion,
            services: servicesMap,
        };
        if (presetData.subnet)
            dockerCompose.networks = {
                default: {
                    ipam: {
                        config: [
                            {
                                subnet: presetData.subnet,
                            },
                        ],
                    },
                },
            };
        dockerCompose = BootstrapUtils_1.BootstrapUtils.pruneEmpty(dockerCompose);
        await BootstrapUtils_1.BootstrapUtils.writeYaml(dockerFile, dockerCompose, undefined);
        this.logger.info(`The docker-compose.yml file created ${dockerFile}`);
        return dockerCompose;
    }
    getMainAccountPrivateKey(passedAddresses) {
        var _a, _b;
        const addresses = passedAddresses !== null && passedAddresses !== void 0 ? passedAddresses : this.configLoader.loadExistingAddresses(this.params.target, this.params.password);
        return (_b = (_a = addresses === null || addresses === void 0 ? void 0 : addresses.mosaics) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.accounts[0].privateKey;
    }
}
exports.ComposeService = ComposeService;
ComposeService.defaultParams = {
    target: BootstrapUtils_1.BootstrapUtils.defaultTargetFolder,
    user: BootstrapUtils_1.BootstrapUtils.CURRENT_USER,
    upgrade: false,
};
ComposeService.DEBUG_SERVICE_PARAMS = {
    security_opt: ['seccomp:unconfined'],
    cap_add: ['ALL'],
    privileged: true,
};
