// SPDX-License-Identifier: Apache-2.0

import {Listr} from 'listr2';
import {SoloError} from '../core/errors/solo-error.js';
import {MissingArgumentError} from '../core/errors/missing-argument-error.js';
import * as helpers from '../core/helpers.js';
import {showVersionBanner} from '../core/helpers.js';
import * as constants from '../core/constants.js';
import {type AccountManager} from '../core/account-manager.js';
import {BaseCommand} from './base.js';
import {Flags as flags} from './flags.js';
import {
  type AnyListrContext,
  type ArgvStruct,
  type NodeAlias,
  type NodeAliases,
  type NodeId,
} from '../types/aliases.js';
import {ListrLock} from '../core/lock/listr-lock.js';
import * as Base64 from 'js-base64';
import {
  type ClusterReferenceName,
  type ComponentId,
  type Context,
  type DeploymentName,
  NamespaceNameAsString,
  type Optional,
  type SoloListr,
  type SoloListrTask,
} from '../types/index.js';
import {inject, injectable} from 'tsyringe-neo';
import {InjectTokens} from '../core/dependency-injection/inject-tokens.js';
import {patchInject} from '../core/dependency-injection/container-helper.js';
import {ComponentTypes} from '../core/config/remote/enumerations/component-types.js';
import {Templates} from '../core/templates.js';
import {NamespaceName} from '../types/namespace/namespace-name.js';
import {Lock} from '../core/lock/lock.js';
import {NodeServiceMapping} from '../types/mappings/node-service-mapping.js';
import {Secret} from '../integration/kube/resources/secret/secret.js';
import {type RelayNodeStateSchema} from '../data/schema/model/remote/state/relay-node-state-schema.js';
import {PodReference} from '../integration/kube/resources/pod/pod-reference.js';
import {Pod} from '../integration/kube/resources/pod/pod.js';
import {SemanticVersion} from '../business/utils/semantic-version.js';
import {assertUpgradeVersionNotOlder} from '../core/upgrade-version-guard.js';
import {type CommandFlag, type CommandFlags} from '../types/flag-types.js';
import {MIRROR_INGRESS_CONTROLLER} from '../core/constants.js';
import {OperatingSystem} from '../business/utils/operating-system.js';
import {ImageReference, type ParsedImageReference} from '../business/utils/image-reference.js';
import {Duration} from '../core/time/duration.js';
import {DeploymentPhase} from '../data/schema/model/remote/deployment-phase.js';
import {optionFromFlag} from './command-helpers.js';

interface RelayDestroyConfigClass {
  chartDirectory: string;
  namespace: NamespaceName;
  deployment: string;
  nodeAliases: NodeAliases;
  releaseName: string;
  isChartInstalled: boolean;
  clusterRef: Optional<ClusterReferenceName>;
  context: Optional<string>;
  id: number;
  isLegacyChartInstalled: boolean;
}

interface RelayDestroyContext {
  config: RelayDestroyConfigClass;
}

interface RelayDeployConfigClass {
  chainId: string;
  chartDirectory: string;
  relayChartDirectory: string;
  namespace: NamespaceName;
  deployment: string;
  nodeAliasesUnparsed: string;
  operatorId: string;
  operatorKey: string;
  relayReleaseTag: string;
  componentImage: string;
  replicaCount: number;
  valuesFile: string;
  isChartInstalled: boolean;
  nodeAliases: NodeAliases;
  releaseName: string;
  valuesArg: string;
  clusterRef: Optional<ClusterReferenceName>;
  domainName: Optional<string>;
  context: Optional<string>;
  newRelayComponent: RelayNodeStateSchema;
  id: ComponentId;
  forcePortForward: Optional<boolean>;
  cacheDir: Optional<string>;
  isLegacyChartInstalled: false;

  // Mirror Node
  mirrorNodeId: ComponentId;
  mirrorNamespace: NamespaceNameAsString;
  mirrorNodeReleaseName: string;
  isMirrorNodeLegacyChartInstalled: boolean;
}

interface RelayDeployContext {
  config: RelayDeployConfigClass;
}

