// SPDX-License-Identifier: Apache-2.0
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
import fs from 'node:fs';
import path from 'node:path';
import { SoloError } from './errors/solo-error.js';
import { IllegalArgumentError } from './errors/illegal-argument-error.js';
import { MissingArgumentError } from './errors/missing-argument-error.js';
import * as yaml from 'yaml';
import dot from 'dot-object';
import { readFile, writeFile } from 'node:fs/promises';
import { Flags as flags } from '../commands/flags.js';
import { Templates } from './templates.js';
import * as constants from './constants.js';
import * as helpers from './helpers.js';
import { inject, injectable } from 'tsyringe-neo';
import { patchInject } from './dependency-injection/container-helper.js';
import { InjectTokens } from './dependency-injection/inject-tokens.js';
import { PathEx } from '../business/utils/path-ex.js';
import { AccountManager } from './account-manager.js';
import { LocalConfigRuntimeState } from '../business/runtime-state/config/local/local-config-runtime-state.js';
import { BlockNodesJsonWrapper } from './block-nodes-json-wrapper.js';
import { NamespaceName } from '../types/namespace/namespace-name.js';
import { Address } from '../business/address/address.js';
import * as versions from '../../version.js';
import { Numbers } from '../business/utils/numbers.js';
import { SemanticVersion } from '../business/utils/semantic-version.js';
let ProfileManager = class ProfileManager {
    logger;
    configManager;
    cacheDir;
    k8Factory;
    remoteConfig;
    accountManager;
    localConfig;
    constructor(logger, configManager, cacheDirectory, k8Factory, remoteConfig, accountManager, localConfig) {
        this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
        this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
        this.cacheDir = PathEx.resolve(patchInject(cacheDirectory, InjectTokens.CacheDir, this.constructor.name));
        this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
        this.remoteConfig = patchInject(remoteConfig, InjectTokens.RemoteConfigRuntimeState, this.constructor.name);
        this.accountManager = patchInject(accountManager, InjectTokens.AccountManager, this.constructor.name);
        this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name);
    }
    /**
     * Set value in the YAML object
     * @param itemPath - item path in the yaml
     * @param value - value to be set
     * @param yamlRoot - root of the YAML object
     * @returns
     */
    _setValue(itemPath, value, yamlRoot) {
        // find the location where to set the value in the YAML
        const itemPathParts = itemPath.split('.');
        let parent = yamlRoot;
        let current = parent;
        let previousItemPath = '';
        for (const itemPathPart of itemPathParts) {
            if (Numbers.isNumeric(itemPathPart)) {
                const itemPathIndex = Number.parseInt(itemPathPart, 10); // numeric path part can only be array index
                if (!Array.isArray(parent[previousItemPath])) {
                    parent[previousItemPath] = [];
                }
                const parentArray = parent[previousItemPath];
                if (!parentArray[itemPathIndex]) {
                    parentArray[itemPathIndex] = {};
                }
                parent = parentArray;
                previousItemPath = itemPathIndex;
                current = parent[itemPathIndex];
            }
            else {
                if (!current[itemPathPart]) {
                    current[itemPathPart] = {};
                }
                parent = current;
                previousItemPath = itemPathPart;
                current = parent[itemPathPart];
            }
        }
        parent[previousItemPath] = value;
        return yamlRoot;
    }
    /**
     * Set items for the chart
     * @param itemPath - item path in the YAML, if empty then root of the YAML object will be used
     * @param items - the element object
     * @param yamlRoot - root of the YAML object to update
     */
    _setChartItems(itemPath, items, yamlRoot) {
        if (!items) {
            return;
        }
        const dotItems = dot.dot(items);
        for (const key in dotItems) {
            let itemKey = key;
            // if it is an array key like extraEnv[0].JAVA_OPTS, convert it into a dot separated key as extraEnv.0.JAVA_OPTS
            if (key.includes('[')) {
                itemKey = key.replace('[', '.').replace(']', '');
            }
            if (itemPath) {
                this._setValue(`${itemPath}.${itemKey}`, dotItems[key], yamlRoot);
            }
            else {
                this._setValue(itemKey, dotItems[key], yamlRoot);
            }
        }
    }
    async prepareStagingDirectory(consensusNodes, nodeAliases, yamlRoot, domainNamesMapping, deploymentName, applicationPropertiesPath) {
        const accountMap = this.accountManager.getNodeAccountMap(consensusNodes.map((node) => node.name), deploymentName);
        // set consensus pod level resources
        for (const [nodeIndex, nodeAlias] of nodeAliases.entries()) {
            this._setValue(`hedera.nodes.${nodeIndex}.name`, nodeAlias, yamlRoot);
            this._setValue(`hedera.nodes.${nodeIndex}.nodeId`, `${Templates.nodeIdFromNodeAlias(nodeAlias)}`, yamlRoot);
            this._setValue(`hedera.nodes.${nodeIndex}.accountId`, accountMap.get(nodeAlias), yamlRoot);
        }
        const stagingDirectory = Templates.renderStagingDir(this.configManager.getFlag(flags.cacheDir), this.configManager.getFlag(flags.releaseTag));
        if (!fs.existsSync(stagingDirectory)) {
            fs.mkdirSync(stagingDirectory, { recursive: true });
        }
        const releaseTag = this.configManager.getFlag(flags.releaseTag);
        const needsConfigTxt = versions.needsConfigTxtForConsensusVersion(releaseTag);
        let configTxtPath;
        if (needsConfigTxt) {
            configTxtPath = await this.prepareConfigTxt(accountMap, consensusNodes, stagingDirectory, releaseTag, domainNamesMapping, this.configManager.getFlag(flags.app), this.configManager.getFlag(flags.chainId));
        }
        // Update application.properties with shard and realm
        await this.updateApplicationPropertiesWithRealmAndShard(applicationPropertiesPath, this.localConfig.configuration.realmForDeployment(deploymentName), this.localConfig.configuration.shardForDeployment(deploymentName));
        await this.updateApplicationPropertiesForBlockNode(applicationPropertiesPath);
        for (const flag of flags.nodeConfigFileFlags.values()) {
            const sourceFilePath = this.configManager.getFlagFile(flag);
            const currentWorkingDirectory = process.env.INIT_CWD || process.cwd();
            const sourceAbsoluteFilePath = PathEx.resolve(currentWorkingDirectory, sourceFilePath);
            if (!fs.existsSync(sourceAbsoluteFilePath)) {
                throw new SoloError(`Configuration file does not exist for: ${flag.name}, absolute path: ${sourceAbsoluteFilePath}, path: ${sourceFilePath}`);
            }
            const destinationFileName = path.basename(flag.definition.defaultValue);
            const destinationPath = PathEx.join(stagingDirectory, 'templates', destinationFileName);
            this.logger.debug(`Copying configuration file to staging: ${sourceAbsoluteFilePath} -> ${destinationPath}`);
            fs.cpSync(sourceAbsoluteFilePath, destinationPath, { force: true });
        }
        if (configTxtPath) {
            this._setFileContentsAsValue('hedera.configMaps.configTxt', configTxtPath, yamlRoot);
        }
        this._setFileContentsAsValue('hedera.configMaps.log4j2Xml', PathEx.joinWithRealPath(stagingDirectory, 'templates', 'log4j2.xml'), yamlRoot);
        this._setFileContentsAsValue('hedera.configMaps.settingsTxt', PathEx.joinWithRealPath(stagingDirectory, 'templates', 'settings.txt'), yamlRoot);
        this._setFileContentsAsValue('hedera.configMaps.applicationProperties', PathEx.joinWithRealPath(stagingDirectory, 'templates', 'application.properties'), yamlRoot);
        this._setFileContentsAsValue('hedera.configMaps.apiPermissionsProperties', PathEx.joinWithRealPath(stagingDirectory, 'templates', 'api-permission.properties'), yamlRoot);
        this._setFileContentsAsValue('hedera.configMaps.bootstrapProperties', PathEx.joinWithRealPath(stagingDirectory, 'templates', 'bootstrap.properties'), yamlRoot);
        this._setFileContentsAsValue('hedera.configMaps.applicationEnv', PathEx.joinWithRealPath(stagingDirectory, 'templates', 'application.env'), yamlRoot);
        try {
            if (this.remoteConfig.configuration.state.blockNodes.length === 0 &&
                this.remoteConfig.configuration.state.externalBlockNodes.length === 0) {
                return;
            }
        }
        catch {
            // quick fix for tests where field on remote config are unaccessible
            return;
        }
        for (const node of consensusNodes) {
            const blockNodesJsonData = new BlockNodesJsonWrapper(node.blockNodeMap, node.externalBlockNodeMap).toJSON();
            let nodeIndex = 0;
            for (const [index, nodeAlias] of nodeAliases.entries()) {
                if (nodeAlias === node.name) {
                    nodeIndex = index;
                }
            }
            // Create a unique filename for each consensus node
            const blockNodesJsonFilename = `${constants.BLOCK_NODES_JSON_FILE.replace('.json', '')}-${node.name}.json`;
            const blockNodesJsonPath = PathEx.join(constants.SOLO_CACHE_DIR, blockNodesJsonFilename);
            fs.writeFileSync(blockNodesJsonPath, JSON.stringify(JSON.parse(blockNodesJsonData), undefined, 2));
            this._setFileContentsAsValue(`hedera.nodes.${nodeIndex}.blockNodesJson`, blockNodesJsonPath, yamlRoot);
        }
    }
    resourcesForNetworkUpgrade(itemPath, fileName, stagingDirectory, yamlRoot) {
        const filePath = PathEx.join(stagingDirectory, 'templates', fileName);
        if (!fs.existsSync(filePath)) {
            return;
        }
        this._setFileContentsAsValue(itemPath, filePath, yamlRoot);
    }
    /**
     * Prepare a values file for Solo Helm chart
     * @param consensusNodes - the list of consensus nodes
     * @param domainNamesMapping
     * @param deploymentName
     * @param applicationPropertiesPath
     * @param jfrFile - the name of the custom JFR settings file to use for recording (basename only)
     * @returns mapping of cluster-ref to the full path to the values file
     */
    async prepareValuesForSoloChart(consensusNodes, domainNamesMapping, deploymentName, applicationPropertiesPath, jfrFile = '') {
        const filesMapping = {};
        for (const [clusterReference] of this.remoteConfig.getClusterRefs()) {
            const nodeAliases = consensusNodes
                .filter((node) => node.cluster === clusterReference)
                .map((node) => node.name);
            // generate the YAML
            const yamlRoot = {};
            await this.prepareStagingDirectory(consensusNodes, nodeAliases, yamlRoot, domainNamesMapping, deploymentName, applicationPropertiesPath);
            // If a JFR settings file is provided, read the defaults from solo-values.yaml,
            // find the JAVA_OPTS entry in defaults.root.extraEnv, and append the
            // -XX:StartFlightRecording flags so that the recorder starts automatically
            // when the consensus node JVM launches.
            if (jfrFile !== '') {
                const soloValuesYaml = yaml.parse(fs.readFileSync(constants.SOLO_DEPLOYMENT_VALUES_FILE, 'utf8'));
                const extraEnvironment = soloValuesYaml?.defaults?.root?.extraEnv ?? [];
                const javaOption = extraEnvironment.find((environmentObject) => environmentObject.name === 'JAVA_OPTS');
                if (javaOption) {
                    javaOption.value +=
                        ' -XX:StartFlightRecording=dumponexit=true' +
                            `,settings=${constants.HEDERA_HAPI_PATH}/data/config/${jfrFile}` +
                            `,filename=${constants.HEDERA_HAPI_PATH}/output/recording.jfr`;
                }
                else {
                    this.logger.warn(`JAVA_OPTS not found in ${constants.SOLO_DEPLOYMENT_VALUES_FILE}; JFR settings file '${jfrFile}' will not be applied`);
                }
                this._setChartItems('defaults.root', soloValuesYaml.defaults.root, yamlRoot);
            }
            const cachedValuesFile = PathEx.join(this.cacheDir, `solo-${clusterReference}.yaml`);
            filesMapping[clusterReference] = await this.writeToYaml(cachedValuesFile, yamlRoot);
        }
        return filesMapping;
    }
    async bumpHederaConfigVersion(applicationPropertiesPath) {
        const fileContents = await readFile(applicationPropertiesPath, 'utf8');
        const lines = fileContents.split('\n');
        for (const line of lines) {
            if (line.startsWith('hedera.config.version=')) {
                const version = Number.parseInt(line.split('=')[1], 10) + 1;
                lines[lines.indexOf(line)] = `hedera.config.version=${version}`;
                break;
            }
        }
        await writeFile(applicationPropertiesPath, lines.join('\n'));
    }
    async updateApplicationPropertiesForBlockNode(applicationPropertiesPath) {
        const blockNodes = this.remoteConfig.configuration.components.state.blockNodes;
        const hasDeployedBlockNodes = blockNodes.length > 0;
        if (!hasDeployedBlockNodes) {
            return;
        }
        const lines = await readFile(applicationPropertiesPath, 'utf8').then((fileText) => fileText.split('\n'));
        const streamMode = constants.BLOCK_STREAM_STREAM_MODE;
        const writerMode = constants.BLOCK_STREAM_WRITER_MODE;
        let streamModeUpdated = false;
        let writerModeUpdated = false;
        for (const line of lines) {
            if (line.startsWith('blockStream.streamMode=')) {
                lines[lines.indexOf(line)] = `blockStream.streamMode=${streamMode}`;
                streamModeUpdated = true;
                continue;
            }
            if (line.startsWith('blockStream.writerMode=')) {
                lines[lines.indexOf(line)] = `blockStream.writerMode=${writerMode}`;
                writerModeUpdated = true;
            }
        }
        if (!streamModeUpdated) {
            lines.push(`blockStream.streamMode=${streamMode}`);
        }
        if (!writerModeUpdated) {
            lines.push(`blockStream.writerMode=${writerMode}`);
        }
        await writeFile(applicationPropertiesPath, lines.join('\n') + '\n');
    }
    async updateApplicationPropertiesWithRealmAndShard(applicationPropertiesPath, realm, shard) {
        const fileContents = await readFile(applicationPropertiesPath, 'utf8');
        const lines = fileContents.split('\n');
        let realmUpdated = false;
        let shardUpdated = false;
        for (const line of lines) {
            if (line.startsWith('hedera.realm=')) {
                lines[lines.indexOf(line)] = `hedera.realm=${realm}`;
                realmUpdated = true;
                continue;
            }
            if (line.startsWith('hedera.shard=')) {
                lines[lines.indexOf(line)] = `hedera.shard=${shard}`;
                shardUpdated = true;
            }
        }
        if (!realmUpdated) {
            lines.push(`hedera.realm=${realm}`);
        }
        if (!shardUpdated) {
            lines.push(`hedera.shard=${shard}`);
        }
        let releaseTag = new SemanticVersion(versions.HEDERA_PLATFORM_VERSION);
        try {
            releaseTag = this.remoteConfig.configuration.versions.consensusNode;
        }
        catch {
            // Guard
        }
        let tssEnabled = false;
        try {
            tssEnabled = this.remoteConfig.configuration.state.tssEnabled;
        }
        catch {
            // Guard
        }
        if (!releaseTag.lessThan(versions.MINIMUM_HIERO_PLATFORM_VERSION_FOR_TSS) && tssEnabled) {
            lines.push('tss.hintsEnabled=true', 'tss.historyEnabled=true');
            if (this.remoteConfig.configuration.state.wrapsEnabled) {
                lines.push('tss.wrapsEnabled=true');
            }
        }
        await writeFile(applicationPropertiesPath, lines.join('\n') + '\n');
    }
    async prepareValuesForNodeTransaction(applicationPropertiesPath, configTxtPath) {
        const yamlRoot = {};
        if (configTxtPath) {
            this._setFileContentsAsValue('hedera.configMaps.configTxt', configTxtPath, yamlRoot);
        }
        await this.bumpHederaConfigVersion(applicationPropertiesPath);
        this._setFileContentsAsValue('hedera.configMaps.applicationProperties', applicationPropertiesPath, yamlRoot);
        const cachedValuesFile = PathEx.join(this.cacheDir, 'solo-node-transaction.yaml');
        return this.writeToYaml(cachedValuesFile, yamlRoot);
    }
    /**
     * Writes the YAML to file.
     *
     * @param cachedValuesFile - the target file to write the YAML root to.
     * @param yamlRoot - object to turn into YAML and write to file.
     */
    async writeToYaml(cachedValuesFile, yamlRoot) {
        return await new Promise((resolve, reject) => {
            fs.writeFile(cachedValuesFile, yaml.stringify(yamlRoot), (error) => {
                if (error) {
                    reject(error);
                }
                resolve(cachedValuesFile);
            });
        });
    }
    /**
     * Writes the contents of a file as a value for the given nested item path in the YAML object
     * @param itemPath - nested item path in the YAML object to store the file contents
     * @param valueFilePath - path to the file whose contents will be stored in the YAML object
     * @param yamlRoot - root of the YAML object
     */
    _setFileContentsAsValue(itemPath, valueFilePath, yamlRoot) {
        const fileContents = fs.readFileSync(valueFilePath, 'utf8');
        this._setValue(itemPath, fileContents, yamlRoot);
    }
    /**
     * Prepares config.txt file for the node
     * @param nodeAccountMap - the map of node aliases to account IDs
     * @param consensusNodes - the list of consensus nodes
     * @param destPath - path to the destination directory to write the config.txt file
     * @param releaseTagOverride - release tag override
     * @param domainNamesMapping
     * @param [appName] - the app name (default: HederaNode.jar)
     * @param [chainId] - chain ID (298 for local network)
     * @param [loadBalancerEnabled] - whether the load balancer is enabled (flag is not set by default)
     * @returns the config.txt file path
     */
    async prepareConfigTxt(nodeAccountMap, consensusNodes, destinationPath, releaseTagOverride, domainNamesMapping, appName = constants.HEDERA_APP_NAME, chainId = constants.HEDERA_CHAIN_ID) {
        let releaseTag = releaseTagOverride;
        if (!nodeAccountMap || nodeAccountMap.size === 0) {
            throw new MissingArgumentError('nodeAccountMap the map of node IDs to account IDs is required');
        }
        if (!releaseTag) {
            releaseTag = versions.HEDERA_PLATFORM_VERSION;
        }
        if (!fs.existsSync(destinationPath)) {
            throw new IllegalArgumentError(`config destPath does not exist: ${destinationPath}`, destinationPath);
        }
        const configFilePath = PathEx.join(destinationPath, 'config.txt');
        if (fs.existsSync(configFilePath)) {
            fs.unlinkSync(configFilePath);
        }
        // init variables
        const internalPort = +constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT;
        const externalPort = +constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT;
        const nodeStakeAmount = constants.HEDERA_NODE_DEFAULT_STAKE_AMOUNT;
        const releaseVersion = new SemanticVersion(releaseTag);
        try {
            const configLines = [`swirld, ${chainId}`, `app, ${appName}`];
            let nodeSeq = 0;
            for (const consensusNode of consensusNodes) {
                const internalIP = helpers.getInternalAddress(releaseVersion, NamespaceName.of(consensusNode.namespace), consensusNode.name);
                const address = await Address.getExternalAddress(consensusNode, this.k8Factory.getK8(consensusNode.context), externalPort);
                const account = nodeAccountMap.get(consensusNode.name);
                configLines.push(`address, ${nodeSeq}, ${nodeSeq}, ${consensusNode.name}, ${nodeStakeAmount}, ${internalIP}, ${internalPort}, ${address.hostString()}, ${address.port}, ${account}`);
                nodeSeq += 1;
            }
            // TODO: remove once we no longer need less than v0.56
            if (releaseVersion.minor >= 41 && releaseVersion.minor < 56) {
                configLines.push(`nextNodeId, ${nodeSeq}`);
            }
            fs.writeFileSync(configFilePath, configLines.join('\n'));
            return configFilePath;
        }
        catch (error) {
            throw new SoloError(`failed to generate config.txt, ${error instanceof Error ? error.message : 'unknown error'}`, error);
        }
    }
};
ProfileManager = __decorate([
    injectable(),
    __param(0, inject(InjectTokens.SoloLogger)),
    __param(1, inject(InjectTokens.ConfigManager)),
    __param(2, inject(InjectTokens.CacheDir)),
    __param(3, inject(InjectTokens.K8Factory)),
    __param(4, inject(InjectTokens.RemoteConfigRuntimeState)),
    __param(5, inject(InjectTokens.AccountManager)),
    __param(6, inject(InjectTokens.LocalConfigRuntimeState)),
    __metadata("design:paramtypes", [Object, Function, String, Object, Object, AccountManager,
        LocalConfigRuntimeState])
], ProfileManager);
export { ProfileManager };
//# sourceMappingURL=profile-manager.js.map