"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigLoader = void 0;
/*
 * Copyright 2020 NEM
 *
 * 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.
 */
const fs_1 = require("fs");
const _ = require("lodash");
const path_1 = require("path");
const symbol_sdk_1 = require("symbol-sdk");
const logger_1 = require("../logger");
const LoggerFactory_1 = require("../logger/LoggerFactory");
const model_1 = require("../model");
const BootstrapUtils_1 = require("./BootstrapUtils");
const CommandUtils_1 = require("./CommandUtils");
const ConfigService_1 = require("./ConfigService");
const CryptoUtils_1 = require("./CryptoUtils");
const logger = LoggerFactory_1.default.getLogger(logger_1.LogType.System);
class ConfigLoader {
    async generateRandomConfiguration(oldAddresses, presetData) {
        const networkType = presetData.networkType;
        const addresses = {
            version: this.getAddressesMigration(presetData.networkType).length + 1,
            networkType: networkType,
            nemesisGenerationHashSeed: presetData.nemesisGenerationHashSeed || (oldAddresses === null || oldAddresses === void 0 ? void 0 : oldAddresses.nemesisGenerationHashSeed) ||
                symbol_sdk_1.Convert.uint8ToHex(symbol_sdk_1.Crypto.randomBytes(32)),
            sinkAddress: presetData.sinkAddress || (oldAddresses === null || oldAddresses === void 0 ? void 0 : oldAddresses.sinkAddress) || symbol_sdk_1.Account.generateNewAccount(networkType).address.plain(),
        };
        if (presetData.nodes) {
            addresses.nodes = await this.generateNodeAccounts(oldAddresses, presetData, networkType);
        }
        if (!presetData.harvestNetworkFeeSinkAddress) {
            presetData.harvestNetworkFeeSinkAddress = addresses.sinkAddress;
        }
        if (!presetData.mosaicRentalFeeSinkAddress) {
            presetData.mosaicRentalFeeSinkAddress = addresses.sinkAddress;
        }
        if (!presetData.namespaceRentalFeeSinkAddress) {
            presetData.namespaceRentalFeeSinkAddress = addresses.sinkAddress;
        }
        presetData.networkIdentifier = BootstrapUtils_1.BootstrapUtils.getNetworkIdentifier(presetData.networkType);
        presetData.networkName = BootstrapUtils_1.BootstrapUtils.getNetworkName(presetData.networkType);
        if (!presetData.nemesisGenerationHashSeed) {
            presetData.nemesisGenerationHashSeed = addresses.nemesisGenerationHashSeed;
        }
        const privateKeySecurityMode = CryptoUtils_1.CryptoUtils.getPrivateKeySecurityMode(presetData.privateKeySecurityMode);
        if (presetData.nemesis) {
            addresses.nemesisSigner = this.generateAccount(networkType, privateKeySecurityMode, ConfigService_1.KeyName.NemesisSigner, oldAddresses === null || oldAddresses === void 0 ? void 0 : oldAddresses.nemesisSigner, presetData.nemesis.nemesisSignerPrivateKey, presetData.nemesisSignerPublicKey);
            presetData.nemesisSignerPublicKey = addresses.nemesisSigner.publicKey;
            presetData.nemesis.nemesisSignerPrivateKey = await CommandUtils_1.CommandUtils.resolvePrivateKey(presetData.networkType, addresses.nemesisSigner, ConfigService_1.KeyName.NemesisSigner, '', 'creating the network nemesis seed and configuration');
        }
        const nemesisSignerAddress = symbol_sdk_1.Address.createFromPublicKey(presetData.nemesisSignerPublicKey, networkType);
        if (!presetData.currencyMosaicId)
            presetData.currencyMosaicId = BootstrapUtils_1.BootstrapUtils.toHex(symbol_sdk_1.MosaicId.createFromNonce(symbol_sdk_1.MosaicNonce.createFromNumber(0), nemesisSignerAddress).toHex());
        if (!presetData.harvestingMosaicId) {
            if (!presetData.nemesis) {
                throw new Error('nemesis must be defined!');
            }
            if (presetData.nemesis.mosaics && presetData.nemesis.mosaics.length > 1) {
                presetData.harvestingMosaicId = BootstrapUtils_1.BootstrapUtils.toHex(symbol_sdk_1.MosaicId.createFromNonce(symbol_sdk_1.MosaicNonce.createFromNumber(1), nemesisSignerAddress).toHex());
            }
            else {
                presetData.harvestingMosaicId = presetData.currencyMosaicId;
            }
        }
        if (presetData.nemesis) {
            if (oldAddresses) {
                // Nemesis configuration cannot be changed on upgrade.
                addresses.mosaics = oldAddresses.mosaics;
            }
            else {
                if (presetData.nemesis.mosaics) {
                    const mosaics = [];
                    presetData.nemesis.mosaics.forEach((m, index) => {
                        const accounts = this.generateAddresses(networkType, privateKeySecurityMode, m.accounts);
                        mosaics.push({
                            id: index ? presetData.currencyMosaicId : presetData.harvestingMosaicId,
                            name: m.name,
                            type: index ? 'harvest' : 'currency',
                            accounts,
                        });
                    });
                    presetData.nemesis.mosaics.forEach((m, index) => {
                        const accounts = mosaics[index].accounts;
                        if (!m.currencyDistributions) {
                            const nodeMainAccounts = (addresses.nodes || []).filter((node) => node.main);
                            const totalAccounts = (m.accounts || 0) + nodeMainAccounts.length;
                            const amountPerAccount = Math.floor(m.supply / totalAccounts);
                            m.currencyDistributions = [
                                ...accounts.map((a) => ({ address: a.address, amount: amountPerAccount })),
                                ...nodeMainAccounts.map((n) => ({ address: n.main.address, amount: amountPerAccount })),
                            ];
                            if (m.currencyDistributions.length)
                                m.currencyDistributions[0].amount += m.supply - totalAccounts * amountPerAccount;
                        }
                        const supplied = m.currencyDistributions.map((d) => d.amount).reduce((a, b) => a + b, 0);
                        if (m.supply != supplied) {
                            throw new Error(`Invalid nemgen total supplied value, expected ${m.supply} but total is ${supplied}`);
                        }
                    });
                    addresses.mosaics = mosaics;
                }
            }
        }
        return addresses;
    }
    generateAddresses(networkType, privateKeySecurityMode, size) {
        return ConfigLoader.getArray(size).map(() => this.generateAccount(networkType, privateKeySecurityMode, ConfigService_1.KeyName.NemesisAccount, undefined, undefined, undefined));
    }
    getAccount(networkType, publicKey, privateKey) {
        if (privateKey) {
            return symbol_sdk_1.Account.createFromPrivateKey(privateKey, networkType);
        }
        if (publicKey) {
            return symbol_sdk_1.PublicAccount.createFromPublicKey(publicKey, networkType);
        }
        return undefined;
    }
    toConfig(account) {
        if (account instanceof symbol_sdk_1.Account) {
            return {
                privateKey: account.privateKey,
                publicKey: account.publicKey,
                address: account.address.plain(),
            };
        }
        return {
            publicKey: account.publicKey,
            address: account.address.plain(),
        };
    }
    generateAccount(networkType, privateKeySecurityMode, keyName, oldStoredAccount, privateKey, publicKey) {
        var _a;
        const oldAccount = this.getAccount(networkType, oldStoredAccount === null || oldStoredAccount === void 0 ? void 0 : oldStoredAccount.publicKey.toUpperCase(), (_a = oldStoredAccount === null || oldStoredAccount === void 0 ? void 0 : oldStoredAccount.privateKey) === null || _a === void 0 ? void 0 : _a.toUpperCase());
        const newAccount = this.getAccount(networkType, publicKey === null || publicKey === void 0 ? void 0 : publicKey.toUpperCase(), privateKey === null || privateKey === void 0 ? void 0 : privateKey.toUpperCase());
        if (oldAccount && !newAccount) {
            logger.info(`Reusing ${keyName} account ${oldAccount.address.plain()}`);
            return this.toConfig(oldAccount);
        }
        if (!oldAccount && newAccount) {
            logger.info(`${keyName} Account ${newAccount.address.plain()} has been provided`);
            return this.toConfig(newAccount);
        }
        if (oldAccount && newAccount) {
            if (oldAccount.address.equals(newAccount.address)) {
                logger.info(`Reusing ${keyName} account ${oldAccount.address.plain()}`);
                return Object.assign(Object.assign({}, this.toConfig(oldAccount)), this.toConfig(newAccount));
            }
            logger.info(`Old ${keyName} Account ${oldAccount.address.plain()} has been changed. New ${keyName} Account is ${newAccount.address.plain()}`);
            return this.toConfig(newAccount);
        }
        //Generation validation.
        if (keyName === ConfigService_1.KeyName.Main &&
            (privateKeySecurityMode === model_1.PrivateKeySecurityMode.PROMPT_ALL ||
                privateKeySecurityMode === model_1.PrivateKeySecurityMode.PROMPT_MAIN_TRANSPORT ||
                privateKeySecurityMode === model_1.PrivateKeySecurityMode.PROMPT_MAIN)) {
            throw new BootstrapUtils_1.KnownError(`Account ${keyName} cannot be generated when Private Key Security Mode is ${privateKeySecurityMode}. Account won't be stored anywhere!. Please use ${model_1.PrivateKeySecurityMode.ENCRYPT}, or provider your ${keyName} account with custom presets!`);
        }
        if (keyName === ConfigService_1.KeyName.Transport &&
            (privateKeySecurityMode === model_1.PrivateKeySecurityMode.PROMPT_ALL ||
                privateKeySecurityMode === model_1.PrivateKeySecurityMode.PROMPT_MAIN_TRANSPORT)) {
            throw new BootstrapUtils_1.KnownError(`Account ${keyName} cannot be generated when Private Key Security Mode is ${privateKeySecurityMode}. Account won't be stored anywhere!. Please use ${model_1.PrivateKeySecurityMode.ENCRYPT}, ${model_1.PrivateKeySecurityMode.PROMPT_MAIN}, or provider your ${keyName} account with custom presets!`);
        }
        else {
            if (privateKeySecurityMode === model_1.PrivateKeySecurityMode.PROMPT_ALL) {
                throw new BootstrapUtils_1.KnownError(`Account ${keyName} cannot be generated when Private Key Security Mode is ${privateKeySecurityMode}. Account won't be stored anywhere! Please use ${model_1.PrivateKeySecurityMode.ENCRYPT}, ${model_1.PrivateKeySecurityMode.PROMPT_MAIN}, ${model_1.PrivateKeySecurityMode.PROMPT_MAIN_TRANSPORT}, or provider your ${keyName} account with custom presets!`);
            }
        }
        logger.info(`Generating ${keyName} account...`);
        return ConfigLoader.toConfig(symbol_sdk_1.Account.generateNewAccount(networkType));
    }
    generateNodeAccount(oldNodeAccount, presetData, index, nodePreset, networkType) {
        const privateKeySecurityMode = CryptoUtils_1.CryptoUtils.getPrivateKeySecurityMode(presetData.privateKeySecurityMode);
        const name = nodePreset.name || `node-${index}`;
        const main = this.generateAccount(networkType, privateKeySecurityMode, ConfigService_1.KeyName.Main, oldNodeAccount === null || oldNodeAccount === void 0 ? void 0 : oldNodeAccount.main, nodePreset.mainPrivateKey, nodePreset.mainPublicKey);
        const transport = this.generateAccount(networkType, privateKeySecurityMode, ConfigService_1.KeyName.Transport, oldNodeAccount === null || oldNodeAccount === void 0 ? void 0 : oldNodeAccount.transport, nodePreset.transportPrivateKey, nodePreset.transportPublicKey);
        const friendlyName = nodePreset.friendlyName || main.publicKey.substr(0, 7);
        const nodeAccount = {
            name,
            friendlyName,
            roles: nodePreset.roles || '',
            main: main,
            transport: transport,
        };
        const useRemoteAccount = nodePreset.nodeUseRemoteAccount || presetData.nodeUseRemoteAccount;
        if (useRemoteAccount && (nodePreset.harvesting || nodePreset.voting))
            nodeAccount.remote = this.generateAccount(networkType, privateKeySecurityMode, ConfigService_1.KeyName.Remote, oldNodeAccount === null || oldNodeAccount === void 0 ? void 0 : oldNodeAccount.remote, nodePreset.remotePrivateKey, nodePreset.remotePublicKey);
        if (nodePreset.voting)
            nodeAccount.voting = this.toConfig((oldNodeAccount === null || oldNodeAccount === void 0 ? void 0 : oldNodeAccount.voting) ? symbol_sdk_1.PublicAccount.createFromPublicKey(oldNodeAccount.voting.publicKey, networkType)
                : symbol_sdk_1.Account.generateNewAccount(networkType));
        if (nodePreset.harvesting)
            nodeAccount.vrf = this.generateAccount(networkType, privateKeySecurityMode, ConfigService_1.KeyName.VRF, oldNodeAccount === null || oldNodeAccount === void 0 ? void 0 : oldNodeAccount.vrf, nodePreset.vrfPrivateKey, nodePreset.vrfPublicKey);
        return nodeAccount;
    }
    async generateNodeAccounts(oldAddresses, presetData, networkType) {
        return Promise.all(presetData.nodes.map((node, index) => { var _a; return this.generateNodeAccount((_a = oldAddresses === null || oldAddresses === void 0 ? void 0 : oldAddresses.nodes) === null || _a === void 0 ? void 0 : _a[index], presetData, index, node, networkType); }));
    }
    static getArray(size) {
        return [...Array(size).keys()];
    }
    loadCustomPreset(customPreset, password) {
        if (!customPreset) {
            return {};
        }
        if (!fs_1.existsSync(customPreset)) {
            throw new BootstrapUtils_1.KnownError(`Custom preset '${customPreset}' doesn't exist. Have you provided the right --customPreset <customPrestFileLocation> ?`);
        }
        return BootstrapUtils_1.BootstrapUtils.loadYaml(customPreset, password);
    }
    loadAssembly(root, preset, assembly) {
        if (!assembly) {
            return {};
        }
        const fileLocation = `${root}/presets/${preset}/assembly-${assembly}.yml`;
        if (!fs_1.existsSync(fileLocation)) {
            throw new BootstrapUtils_1.KnownError(`Assembly '${assembly}' is not valid for preset '${preset}'. Have you provided the right --preset <preset> --assembly <assembly> ?`);
        }
        return BootstrapUtils_1.BootstrapUtils.loadYaml(fileLocation, false);
    }
    createPresetData(params) {
        var _a, _b, _c;
        const customPreset = params.customPreset;
        const customPresetObject = params.customPresetObject;
        const oldPresetData = params.oldPresetData;
        const customPresetFileObject = this.loadCustomPreset(customPreset, params.password);
        const preset = params.preset || ((_a = params.customPresetObject) === null || _a === void 0 ? void 0 : _a.preset) || (customPresetFileObject === null || customPresetFileObject === void 0 ? void 0 : customPresetFileObject.preset) || (oldPresetData === null || oldPresetData === void 0 ? void 0 : oldPresetData.preset) ||
            ConfigService_1.Preset.bootstrap;
        const assembly = params.assembly || ((_b = params.customPresetObject) === null || _b === void 0 ? void 0 : _b.assembly) || (customPresetFileObject === null || customPresetFileObject === void 0 ? void 0 : customPresetFileObject.assembly) || ((_c = params.oldPresetData) === null || _c === void 0 ? void 0 : _c.assembly);
        const root = params.root;
        const sharedPreset = BootstrapUtils_1.BootstrapUtils.loadYaml(path_1.join(root, 'presets', 'shared.yml'), false);
        const networkPreset = BootstrapUtils_1.BootstrapUtils.loadYaml(`${root}/presets/${preset}/network.yml`, false);
        const assemblyPreset = this.loadAssembly(root, preset, assembly);
        //Deep merge
        const inflation = (customPresetObject === null || customPresetObject === void 0 ? void 0 : customPresetObject.inflation) || (customPresetFileObject === null || customPresetFileObject === void 0 ? void 0 : customPresetFileObject.inflation) || (assemblyPreset === null || assemblyPreset === void 0 ? void 0 : assemblyPreset.inflation) || (networkPreset === null || networkPreset === void 0 ? void 0 : networkPreset.inflation) || (sharedPreset === null || sharedPreset === void 0 ? void 0 : sharedPreset.inflation) ||
            [];
        const presetData = _.merge(sharedPreset, networkPreset, assemblyPreset, customPresetFileObject, customPresetObject, {
            preset,
        });
        if (presetData.assemblies && !assembly) {
            throw new Error(`Preset ${preset} requires assembly (-a, --assembly option). Possible values are: ${presetData.assemblies}`);
        }
        presetData.inflation = inflation;
        if (!ConfigLoader.presetInfoLogged) {
            logger.info(`Generating config from preset '${preset}'`);
            if (assembly) {
                logger.info(`Using assembly '${assembly}'`);
            }
            if (customPreset) {
                logger.info(`Using custom preset file '${customPreset}'`);
            }
        }
        ConfigLoader.presetInfoLogged = true;
        const presetDataWithDynamicDefaults = Object.assign(Object.assign({ version: 1, preset: preset, assembly: assembly || '' }, presetData), { nodes: this.dynamicDefaultNodeConfiguration(presetData.nodes) });
        return _.merge(oldPresetData || {}, this.expandRepeat(presetDataWithDynamicDefaults));
    }
    dynamicDefaultNodeConfiguration(nodes) {
        return _.map(nodes || [], (node) => {
            return Object.assign(Object.assign({}, this.getDefaultConfiguration(node)), node);
        });
    }
    getDefaultConfiguration(node) {
        if (node.harvesting && node.api) {
            return {
                syncsource: true,
                filespooling: true,
                partialtransaction: true,
                addressextraction: true,
                mongo: true,
                zeromq: true,
                enableAutoSyncCleanup: false,
            };
        }
        if (node.api) {
            return {
                syncsource: false,
                filespooling: true,
                partialtransaction: true,
                addressextraction: true,
                mongo: true,
                zeromq: true,
                enableAutoSyncCleanup: false,
            };
        }
        // peer only (harvesting or not).
        return {
            syncsource: true,
            filespooling: false,
            partialtransaction: false,
            addressextraction: false,
            mongo: false,
            zeromq: false,
            enableAutoSyncCleanup: true,
        };
    }
    static resolveRoles(nodePreset) {
        if (nodePreset.roles) {
            return nodePreset.roles;
        }
        const roles = [];
        if (nodePreset.syncsource) {
            roles.push('Peer');
        }
        if (nodePreset.api) {
            roles.push('Api');
        }
        if (nodePreset.voting) {
            roles.push('Voting');
        }
        return roles.join(',');
    }
    static toConfig(account) {
        if (account instanceof symbol_sdk_1.Account) {
            return {
                privateKey: account.privateKey,
                publicKey: account.publicAccount.publicKey,
                address: account.address.plain(),
            };
        }
        else {
            return {
                publicKey: account.publicKey,
                address: account.address.plain(),
            };
        }
    }
    expandRepeat(presetData) {
        return Object.assign(Object.assign({}, presetData), { databases: this.expandServicesRepeat(presetData, presetData.databases || []), nodes: this.expandServicesRepeat(presetData, presetData.nodes || []), gateways: this.expandServicesRepeat(presetData, presetData.gateways || []), explorers: this.expandServicesRepeat(presetData, presetData.explorers || []), wallets: this.expandServicesRepeat(presetData, presetData.wallets || []), faucets: this.expandServicesRepeat(presetData, presetData.faucets || []), nemesis: this.applyValueTemplate(presetData, presetData.nemesis) });
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    applyValueTemplate(context, value) {
        if (!value) {
            return value;
        }
        if (_.isArray(value)) {
            return this.expandServicesRepeat(context, value);
        }
        if (_.isObject(value)) {
            return _.mapValues(value, (v) => this.applyValueTemplate(Object.assign(Object.assign({}, context), value), v));
        }
        if (!_.isString(value)) {
            return value;
        }
        return BootstrapUtils_1.BootstrapUtils.runTemplate(value, context);
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    expandServicesRepeat(context, services) {
        return _.flatMap(services || [], (service) => {
            if (service.repeat === 0) {
                return [];
            }
            return _.range(service.repeat || 1).map((index) => {
                return _.omit(_.mapValues(service, (v) => this.applyValueTemplate(Object.assign(Object.assign(Object.assign({}, context), service), { $index: index }), v)), 'repeat');
            });
        });
    }
    loadExistingPresetDataIfPreset(target, password) {
        const generatedPresetLocation = this.getGeneratedPresetLocation(target);
        if (fs_1.existsSync(generatedPresetLocation)) {
            return BootstrapUtils_1.BootstrapUtils.loadYaml(generatedPresetLocation, password);
        }
        return undefined;
    }
    loadExistingPresetData(target, password) {
        const presetData = this.loadExistingPresetDataIfPreset(target, password);
        if (!presetData) {
            throw new Error(`The file ${this.getGeneratedPresetLocation(target)} doesn't exist. Have you executed the 'config' command?`);
        }
        return presetData;
    }
    loadExistingAddressesIfPreset(target, password) {
        const generatedAddressLocation = this.getGeneratedAddressLocation(target);
        if (fs_1.existsSync(generatedAddressLocation)) {
            const presetData = this.loadExistingPresetData(target, password);
            return this.migrateAddresses(BootstrapUtils_1.BootstrapUtils.loadYaml(generatedAddressLocation, password), presetData.networkType);
        }
        return undefined;
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    migrateAddresses(addresses, networkType) {
        const migrations = this.getAddressesMigration(networkType);
        return BootstrapUtils_1.BootstrapUtils.migrate('addresses.yml', addresses, migrations);
    }
    getAddressesMigration(networkType) {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const configLoader = this;
        return [
            {
                description: 'Key names migration',
                migrate(from) {
                    (from.nodes || []).forEach((nodeAddresses) => {
                        var _a;
                        if (nodeAddresses.signing) {
                            nodeAddresses.main = nodeAddresses.signing;
                        }
                        else {
                            if (nodeAddresses.ssl) {
                                nodeAddresses.main = ConfigLoader.toConfig(symbol_sdk_1.Account.createFromPrivateKey(nodeAddresses.ssl.privateKey, networkType));
                            }
                        }
                        nodeAddresses.transport = configLoader.generateAccount(networkType, model_1.PrivateKeySecurityMode.ENCRYPT, ConfigService_1.KeyName.Transport, undefined, (_a = nodeAddresses === null || nodeAddresses === void 0 ? void 0 : nodeAddresses.node) === null || _a === void 0 ? void 0 : _a.privateKey, undefined);
                        delete nodeAddresses.node;
                        delete nodeAddresses.signing;
                        delete nodeAddresses.ssl;
                    });
                    return from;
                },
            },
        ];
    }
    loadExistingAddresses(target, password) {
        const addresses = this.loadExistingAddressesIfPreset(target, password);
        if (!addresses) {
            throw new Error(`The file ${this.getGeneratedAddressLocation(target)} doesn't exist. Have you executed the 'config' command?`);
        }
        return addresses;
    }
    getGeneratedPresetLocation(target) {
        return path_1.join(target, 'preset.yml');
    }
    getGeneratedAddressLocation(target) {
        return path_1.join(target, 'addresses.yml');
    }
}
exports.ConfigLoader = ConfigLoader;
ConfigLoader.presetInfoLogged = false;