interface RelayUpgradeConfigClass {
  chainId: string;
  chartDirectory: string;
  relayChartDirectory: string;
  namespace: NamespaceName;
  deployment: string;
  nodeAliasesUnparsed: string;
  operatorId: string;
  operatorKey: string;
  relayReleaseTag: string;
  componentImage: string;
  replicaCount: number;
  valuesFile: string;
  isChartInstalled: boolean;
  nodeAliases: NodeAliases;
  releaseName: string;
  valuesArg: string;
  clusterRef: Optional<ClusterReferenceName>;
  domainName: Optional<string>;
  context: Optional<string>;
  id: ComponentId;
  forcePortForward: Optional<boolean>;
  cacheDir: Optional<string>;
  isLegacyChartInstalled: boolean;

  // Mirror Node
  mirrorNodeId: ComponentId;
  mirrorNamespace: NamespaceNameAsString;
  mirrorNodeReleaseName: string;
  isMirrorNodeLegacyChartInstalled: boolean;
}

interface RelayUpgradeContext {
  config: RelayUpgradeConfigClass;
}

interface InferredData {
  id: ComponentId;
  nodeAliases: NodeAliases;
  releaseName: string;
  isChartInstalled: boolean;
  isLegacyChartInstalled: boolean;
}

enum RelayCommandType {
  ADD = 'add',
  UPGRADE = 'upgrade',
  DESTROY = 'destroy',
}

@injectable()
export class RelayCommand extends BaseCommand {
  public constructor(@inject(InjectTokens.AccountManager) private readonly accountManager: AccountManager) {
    super();

    this.accountManager = patchInject(accountManager, InjectTokens.AccountManager, this.constructor.name);
  }

  private static readonly DEPLOY_CONFIGS_NAME: string = 'deployConfigs';

  private static readonly UPGRADE_CONFIGS_NAME: string = 'deployConfigs';

  public static readonly DEPLOY_FLAGS_LIST: CommandFlags = {
    required: [flags.deployment],
    optional: [
      flags.chainId,
      flags.chartDirectory,
      flags.relayChartDirectory,
      flags.clusterRef,
      flags.nodeAliasesUnparsed,
      flags.operatorId,
      flags.operatorKey,
      flags.quiet,
      flags.relayReleaseTag,
      flags.componentImage,
      flags.replicaCount,
      flags.valuesFile,
      flags.domainName,
      flags.forcePortForward,
      flags.externalAddress,
      flags.cacheDir,
      flags.devMode,

      // Mirror Node
      flags.mirrorNodeId,
      flags.mirrorNamespace,
    ],
  };

  public static readonly UPGRADE_FLAGS_LIST: CommandFlags = {
    required: [flags.deployment],
    optional: [
      flags.chainId,
      flags.chartDirectory,
      flags.relayChartDirectory,
      flags.clusterRef,
      flags.nodeAliasesUnparsed,
      flags.operatorId,
      flags.operatorKey,
      flags.quiet,
      flags.relayReleaseTag,
      flags.componentImage,
      flags.replicaCount,
      flags.valuesFile,
      flags.domainName,
      flags.forcePortForward,
      flags.externalAddress,
      flags.cacheDir,
      flags.id,
      flags.devMode,

      // Mirror Node
      flags.mirrorNodeId,
      flags.mirrorNamespace,
    ],
  };

  public static readonly DESTROY_FLAGS_LIST: CommandFlags = {
    required: [flags.deployment],
    optional: [flags.chartDirectory, flags.clusterRef, flags.nodeAliasesUnparsed, flags.quiet, flags.devMode, flags.id],
  };

