// 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); }
};
var BlockNodeCommand_1;
import { Listr } from 'listr2';
import { SoloError } from '../core/errors/solo-error.js';
import * as helpers from '../core/helpers.js';
import { checkDockerImageExists, showVersionBanner, sleep } from '../core/helpers.js';
import * as constants from '../core/constants.js';
import { BaseCommand } from './base.js';
import { Flags as flags } from './flags.js';
import { resolveNamespaceFromDeployment } from '../core/resolvers.js';
import { ListrLock } from '../core/lock/listr-lock.js';
import * as versions from '../../version.js';
import { ContainerReference } from '../integration/kube/resources/container/container-reference.js';
import { Duration } from '../core/time/duration.js';
import chalk from 'chalk';
import { CommandBuilder, CommandGroup, Subcommand } from '../core/command-path-builders/command-builder.js';
import { ComponentTypes } from '../core/config/remote/enumerations/component-types.js';
import { gte, lt, SemVer } from 'semver';
import { inject, injectable } from 'tsyringe-neo';
import { InjectTokens } from '../core/dependency-injection/inject-tokens.js';
import { BLOCK_NODE_IMAGE_NAME } from '../core/constants.js';
import { Version } from '../business/utils/version.js';
import { MINIMUM_HIERO_BLOCK_NODE_VERSION_FOR_NEW_LIVENESS_CHECK_PORT } from '../../version.js';
let BlockNodeCommand = class BlockNodeCommand extends BaseCommand {
    static { BlockNodeCommand_1 = this; }
    componentFactory;
    constructor(componentFactory) {
        super();
        this.componentFactory = componentFactory;
    }
    static COMMAND_NAME = 'block';
    static ADD_CONFIGS_NAME = 'addConfigs';
    static DESTROY_CONFIGS_NAME = 'destroyConfigs';
    static UPGRADE_CONFIGS_NAME = 'upgradeConfigs';
    static ADD_FLAGS_LIST = {
        required: [flags.deployment, flags.clusterRef],
        optional: [
            flags.blockNodeChartVersion,
            flags.chartDirectory,
            flags.devMode,
            flags.domainName,
            flags.enableIngress,
            flags.quiet,
            flags.valuesFile,
            flags.releaseTag,
            flags.imageTag,
        ],
    };
    static DESTROY_FLAGS_LIST = {
        required: [flags.deployment, flags.clusterRef],
        optional: [flags.chartDirectory, flags.devMode, flags.force, flags.quiet],
    };
    static UPGRADE_FLAGS_LIST = {
        required: [flags.deployment, flags.clusterRef],
        optional: [flags.chartDirectory, flags.devMode, flags.force, flags.quiet, flags.upgradeVersion],
    };
    async prepareValuesArgForBlockNode(config) {
        let valuesArgument = '';
        valuesArgument += helpers.prepareValuesFiles(constants.BLOCK_NODE_VALUES_FILE);
        if (config.valuesFile) {
            valuesArgument += helpers.prepareValuesFiles(config.valuesFile);
        }
        valuesArgument += helpers.populateHelmArguments({ nameOverride: config.releaseName });
        if (config.domainName) {
            valuesArgument += helpers.populateHelmArguments({
                'ingress.enabled': true,
                'ingress.hosts[0].host': config.domainName,
                'ingress.hosts[0].paths[0].path': '/',
                'ingress.hosts[0].paths[0].pathType': 'ImplementationSpecific',
            });
        }
        if (config.imageTag) {
            config.imageTag = Version.getValidSemanticVersion(config.imageTag, false, 'Block node image tag');
            if (!checkDockerImageExists(BLOCK_NODE_IMAGE_NAME, config.imageTag)) {
                throw new SoloError(`Local block node image with tag "${config.imageTag}" does not exist.`);
            }
            // use local image from docker engine
            valuesArgument += helpers.populateHelmArguments({
                'image.repository': BLOCK_NODE_IMAGE_NAME,
                'image.tag': config.imageTag,
                'image.pullPolicy': 'Never',
            });
        }
        return valuesArgument;
    }
    getNextReleaseName() {
        return (constants.BLOCK_NODE_RELEASE_NAME +
            '-' +
            this.remoteConfig.configuration.components.getNewComponentId(ComponentTypes.BlockNode));
    }
    async add(argv) {
        // eslint-disable-next-line @typescript-eslint/typedef,unicorn/no-this-assignment
        const self = this;
        let lease;
        const tasks = new Listr([
            {
                title: 'Initialize',
                task: async (context_, task) => {
                    await self.localConfig.load();
                    await self.remoteConfig.loadAndValidate(argv);
                    lease = await self.leaseManager.create();
                    this.configManager.update(argv);
                    flags.disablePrompts(BlockNodeCommand_1.ADD_FLAGS_LIST.optional);
                    const allFlags = [
                        ...BlockNodeCommand_1.ADD_FLAGS_LIST.required,
                        ...BlockNodeCommand_1.ADD_FLAGS_LIST.optional,
                    ];
                    await this.configManager.executePrompt(task, allFlags);
                    const config = this.configManager.getConfig(BlockNodeCommand_1.ADD_CONFIGS_NAME, allFlags);
                    context_.config = config;
                    if (lt(new SemVer(config.releaseTag), new SemVer(versions.MINIMUM_HIERO_PLATFORM_VERSION_FOR_BLOCK_NODE_LEGACY_RELEASE))) {
                        throw new SoloError(`Hedera platform versions less than ${versions.MINIMUM_HIERO_PLATFORM_VERSION_FOR_BLOCK_NODE_LEGACY_RELEASE} are not supported`);
                    }
                    const currentBlockNodeVersion = new SemVer(config.chartVersion);
                    if (lt(new SemVer(config.releaseTag), new SemVer(versions.MINIMUM_HIERO_PLATFORM_VERSION_FOR_BLOCK_NODE)) &&
                        gte(currentBlockNodeVersion, MINIMUM_HIERO_BLOCK_NODE_VERSION_FOR_NEW_LIVENESS_CHECK_PORT)) {
                        throw new SoloError(`Hedera platform version less than ${versions.MINIMUM_HIERO_PLATFORM_VERSION_FOR_BLOCK_NODE} ` +
                            `are not supported for block node version ${MINIMUM_HIERO_BLOCK_NODE_VERSION_FOR_NEW_LIVENESS_CHECK_PORT.version}`);
                    }
                    config.namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
                    config.nodeAliases = this.remoteConfig.getConsensusNodes().map((node) => node.name);
                    if (!config.clusterRef) {
                        config.clusterRef = this.k8Factory.default().clusters().readCurrent();
                    }
                    config.context = this.remoteConfig.getClusterRefs()[config.clusterRef];
                    config.chartVersion = Version.getValidSemanticVersion(config.chartVersion, false, 'Block node chart version');
                    config.livenessCheckPort = this.getLivenessCheckPortNumber(config.chartVersion, config.imageTag);
                    return ListrLock.newAcquireLockTask(lease, task);
                },
            },
            {
                title: 'Prepare release name',
                task: async ({ config }) => {
                    config.releaseName = this.getNextReleaseName();
                },
            },
            {
                title: 'Prepare chart values',
                task: async ({ config }) => {
                    config.valuesArg = await this.prepareValuesArgForBlockNode(config);
                },
            },
            {
                title: 'Deploy block node',
                task: async ({ config }, task) => {
                    await this.chartManager.install(config.namespace, config.releaseName, constants.BLOCK_NODE_CHART, constants.BLOCK_NODE_CHART_URL, config.chartVersion, config.valuesArg, config.context);
                    if (config.imageTag) {
                        // update config map with new VERSION info since
                        // it will be used as a critical environment variable by block node
                        const blockNodeStateSchema = this.componentFactory.createNewBlockNodeComponent(config.clusterRef, config.namespace);
                        const blockNodeId = blockNodeStateSchema.metadata.id;
                        const k8 = this.k8Factory.getK8(config.context);
                        await k8.configMaps().update(config.namespace, `block-node-${blockNodeId}-config`, {
                            VERSION: config.imageTag,
                        });
                        task.title += ` with local built image (${config.imageTag})`;
                    }
                    showVersionBanner(this.logger, config.releaseName, versions.BLOCK_NODE_VERSION);
                    await this.updateBlockNodeVersionInRemoteConfig(config);
                },
            },
            {
                title: 'Check block node pod is running',
                task: async ({ config }) => {
                    await this.k8Factory
                        .getK8(config.context)
                        .pods()
                        .waitForRunningPhase(config.namespace, [`app.kubernetes.io/instance=${config.releaseName}`], constants.BLOCK_NODE_PODS_RUNNING_MAX_ATTEMPTS, constants.BLOCK_NODE_PODS_RUNNING_DELAY);
                },
            },
            {
                title: 'Check software',
                task: async ({ config }) => {
                    const labels = [`app.kubernetes.io/instance=${config.releaseName}`];
                    const blockNodePods = await this.k8Factory
                        .getK8(config.context)
                        .pods()
                        .list(config.namespace, labels);
                    if (blockNodePods.length === 0) {
                        throw new SoloError('Failed to list block node pod');
                    }
                },
            },
            {
                title: 'Check block node pod is ready',
                task: async ({ config }) => {
                    try {
                        await this.k8Factory
                            .getK8(config.context)
                            .pods()
                            .waitForReadyStatus(config.namespace, [`app.kubernetes.io/instance=${config.releaseName}`], constants.BLOCK_NODE_PODS_RUNNING_MAX_ATTEMPTS, constants.BLOCK_NODE_PODS_RUNNING_DELAY);
                    }
                    catch (error) {
                        throw new SoloError(`Block node ${config.releaseName} is not ready: ${error.message}`, error);
                    }
                },
            },
            this.checkBlockNodeReadiness(),
            this.addBlockNodeComponent(),
        ], {
            concurrent: false,
            rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION,
        });
        try {
            await tasks.run();
        }
        catch (error) {
            throw new SoloError(`Error deploying block node: ${error.message}`, error);
        }
        finally {
            await lease.release();
        }
        return true;
    }
    async destroy(argv) {
        // eslint-disable-next-line @typescript-eslint/typedef,unicorn/no-this-assignment
        const self = this;
        let lease;
        const tasks = new Listr([
            {
                title: 'Initialize',
                task: async (context_, task) => {
                    await self.localConfig.load();
                    await self.remoteConfig.loadAndValidate(argv);
                    lease = await self.leaseManager.create();
                    this.configManager.update(argv);
                    flags.disablePrompts(BlockNodeCommand_1.DESTROY_FLAGS_LIST.optional);
                    const allFlags = [
                        ...BlockNodeCommand_1.DESTROY_FLAGS_LIST.required,
                        ...BlockNodeCommand_1.DESTROY_FLAGS_LIST.optional,
                    ];
                    await this.configManager.executePrompt(task, allFlags);
                    const config = this.configManager.getConfig(BlockNodeCommand_1.DESTROY_CONFIGS_NAME, allFlags);
                    context_.config = config;
                    config.namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
                    if (!config.clusterRef) {
                        config.clusterRef = this.k8Factory.default().clusters().readCurrent();
                    }
                    config.context = this.remoteConfig.getClusterRefs()[config.clusterRef];
                    const existingBlockNodeComponents = this.remoteConfig.configuration.components.getComponentsByClusterReference(ComponentTypes.BlockNode, config.clusterRef);
                    // check if any of the block node components are installed
                    for (const blockNodeComponent of existingBlockNodeComponents) {
                        const releaseName = constants.BLOCK_NODE_RELEASE_NAME + '-' + blockNodeComponent.metadata.id;
                        const installed = await this.chartManager.isChartInstalled(config.namespace, releaseName, config.context);
                        if (installed) {
                            config.isChartInstalled = installed;
                            config.releaseName = releaseName;
                            break;
                        }
                    }
                    return ListrLock.newAcquireLockTask(lease, task);
                },
            },
            {
                title: 'Look-up block node',
                task: async ({ config }) => {
                    try {
                        // TODO: Add support for multiple block nodes
                        this.remoteConfig.configuration.components.getComponent(ComponentTypes.BlockNode, 0);
                    }
                    catch (error) {
                        throw new SoloError(`Block node ${config.releaseName} was not found`, error);
                    }
                },
            },
            {
                title: 'Destroy block node',
                task: async ({ config }) => {
                    await this.chartManager.uninstall(config.namespace, config.releaseName, config.context);
                },
                skip: ({ config }) => !config.isChartInstalled,
            },
            this.removeBlockNodeComponent(),
        ], {
            concurrent: false,
            rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION,
        });
        try {
            await tasks.run();
        }
        catch (error) {
            throw new SoloError(`Error destroying block node: ${error.message}`, error);
        }
        finally {
            await lease.release();
        }
        return true;
    }
    async upgrade(argv) {
        let lease;
        const tasks = new Listr([
            {
                title: 'Initialize',
                task: async (context_, task) => {
                    await this.localConfig.load();
                    await this.remoteConfig.loadAndValidate(argv);
                    lease = await this.leaseManager.create();
                    this.configManager.update(argv);
                    flags.disablePrompts(BlockNodeCommand_1.UPGRADE_FLAGS_LIST.optional);
                    const allFlags = [
                        ...BlockNodeCommand_1.UPGRADE_FLAGS_LIST.required,
                        ...BlockNodeCommand_1.UPGRADE_FLAGS_LIST.optional,
                    ];
                    await this.configManager.executePrompt(task, allFlags);
                    const config = this.configManager.getConfig(BlockNodeCommand_1.UPGRADE_CONFIGS_NAME, allFlags);
                    context_.config = config;
                    config.namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
                    if (!config.clusterRef) {
                        config.clusterRef = this.k8Factory.default().clusters().readCurrent();
                    }
                    config.releaseName = `${constants.BLOCK_NODE_RELEASE_NAME}-0`;
                    config.context = this.remoteConfig.getClusterRefs()[config.clusterRef];
                    if (!config.upgradeVersion) {
                        config.upgradeVersion = versions.BLOCK_NODE_VERSION;
                    }
                    return ListrLock.newAcquireLockTask(lease, task);
                },
            },
            {
                title: 'Look-up block node',
                task: async ({ config }) => {
                    try {
                        // TODO: Add support for multiple block nodes
                        this.remoteConfig.configuration.components.getComponent(ComponentTypes.BlockNode, 0);
                    }
                    catch (error) {
                        throw new SoloError(`Block node ${config.releaseName} was not found`, error);
                    }
                },
            },
            {
                title: 'Update block node chart',
                task: async ({ config }) => {
                    const { namespace, releaseName, context, upgradeVersion } = config;
                    const validatedUpgradeVersion = Version.getValidSemanticVersion(upgradeVersion, false, 'Block node chart version');
                    await this.chartManager.upgrade(namespace, releaseName, constants.BLOCK_NODE_CHART, constants.BLOCK_NODE_CHART_URL, validatedUpgradeVersion, '', context);
                    showVersionBanner(this.logger, constants.BLOCK_NODE_CHART, config.upgradeVersion);
                    await this.updateBlockNodeVersionInRemoteConfig(config);
                },
            },
        ], {
            concurrent: false,
            rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION,
        });
        try {
            await tasks.run();
        }
        catch (error) {
            throw new SoloError(`Error upgrading block node: ${error.message}`, error);
        }
        finally {
            await lease.release();
        }
        return true;
    }
    /**
     * Gives the port used for liveness check based on the chart version and image tag (if set)
     */
    getLivenessCheckPortNumber(chartVersion, imageTag) {
        let useLegacyPort = false;
        chartVersion = typeof chartVersion === 'string' ? new SemVer(chartVersion) : chartVersion;
        imageTag = typeof imageTag === 'string' && imageTag ? new SemVer(imageTag) : undefined;
        if (lt(chartVersion, versions.MINIMUM_HIERO_BLOCK_NODE_VERSION_FOR_NEW_LIVENESS_CHECK_PORT)) {
            useLegacyPort = true;
        }
        else if (imageTag && lt(imageTag, versions.MINIMUM_HIERO_BLOCK_NODE_VERSION_FOR_NEW_LIVENESS_CHECK_PORT)) {
            useLegacyPort = true;
        }
        return useLegacyPort ? constants.BLOCK_NODE_PORT_LEGACY : constants.BLOCK_NODE_PORT;
    }
    async updateBlockNodeVersionInRemoteConfig(config) {
        let blockNodeVersion;
        let imageTag;
        if (config.hasOwnProperty('upgradeVersion') && config.upgradeVersion) {
            const version = config.upgradeVersion;
            blockNodeVersion = typeof version === 'string' ? new SemVer(version) : version;
        }
        if (config.hasOwnProperty('chartVersion') && config.chartVersion) {
            const version = config.chartVersion;
            blockNodeVersion = typeof version === 'string' ? new SemVer(version) : version;
        }
        if (config.hasOwnProperty('imageTag') && config.imageTag) {
            const tag = config.imageTag;
            imageTag = typeof tag === 'string' ? new SemVer(tag) : tag;
        }
        this.remoteConfig.configuration.versions.blockNodeChart =
            imageTag && lt(blockNodeVersion, imageTag) ? imageTag : blockNodeVersion;
        await this.remoteConfig.persist();
    }
    /** Adds the block node component to remote config. */
    addBlockNodeComponent() {
        return {
            title: 'Add block node component in remote config',
            skip: () => !this.remoteConfig.isLoaded(),
            task: async ({ config }) => {
                const { namespace, clusterRef } = config;
                this.remoteConfig.configuration.components.addNewComponent(this.componentFactory.createNewBlockNodeComponent(clusterRef, namespace), ComponentTypes.BlockNode);
                await this.remoteConfig.persist();
            },
        };
    }
    /** Adds the block node component to remote config. */
    removeBlockNodeComponent() {
        return {
            title: 'Disable block node component in remote config',
            skip: () => !this.remoteConfig.isLoaded(),
            task: async () => {
                // TODO: Add support for multiple block nodes
                this.remoteConfig.configuration.components.removeComponent(0, ComponentTypes.BlockNode);
                await this.remoteConfig.persist();
            },
        };
    }
    displayHealthCheckData(task) {
        const baseTitle = task.title;
        return function (attempt, maxAttempt, color = 'yellow', additionalData = '') {
            task.title = `${baseTitle} - ${chalk[color](`[${attempt}/${maxAttempt}]`)} ${chalk[color](additionalData)}`;
        };
    }
    checkBlockNodeReadiness() {
        return {
            title: 'Check block node readiness',
            task: async ({ config }, task) => {
                const displayHealthcheckCallback = this.displayHealthCheckData(task);
                const blockNodePodReference = await this.k8Factory
                    .getK8(config.context)
                    .pods()
                    .list(config.namespace, [`app.kubernetes.io/instance=${config.releaseName}`])
                    .then((pods) => pods[0].podReference);
                const containerReference = ContainerReference.of(blockNodePodReference, constants.BLOCK_NODE_CONTAINER_NAME);
                const maxAttempts = constants.BLOCK_NODE_ACTIVE_MAX_ATTEMPTS;
                let attempt = 1;
                let success = false;
                displayHealthcheckCallback(attempt, maxAttempts);
                while (attempt < maxAttempts) {
                    try {
                        const response = await helpers.withTimeout(this.k8Factory
                            .getK8(config.context)
                            .containers()
                            .readByRef(containerReference)
                            .execContainer(['bash', '-c', `curl -s http://localhost:${config.livenessCheckPort}/healthz/readyz`]), Duration.ofMillis(constants.BLOCK_NODE_ACTIVE_TIMEOUT), 'Healthcheck timed out');
                        if (response !== 'OK') {
                            throw new SoloError('Bad response status');
                        }
                        success = true;
                        break;
                    }
                    catch {
                        // Guard
                    }
                    attempt++;
                    await sleep(Duration.ofSeconds(constants.BLOCK_NODE_ACTIVE_DELAY));
                    displayHealthcheckCallback(attempt, maxAttempts);
                }
                if (!success) {
                    displayHealthcheckCallback(attempt, maxAttempts, 'red', 'max attempts reached');
                    throw new SoloError('Max attempts reached');
                }
                displayHealthcheckCallback(attempt, maxAttempts, 'green', 'success');
            },
        };
    }
    getCommandDefinition() {
        return new CommandBuilder(BlockNodeCommand_1.COMMAND_NAME, 'Manage block related components in solo network', this.logger)
            .addCommandGroup(new CommandGroup('node', 'Manage block nodes in solo network')
            .addSubcommand(new Subcommand('add', 'Add block node', this, this.add, (y) => {
            flags.setRequiredCommandFlags(y, ...BlockNodeCommand_1.ADD_FLAGS_LIST.required);
            flags.setOptionalCommandFlags(y, ...BlockNodeCommand_1.ADD_FLAGS_LIST.optional);
        }))
            .addSubcommand(new Subcommand('destroy', 'destroy block node', this, this.destroy, (y) => {
            flags.setRequiredCommandFlags(y, ...BlockNodeCommand_1.DESTROY_FLAGS_LIST.required);
            flags.setOptionalCommandFlags(y, ...BlockNodeCommand_1.DESTROY_FLAGS_LIST.optional);
        }))
            .addSubcommand(new Subcommand('upgrade', 'upgrade block node', this, this.upgrade, (y) => {
            flags.setRequiredCommandFlags(y, ...BlockNodeCommand_1.UPGRADE_FLAGS_LIST.required);
            flags.setOptionalCommandFlags(y, ...BlockNodeCommand_1.UPGRADE_FLAGS_LIST.optional);
        })))
            .build();
    }
    async close() { } // no-op
};
BlockNodeCommand = BlockNodeCommand_1 = __decorate([
    injectable(),
    __param(0, inject(InjectTokens.ComponentFactory)),
    __metadata("design:paramtypes", [Object])
], BlockNodeCommand);
export { BlockNodeCommand };
//# sourceMappingURL=block-node.js.map