  private async prepareValuesArgForRelay({
    valuesFile,
    nodeAliases,
    chainId,
    relayReleaseTag,
    componentImage,
    replicaCount,
    operatorId,
    operatorKey,
    namespace,
    domainName,
    context,
    releaseName,
    deployment,
    mirrorNamespace,
  }: RelayDeployConfigClass | RelayUpgradeConfigClass): Promise<string> {
    let valuesArgument: string = '';

    valuesArgument += helpers.prepareValuesFiles(constants.RELAY_VALUES_FILE);
    valuesArgument += ' --install';
    valuesArgument += helpers.populateHelmArguments({nameOverride: releaseName});

    valuesArgument += ' --set ws.enabled=true';
    valuesArgument += ` --set relay.config.MIRROR_NODE_URL=http://${MIRROR_INGRESS_CONTROLLER}-${mirrorNamespace}.${mirrorNamespace}.svc.cluster.local`;
    valuesArgument += ` --set relay.config.MIRROR_NODE_URL_WEB3=http://${MIRROR_INGRESS_CONTROLLER}-${mirrorNamespace}.${mirrorNamespace}.svc.cluster.local`;

    valuesArgument += ` --set ws.config.MIRROR_NODE_URL=http://${MIRROR_INGRESS_CONTROLLER}-${mirrorNamespace}.${mirrorNamespace}.svc.cluster.local`;

    if (chainId) {
      valuesArgument += ` --set relay.config.CHAIN_ID=${chainId}`;
      valuesArgument += ` --set ws.config.CHAIN_ID=${chainId}`;
    }

    if (relayReleaseTag) {
      relayReleaseTag = SemanticVersion.getValidSemanticVersion(relayReleaseTag, false, 'Relay release');
      valuesArgument += ` --set relay.image.tag=${relayReleaseTag}`;
      valuesArgument += ` --set ws.image.tag=${relayReleaseTag}`;
    }

    if (componentImage) {
      const parsedImageReference: ParsedImageReference = ImageReference.parseImageReference(componentImage);
      valuesArgument += ` --set relay.image.registry=${parsedImageReference.registry}`;
      valuesArgument += ` --set ws.image.registry=${parsedImageReference.registry}`;
      valuesArgument += ` --set relay.image.repository=${parsedImageReference.repository}`;
      valuesArgument += ` --set ws.image.repository=${parsedImageReference.repository}`;
      valuesArgument += ` --set relay.image.tag=${parsedImageReference.tag}`;
      valuesArgument += ` --set ws.image.tag=${parsedImageReference.tag}`;
    }

    if (replicaCount) {
      valuesArgument += ` --set relay.replicaCount=${replicaCount}`;
      valuesArgument += ` --set ws.replicaCount=${replicaCount}`;
    }

    const operatorIdUsing: string = operatorId || this.accountManager.getOperatorAccountId(deployment).toString();
    valuesArgument += ` --set relay.config.OPERATOR_ID_MAIN=${operatorIdUsing}`;
    valuesArgument += ` --set ws.config.OPERATOR_ID_MAIN=${operatorIdUsing}`;

    if (operatorKey) {
      // use user provided operatorKey if available
      valuesArgument += ` --set relay.config.OPERATOR_KEY_MAIN=${operatorKey}`;
      valuesArgument += ` --set ws.config.OPERATOR_KEY_MAIN=${operatorKey}`;
    } else {
      try {
        const secrets: Secret[] = await this.k8Factory
          .getK8(context)
          .secrets()
          .list(namespace, [`solo.hedera.com/account-id=${operatorIdUsing}`]);
        if (secrets.length === 0) {
          this.logger.info(`No k8s secret found for operator account id ${operatorIdUsing}, use default one`);
          valuesArgument += ` --set relay.config.OPERATOR_KEY_MAIN=${constants.OPERATOR_KEY}`;
          valuesArgument += ` --set ws.config.OPERATOR_KEY_MAIN=${constants.OPERATOR_KEY}`;
        } else {
          this.logger.info('Using operator key from k8s secret');
          const operatorKeyFromK8: string = Base64.decode(secrets[0].data.privateKey);
          valuesArgument += ` --set relay.config.OPERATOR_KEY_MAIN=${operatorKeyFromK8}`;
          valuesArgument += ` --set ws.config.OPERATOR_KEY_MAIN=${operatorKeyFromK8}`;
        }
      } catch (error) {
        throw new SoloError(`Error getting operator key: ${error.message}`, error);
      }
    }

    if (!nodeAliases) {
      throw new MissingArgumentError('Node IDs must be specified');
    }

    const networkJsonString: string = await this.prepareNetworkJsonString(nodeAliases, namespace, deployment);
    const quotedNetworkJsonString: string = OperatingSystem.isWin32()
      ? `"${networkJsonString.replaceAll('"', String.raw`\"`)}"`
      : `'${networkJsonString}'`;
    valuesArgument += ` --set-literal relay.config.HEDERA_NETWORK=${quotedNetworkJsonString}`;
    valuesArgument += ` --set-literal ws.config.HEDERA_NETWORK=${quotedNetworkJsonString}`;

    if (domainName) {
      valuesArgument += helpers.populateHelmArguments({
        'relay.ingress.enabled': true,
        'relay.ingress.hosts[0].host': domainName,
        'relay.ingress.hosts[0].paths[0].path': '/',
        'relay.ingress.hosts[0].paths[0].pathType': 'ImplementationSpecific',
      });
    }

    if (valuesFile) {
      valuesArgument += helpers.prepareValuesFiles(valuesFile);
    }

    return valuesArgument;
  }

  /**
   * created a JSON string to represent the map between the node keys and their ids
   * output example '{"node-1": "0.0.3", "node-2": "0.004"}'
   */
  private async prepareNetworkJsonString(
    nodeAliases: NodeAliases,
    namespace: NamespaceName,
    deployment: DeploymentName,
  ): Promise<string> {
    if (!nodeAliases) {
      throw new MissingArgumentError('Node IDs must be specified');
    }

    const networkIds: Record<string, string> = {};

    const accountMap: Map<NodeAlias, string> = this.accountManager.getNodeAccountMap(nodeAliases, deployment);
    const networkNodeServicesMap: NodeServiceMapping = await this.accountManager.getNodeServiceMap(
      namespace,
      this.remoteConfig.getClusterRefs(),
      deployment,
    );
    for (const nodeAlias of nodeAliases) {
      const haProxyClusterIp: string = networkNodeServicesMap.get(nodeAlias).haProxyClusterIp;
      const haProxyGrpcPort: string | number = networkNodeServicesMap.get(nodeAlias).haProxyGrpcPort;
      const networkKey: string = `${haProxyClusterIp}:${haProxyGrpcPort}`;
      networkIds[networkKey] = accountMap.get(nodeAlias);
    }

    return JSON.stringify(networkIds);
  }

  private getReleaseName(): string {
    return this.renderReleaseName(
      this.remoteConfig.configuration.components.getNewComponentId(ComponentTypes.RelayNodes),
    );
  }

  private renderReleaseName(id: ComponentId): string {
    if (typeof id !== 'number') {
      throw new SoloError(`Invalid component id: ${id}, type: ${typeof id}`);
    }
    return `${constants.JSON_RPC_RELAY_RELEASE_NAME}-${id}`;
  }

  private prepareLegacyReleaseName(nodeAliases: NodeAliases = []): string {
    let releaseName: string = constants.JSON_RPC_RELAY_RELEASE_NAME;
    for (const nodeAlias of nodeAliases) {
      releaseName += `-${nodeAlias}`;
    }
    return releaseName;
  }

  private checkChartIsInstalledTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Check chart is installed',
      task: async ({config}: RelayDeployContext | RelayUpgradeContext): Promise<void> => {
        config.isChartInstalled = await this.chartManager.isChartInstalled(
          config.namespace,
          config.releaseName,
          config.context,
        );
      },
    };
  }

  private prepareChartValuesTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Prepare chart values',
      task: async ({config}: RelayDeployContext | RelayUpgradeContext): Promise<void> => {
        config.valuesArg = await this.prepareValuesArgForRelay(config);
      },
    };
  }

  private deployJsonRpcRelayTask(commandType: RelayCommandType): SoloListrTask<AnyListrContext> {
    return {
      title: 'Deploy JSON RPC Relay',
      task: async ({config}: RelayDeployContext | RelayUpgradeContext): Promise<void> => {
        await this.chartManager.upgrade(
          config.namespace,
          config.releaseName,
          constants.JSON_RPC_RELAY_CHART,
          config.relayChartDirectory || constants.JSON_RPC_RELAY_CHART,
          config.relayChartDirectory ? '' : config.relayReleaseTag, // pin chart version to match image version
          config.valuesArg,
          config.context,
        );

        showVersionBanner(this.logger, config.releaseName, config.relayReleaseTag);

        // wait for the pod to destroy in case it was an upgrade
        if (commandType === RelayCommandType.UPGRADE) {
          await helpers.sleep(Duration.ofSeconds(40));

          // update relay version in remote config after successful upgrade
          this.remoteConfig.updateComponentVersion(
            ComponentTypes.RelayNodes,
            new SemanticVersion<string>(config.relayReleaseTag),
          );

          await this.remoteConfig.persist();
        }

        // Add component to remote config
        else if (commandType === RelayCommandType.ADD) {
          this.remoteConfig.configuration.components.changeComponentPhase(
            (config as RelayDeployConfigClass).newRelayComponent.metadata.id,
            ComponentTypes.RelayNodes,
            DeploymentPhase.DEPLOYED,
          );

          await this.remoteConfig.persist();
        }
      },
    };
  }

  private checkRelayIsRunningTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Check relay is running',
      task: async ({config}: RelayDeployContext | RelayUpgradeContext): Promise<void> => {
        try {
          await this.k8Factory
            .getK8(config.context)
            .pods()
            .waitForRunningPhase(
              config.namespace,
              Templates.renderRelayLabels(config.id, config.isLegacyChartInstalled ? config.releaseName : undefined),
              constants.RELAY_PODS_RUNNING_MAX_ATTEMPTS,
              constants.RELAY_PODS_RUNNING_DELAY,
            );
        } catch (error) {
          throw new SoloError(`Relay ${config.releaseName} is not running: ${error.message}`, error);
        }
        // reset nodeAlias
        this.configManager.setFlag(flags.nodeAliasesUnparsed, '');
      },
    };
  }

  private checkRelayIsReadyTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Check relay is ready',
      task: async ({config}: RelayDeployContext | RelayUpgradeContext): Promise<void> => {
        try {
          await this.k8Factory
            .getK8(config.context)
            .pods()
            .waitForReadyStatus(
              config.namespace,
              Templates.renderRelayLabels(config.id, config.isLegacyChartInstalled ? config.releaseName : undefined),
              constants.RELAY_PODS_READY_MAX_ATTEMPTS,
              constants.RELAY_PODS_READY_DELAY,
            );
        } catch (error) {
          throw new SoloError(`Relay ${config.releaseName} is not ready: ${error.message}`, error);
        }
      },
    };
  }

  private enablePortForwardingTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Enable port forwarding for relay node',
      skip: ({config}: RelayDeployContext | RelayUpgradeContext): boolean => !config.forcePortForward,
      task: async ({config}: RelayDeployContext | RelayUpgradeContext): Promise<void> => {
        const externalAddress: string = this.configManager.getFlag<string>(flags.externalAddress);
        const pods: Pod[] = await this.k8Factory
          .getK8(config.context)
          .pods()
          .list(
            config.namespace,
            Templates.renderRelayLabels(config.id, config.isLegacyChartInstalled ? config.releaseName : undefined),
          );

        if (pods.length === 0) {
          throw new SoloError('No Relay pod found');
        }

        const podReference: PodReference = pods[0].podReference;
        const clusterReference: string = this.getClusterReference();

        // always kill pre-existing port forward process first otherwise after upgrade
        // the old port forward process tied with old relay pod will still be running and cause issues
        await this.remoteConfig.configuration.components.stopPortForwards(
          clusterReference,
          podReference,
          constants.JSON_RPC_RELAY_PORT, // Pod port
          constants.JSON_RPC_RELAY_LOCAL_PORT, // Local port
          this.k8Factory.getK8(config.context),
          this.logger,
          ComponentTypes.RelayNodes,
          'JSON RPC Relay',
        );
        await this.remoteConfig.persist();

        await this.remoteConfig.configuration.components.managePortForward(
          clusterReference,
          podReference,
          constants.JSON_RPC_RELAY_PORT, // Pod port
          constants.JSON_RPC_RELAY_LOCAL_PORT, // Local port
          this.k8Factory.getK8(config.context),
          this.logger,
          ComponentTypes.RelayNodes,
          'JSON RPC Relay',
          config.isChartInstalled, // Reuse existing port if chart is already installed
          undefined,
          true, // persist: auto-restart on failure using persist-port-forward.js
          externalAddress,
        );
        await this.remoteConfig.persist();
      },
    };
  }

  public async add(argv: ArgvStruct): Promise<boolean> {
    let lease: Lock;

    const tasks: SoloListr<RelayDeployContext> = this.taskList.newTaskList<RelayDeployContext>(
      [
        {
          title: 'Initialize',
          task: async (context_, task): Promise<Listr<AnyListrContext>> => {
            await this.localConfig.load();
            await this.loadRemoteConfigOrWarn(argv);
            if (!this.oneShotState.isActive()) {
              lease = await this.leaseManager.create();
            }
            // reset nodeAlias
            this.configManager.setFlag(flags.nodeAliasesUnparsed, '');

            this.configManager.update(argv);

            flags.disablePrompts(RelayCommand.DEPLOY_FLAGS_LIST.optional);

            const allFlags: CommandFlag[] = [
              ...RelayCommand.DEPLOY_FLAGS_LIST.required,
              ...RelayCommand.DEPLOY_FLAGS_LIST.optional,
            ];

            await this.configManager.executePrompt(task, allFlags);

            // prompt if inputs are empty and set it in the context
            const config: RelayDeployConfigClass = this.configManager.getConfig(
              RelayCommand.DEPLOY_CONFIGS_NAME,
              allFlags,
              ['nodeAliases'],
            ) as RelayDeployConfigClass;

            context_.config = config;

            config.isLegacyChartInstalled = false;

            config.namespace = await this.getNamespace(task);

            config.nodeAliases = helpers.parseNodeAliases(
              config.nodeAliasesUnparsed,
              this.remoteConfig.getConsensusNodes(),
              this.configManager,
            );

            config.clusterRef = this.getClusterReference();
            config.context = this.getClusterContext(config.clusterRef);
            config.releaseName = this.getReleaseName();

            const nodeIds: NodeId[] = config.nodeAliases.map((nodeAlias: NodeAlias): number =>
              Templates.nodeIdFromNodeAlias(nodeAlias),
            );

            const {mirrorNodeId, mirrorNamespace, mirrorNodeReleaseName} = await this.inferMirrorNodeData(
              config.namespace,
              config.context,
            );

            config.mirrorNodeId = mirrorNodeId;
            config.mirrorNamespace = mirrorNamespace;
            config.mirrorNodeReleaseName = mirrorNodeReleaseName;

            config.newRelayComponent = this.componentFactory.createNewRelayComponent(
              config.clusterRef,
              config.namespace,
              nodeIds,
            );

            config.newRelayComponent.metadata.phase = DeploymentPhase.REQUESTED;

            config.id = config.newRelayComponent.metadata.id;

            if (!this.oneShotState.isActive()) {
              return ListrLock.newAcquireLockTask(lease, task);
            }
            return ListrLock.newSkippedLockTask(task);
          },
        },
        this.addRelayComponent(),
        this.checkChartIsInstalledTask(),
        this.prepareChartValuesTask(),
        this.deployJsonRpcRelayTask(RelayCommandType.ADD),
        this.checkRelayIsRunningTask(),
        this.checkRelayIsReadyTask(),
        this.enablePortForwardingTask(),
        {
          title: 'Show user messages',
          skip: (): boolean => this.oneShotState.isActive(),
          task: (): void => {
            this.logger.showAllMessageGroups();
          },
        },
      ],
      constants.LISTR_DEFAULT_OPTIONS.DEFAULT,
      undefined,
      'relay node add',
    );

    if (tasks.isRoot()) {
      try {
        await tasks.run();
      } catch (error) {
        throw new SoloError(`Error deploying relay: ${error.message}`, error);
      } finally {
        if (lease && !this.oneShotState.isActive()) {
          await lease.release();
        }
      }
    } else {
      this.taskList.registerCloseFunction(async (): Promise<void> => {
        if (!this.oneShotState.isActive()) {
          await lease?.release();
        }
      });
    }

    return true;
  }

  public async upgrade(argv: ArgvStruct): Promise<boolean> {
    let lease: Lock;

    const tasks: SoloListr<RelayUpgradeContext> = this.taskList.newTaskList<RelayUpgradeContext>(
      [
        {
          title: 'Initialize',
          task: async (context_, task): Promise<Listr<AnyListrContext>> => {
            await this.localConfig.load();
            await this.remoteConfig.loadAndValidate(argv);
            if (!this.oneShotState.isActive()) {
              lease = await this.leaseManager.create();
            }
            // reset nodeAlias
            this.configManager.setFlag(flags.nodeAliasesUnparsed, '');

            this.configManager.update(argv);

            flags.disablePrompts(RelayCommand.UPGRADE_FLAGS_LIST.optional);

            const allFlags: CommandFlag[] = [
              ...RelayCommand.UPGRADE_FLAGS_LIST.required,
              ...RelayCommand.UPGRADE_FLAGS_LIST.optional,
            ];

            await this.configManager.executePrompt(task, allFlags);

            // prompt if inputs are empty and set it in the context
            const config: RelayUpgradeConfigClass = this.configManager.getConfig(
              RelayCommand.UPGRADE_CONFIGS_NAME,
              allFlags,
              [],
            ) as RelayUpgradeConfigClass;

            context_.config = config;

            config.namespace = await this.getNamespace(task);

            config.nodeAliases = helpers.parseNodeAliases(
              config.nodeAliasesUnparsed,
              this.remoteConfig.getConsensusNodes(),
              this.configManager,
            );

            config.clusterRef = this.getClusterReference();
            config.context = this.getClusterContext(config.clusterRef);

            const {id, isLegacyChartInstalled, isChartInstalled, releaseName, nodeAliases} = await this.inferRelayData(
              config.namespace,
              config.context,
            );

            config.id = id;
            config.isLegacyChartInstalled = isLegacyChartInstalled;
            config.isChartInstalled = isChartInstalled;
            config.releaseName = releaseName;
            config.nodeAliases = nodeAliases;

            const {mirrorNodeId, mirrorNamespace, mirrorNodeReleaseName} = await this.inferMirrorNodeData(
              config.namespace,
              config.context,
            );

            config.mirrorNodeId = mirrorNodeId;
            config.mirrorNamespace = mirrorNamespace;
            config.mirrorNodeReleaseName = mirrorNodeReleaseName;

            assertUpgradeVersionNotOlder(
              'Relay',
              config.relayReleaseTag,
              this.remoteConfig.getComponentVersion(ComponentTypes.RelayNodes),
              optionFromFlag(flags.relayReleaseTag),
            );

            if (!this.oneShotState.isActive()) {
              return ListrLock.newAcquireLockTask(lease, task);
            }
            return ListrLock.newSkippedLockTask(task);
          },
        },
        this.prepareChartValuesTask(),
        this.deployJsonRpcRelayTask(RelayCommandType.UPGRADE),
        this.checkRelayIsRunningTask(),
        this.checkRelayIsReadyTask(),
        this.enablePortForwardingTask(),
      ],
      constants.LISTR_DEFAULT_OPTIONS.DEFAULT,
      undefined,
      'relay node upgrade',
    );

    if (tasks.isRoot()) {
      try {
        await tasks.run();
      } catch (error) {
        throw new SoloError(`Error upgrading relay: ${error.message}`, error);
      } finally {
        if (!this.oneShotState.isActive()) {
          await lease?.release();
        }
      }
    } else {
      this.taskList.registerCloseFunction(async (): Promise<void> => {
        if (!this.oneShotState.isActive()) {
          await lease?.release();
        }
      });
    }

    return true;
  }

  public async destroy(argv: ArgvStruct): Promise<boolean> {
    let lease: Lock;

    const tasks: SoloListr<RelayDestroyContext> = this.taskList.newTaskList<RelayDestroyContext>(
      [
        {
          title: 'Initialize',
          task: async (context_, task): Promise<Listr<AnyListrContext>> => {
            await this.localConfig.load();
            await this.remoteConfig.loadAndValidate(argv);
            if (!this.oneShotState.isActive()) {
              lease = await this.leaseManager.create();
            }
            // reset nodeAlias
            this.configManager.setFlag(flags.nodeAliasesUnparsed, '');
            this.configManager.update(argv);

            flags.disablePrompts([flags.clusterRef, flags.id, flags.nodeAliasesUnparsed]);

            const allFlags: CommandFlag[] = [
              ...RelayCommand.DESTROY_FLAGS_LIST.required,
              ...RelayCommand.DESTROY_FLAGS_LIST.optional,
            ];

            await this.configManager.executePrompt(task, allFlags);

            const clusterReference: ClusterReferenceName = this.getClusterReference();
            const context: Context = this.getClusterContext(clusterReference);
            const namespace: NamespaceName = await this.getNamespace(task);

            const {id, isLegacyChartInstalled, isChartInstalled, releaseName, nodeAliases} = await this.inferRelayData(
              namespace,
              context,
            );

            const config: RelayDestroyConfigClass = {
              chartDirectory: this.configManager.getFlag(flags.chartDirectory),
              namespace,
              nodeAliases,
              clusterRef: clusterReference,
              id,
              isLegacyChartInstalled,
              isChartInstalled,
              releaseName,
              deployment: this.configManager.getFlag<DeploymentName>(flags.deployment),
              context,
            };

            context_.config = config;

            if (!this.oneShotState.isActive()) {
              return ListrLock.newAcquireLockTask(lease, task);
            }
            return ListrLock.newSkippedLockTask(task);
          },
        },
        {
          title: 'Destroy JSON RPC Relay',
          task: async ({config}): Promise<void> => {
            await this.chartManager.uninstall(config.namespace, config.releaseName, config.context);

            this.logger.showList(
              'Destroyed Relays',
              await this.chartManager.getInstalledCharts(config.namespace, config.context),
            );

            // reset nodeAliasesUnparsed
            this.configManager.setFlag(flags.nodeAliasesUnparsed, '');
          },
          skip: (context_): boolean => !context_.config.isChartInstalled,
        },
        this.disableRelayComponent(),
      ],
      constants.LISTR_DEFAULT_OPTIONS.DEFAULT,
      undefined,
      'relay node destroy',
    );

    if (tasks.isRoot()) {
      try {
        await tasks.run();
      } catch (error) {
        throw new SoloError('Error uninstalling relays', error);
      } finally {
        if (!this.oneShotState.isActive()) {
          await lease?.release();
        }
      }
    } else {
      this.taskList.registerCloseFunction(async (): Promise<void> => {
        if (!this.oneShotState.isActive()) {
          await lease?.release();
        }
      });
    }

    return true;
  }

  /** Adds the relay component to remote config. */
  public addRelayComponent(): SoloListrTask<RelayDeployContext> {
    return {
      title: 'Add relay component in remote config',
      skip: ({config}): boolean =>
        !this.remoteConfig.isLoaded() || config.isChartInstalled || this.oneShotState.isActive(),
      task: async ({config}): Promise<void> => {
        this.remoteConfig.configuration.components.addNewComponent(config.newRelayComponent, ComponentTypes.RelayNodes);

        // save relay version in remote config
        this.remoteConfig.updateComponentVersion(
          ComponentTypes.RelayNodes,
          new SemanticVersion<string>(config.relayReleaseTag),
        );

        await this.remoteConfig.persist();
      },
    };
  }

  /** Remove the relay component from remote config. */
  public disableRelayComponent(): SoloListrTask<RelayDestroyContext> {
    return {
      title: 'Remove relay component from remote config',
      skip: (): boolean => !this.remoteConfig.isLoaded(),
      task: async ({config}): Promise<void> => {
        this.remoteConfig.configuration.components.removeComponent(config.id, ComponentTypes.RelayNodes);

        await this.remoteConfig.persist();
      },
    };
  }

  public async close(): Promise<void> {} // no-op

  private async checkIfLegacyChartIsInstalled(
    id: ComponentId,
    namespace: NamespaceName,
    context: Context,
    nodeAliases: NodeAliases,
  ): Promise<boolean> {
    return id <= 1
      ? await this.chartManager.isChartInstalled(namespace, this.prepareLegacyReleaseName(nodeAliases), context)
      : false;
  }

  private inferRelayId(): ComponentId {
    const id: ComponentId = this.configManager.getFlag(flags.id);
    if (typeof id === 'number') {
      return id;
    }

    if (this.remoteConfig.configuration.components.state.relayNodes.length === 0) {
      throw new SoloError('Relay node not found in remote config');
    }

    return this.remoteConfig.configuration.components.state.relayNodes[0].metadata.id;
  }

  private async inferRelayData(namespace: NamespaceName, context: Context): Promise<InferredData> {
    const id: ComponentId = this.inferRelayId();

    const nodeAliases: NodeAliases = helpers.parseNodeAliases(
      this.configManager.getFlag(flags.nodeAliasesUnparsed),
      this.remoteConfig.getConsensusNodes(),
      this.configManager,
    );

    const isLegacyChartInstalled: boolean = await this.checkIfLegacyChartIsInstalled(
      id,
      namespace,
      context,
      nodeAliases,
    );

    if (isLegacyChartInstalled) {
      return {
        id,
        nodeAliases,
        releaseName: this.prepareLegacyReleaseName(nodeAliases),
        isChartInstalled: true,
        isLegacyChartInstalled,
      };
    }

    const releaseName: string = this.renderReleaseName(id);
    return {
      id,
      nodeAliases,
      releaseName,
      isChartInstalled: await this.chartManager.isChartInstalled(namespace, releaseName, context),
      isLegacyChartInstalled,
    };
  }
}
