// SPDX-License-Identifier: Apache-2.0

import {Listr} from 'listr2';
import {ListrInquirerPromptAdapter} from '@listr2/prompt-adapter-inquirer';
import {confirm as confirmPrompt} from '@inquirer/prompts';
import {SoloError} from '../core/errors/solo-error.js';
import {UserBreak} from '../core/errors/user-break.js';
import * as constants from '../core/constants.js';
import {BaseCommand} from './base.js';
import {Flags as flags} from './flags.js';
import {type AnyListrContext, type ArgvStruct} from '../types/aliases.js';
import {ListrLock} from '../core/lock/listr-lock.js';
import * as helpers from '../core/helpers.js';
import {prepareValuesFiles, showVersionBanner, sleep} from '../core/helpers.js';
import {
  type ClusterReferenceName,
  type ComponentId,
  type Context,
  NamespaceNameAsString,
  type Optional,
  type SoloListr,
  type SoloListrTask,
} from '../types/index.js';
import {NamespaceName} from '../types/namespace/namespace-name.js';
import {type ClusterChecks} from '../core/cluster-checks.js';
import {inject, injectable} from 'tsyringe-neo';
import {InjectTokens} from '../core/dependency-injection/inject-tokens.js';
import {KeyManager} from '../core/key-manager.js';
import {INGRESS_CONTROLLER_VERSION} from '../../version.js';
import {patchInject} from '../core/dependency-injection/container-helper.js';
import {ComponentTypes} from '../core/config/remote/enumerations/component-types.js';
import {Lock} from '../core/lock/lock.js';
import {IngressClass} from '../integration/kube/resources/ingress-class/ingress-class.js';
import {CommandFlag, CommandFlags} from '../types/flag-types.js';
import {Templates} from '../core/templates.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 {Duration} from '../core/time/duration.js';
import {ExplorerStateSchema} from '../data/schema/model/remote/state/explorer-state-schema.js';
import {K8} from '../integration/kube/k8.js';
import {createHash} from 'node:crypto';
import {DeploymentPhase} from '../data/schema/model/remote/deployment-phase.js';
import {optionFromFlag} from './command-helpers.js';

interface ExplorerDeployConfigClass {
  cacheDir: string;
  chartDirectory: string;
  explorerChartDirectory: string;
  clusterRef: ClusterReferenceName;
  clusterContext: string;
  enableIngress: boolean;
  enableExplorerTls: boolean;
  ingressControllerValueFile: string;
  explorerTlsHostName: string;
  explorerStaticIp: string | '';
  explorerVersion: string;
  namespace: NamespaceName;
  tlsClusterIssuerType: string;
  valuesFile: string;
  valuesArg: string;
  clusterSetupNamespace: NamespaceName;
  getUnusedConfigs: () => string[];
  soloChartVersion: string;
  domainName: Optional<string>;
  releaseName: string;
  ingressReleaseName: string;
  newExplorerComponent: ExplorerStateSchema;
  id: ComponentId;
  forcePortForward: Optional<boolean>;
  isChartInstalled: boolean;
  isLegacyChartInstalled: false;

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

interface ExplorerDeployContext {
  config: ExplorerDeployConfigClass;
  addressBook: string;
}

interface ExplorerUpgradeConfigClass {
  cacheDir: string;
  chartDirectory: string;
  explorerChartDirectory: string;
  clusterRef: ClusterReferenceName;
  clusterContext: string;
  enableIngress: boolean;
  enableExplorerTls: boolean;
  ingressControllerValueFile: string;
  explorerTlsHostName: string;
  explorerStaticIp: string | '';
  explorerVersion: string;
  namespace: NamespaceName;
  tlsClusterIssuerType: string;
  valuesFile: string;
  valuesArg: string;
  clusterSetupNamespace: NamespaceName;
  getUnusedConfigs: () => string[];
  soloChartVersion: string;
  domainName: Optional<string>;
  releaseName: string;
  ingressReleaseName: string;
  forcePortForward: Optional<boolean>;
  id: ComponentId;
  isChartInstalled: boolean;
  isLegacyChartInstalled: boolean;

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

interface ExplorerUpgradeContext {
  config: ExplorerUpgradeConfigClass;
  addressBook: string;
}

interface ExplorerDestroyContext {
  config: {
    clusterContext: string;
    clusterReference: ClusterReferenceName;
    namespace: NamespaceName;
    isChartInstalled: boolean;
    id: ComponentId;
    releaseName: string;
    ingressReleaseName: string;
    isLegacyChartInstalled: boolean;
  };
}

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

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

@injectable()
export class ExplorerCommand extends BaseCommand {
  public constructor(@inject(InjectTokens.ClusterChecks) private readonly clusterChecks: ClusterChecks) {
    super();

    this.clusterChecks = patchInject(clusterChecks, InjectTokens.ClusterChecks, this.constructor.name);
  }

  private static readonly DEPLOY_CONFIGS_NAME: string = 'deployConfigs';

  private static readonly UPGRADE_CONFIGS_NAME: string = 'upgradeConfigs';

  public static readonly DEPLOY_FLAGS_LIST: CommandFlags = {
    required: [flags.deployment],
    optional: [
      flags.cacheDir,
      flags.chartDirectory,
      flags.explorerChartDirectory,
      flags.clusterRef,
      flags.enableIngress,
      flags.ingressControllerValueFile,
      flags.enableExplorerTls,
      flags.explorerTlsHostName,
      flags.explorerStaticIp,
      flags.explorerVersion,
      flags.namespace,
      flags.quiet,
      flags.soloChartVersion,
      flags.tlsClusterIssuerType,
      flags.valuesFile,
      flags.clusterSetupNamespace,
      flags.domainName,
      flags.forcePortForward,
      flags.externalAddress,

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

  public static readonly UPGRADE_FLAGS_LIST: CommandFlags = {
    required: [flags.deployment],
    optional: [
      flags.clusterRef,
      flags.cacheDir,
      flags.chartDirectory,
      flags.explorerChartDirectory,
      flags.enableIngress,
      flags.ingressControllerValueFile,
      flags.enableExplorerTls,
      flags.explorerTlsHostName,
      flags.explorerStaticIp,
      flags.explorerVersion,
      flags.namespace,
      flags.quiet,
      flags.soloChartVersion,
      flags.tlsClusterIssuerType,
      flags.valuesFile,
      flags.clusterSetupNamespace,
      flags.domainName,
      flags.forcePortForward,
      flags.externalAddress,
      flags.id,

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

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

  private async prepareHederaExplorerValuesArg(
    config: ExplorerDeployConfigClass | ExplorerUpgradeConfigClass,
  ): Promise<string> {
    let valuesArgument: string = '';

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

    if (config.enableIngress) {
      valuesArgument += ' --set ingress.enabled=true';
      valuesArgument += ` --set ingressClassName=${config.ingressReleaseName}`;
    }
    valuesArgument += ` --set fullnameOverride=${config.releaseName}-${config.namespace.name}`;

    valuesArgument += ` --set proxyPass./api="http://${config.mirrorNodeReleaseName}-rest.${config.mirrorNamespace}.svc.cluster.local" `;

    if (config.domainName) {
      valuesArgument += helpers.populateHelmArguments({
        'ingress.enabled': true,
        'ingress.hosts[0].host': config.domainName,
      });

      if (config.tlsClusterIssuerType === 'self-signed') {
        // Create TLS secret for Explorer
        await KeyManager.createTlsSecret(
          this.k8Factory,
          config.namespace,
          config.domainName,
          config.cacheDir,
          constants.EXPLORER_INGRESS_TLS_SECRET_NAME,
        );

        if (config.enableIngress) {
          valuesArgument += ` --set ingress.tls[0].hosts[0]=${config.domainName}`;
        }
      }
    }
    return valuesArgument;
  }

  private async prepareCertManagerChartValuesArg(
    config: ExplorerDeployConfigClass | ExplorerUpgradeConfigClass,
  ): Promise<string> {
    const {tlsClusterIssuerType, namespace} = config;

    let valuesArgument: string = ' --install ';

    if (!['acme-staging', 'acme-prod', 'self-signed'].includes(tlsClusterIssuerType)) {
      throw new Error(
        `Invalid TLS cluster issuer type: ${tlsClusterIssuerType}, must be one of: "acme-staging", "acme-prod", or "self-signed"`,
      );
    }

    if (!(await this.clusterChecks.isCertManagerInstalled())) {
      valuesArgument += ' --set cert-manager.installCRDs=true';
    }

    if (tlsClusterIssuerType === 'self-signed') {
      valuesArgument += ' --set selfSignedClusterIssuer.enabled=true';
    } else {
      valuesArgument += ` --set global.explorerNamespace=${namespace}`;
      valuesArgument += ' --set acmeClusterIssuer.enabled=true';
      valuesArgument += ` --set certClusterIssuerType=${tlsClusterIssuerType}`;
    }
    if (config.valuesFile) {
      valuesArgument += prepareValuesFiles(config.valuesFile);
    }
    return valuesArgument;
  }

  private async prepareValuesArg(config: ExplorerDeployConfigClass | ExplorerUpgradeConfigClass): Promise<string> {
    let valuesArgument: string = '';
    if (config.valuesFile) {
      valuesArgument += prepareValuesFiles(config.valuesFile);
    }
    return valuesArgument;
  }

  private installCertManagerTask(commandType: ExplorerCommandType): SoloListrTask<AnyListrContext> {
    return {
      title: 'Install cert manager',
      skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.enableExplorerTls,
      task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => {
        config.soloChartVersion = SemanticVersion.getValidSemanticVersion(
          config.soloChartVersion,
          false,
          'Solo chart version',
        );

        const {soloChartVersion} = config;

        const soloCertManagerValuesArgument: string = await this.prepareCertManagerChartValuesArg(config);
        // check if CRDs of cert-manager are already installed
        let needInstall: boolean = false;
        for (const crd of constants.CERT_MANAGER_CRDS) {
          const crdExists: boolean = await this.k8Factory.getK8(config.clusterContext).crds().ifExists(crd);

          if (!crdExists) {
            needInstall = true;
            break;
          }
        }

        if (needInstall) {
          // if cert-manager isn't already installed we want to install it separate from the certificate issuers
          // as they will fail to be created due to the order of the installation being dependent on the cert-manager
          // being installed first
          await this.chartManager.upgrade(
            NamespaceName.of(constants.CERT_MANAGER_NAME_SPACE),
            constants.SOLO_CERT_MANAGER_CHART,
            constants.SOLO_CERT_MANAGER_CHART,
            config.chartDirectory || constants.SOLO_TESTING_CHART_URL,
            soloChartVersion,
            ' --install --create-namespace --set cert-manager.installCRDs=true',
            config.clusterContext,
            commandType !== ExplorerCommandType.ADD,
          );
          showVersionBanner(this.logger, constants.SOLO_CERT_MANAGER_CHART, soloChartVersion);
        }

        // wait cert-manager to be ready to proceed, otherwise may get error of "failed calling webhook"
        await this.k8Factory
          .getK8(config.clusterContext)
          .pods()
          .waitForReadyStatus(
            constants.DEFAULT_CERT_MANAGER_NAMESPACE,
            ['app.kubernetes.io/component=webhook', `app.kubernetes.io/instance=${constants.SOLO_CERT_MANAGER_CHART}`],
            constants.PODS_READY_MAX_ATTEMPTS,
            constants.PODS_READY_DELAY,
          );

        // sleep for a few seconds to allow cert-manager to be ready
        if (commandType === ExplorerCommandType.UPGRADE) {
          await sleep(Duration.ofSeconds(10));
        }

        await this.chartManager.upgrade(
          NamespaceName.of(constants.CERT_MANAGER_NAME_SPACE),
          constants.SOLO_CERT_MANAGER_CHART,
          constants.SOLO_CERT_MANAGER_CHART,
          config.chartDirectory || constants.SOLO_TESTING_CHART_URL,
          soloChartVersion,
          soloCertManagerValuesArgument,
          config.clusterContext,
        );
        showVersionBanner(this.logger, constants.SOLO_CERT_MANAGER_CHART, soloChartVersion, 'Upgraded');
      },
    };
  }

  private installExplorerTask(commandType: ExplorerCommandType): SoloListrTask<AnyListrContext> {
    return {
      title: 'Install explorer',
      task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => {
        config.explorerVersion = SemanticVersion.getValidSemanticVersion(
          config.explorerVersion,
          false,
          'Explorer version',
        );

        let exploreValuesArgument: string = ' --install ';
        exploreValuesArgument += prepareValuesFiles(constants.EXPLORER_VALUES_FILE);
        exploreValuesArgument += await this.prepareHederaExplorerValuesArg(config);

        // Local chart checkouts can keep appVersion/tag at placeholder values (for example 0.0.1),
        // so pin the runtime image tag explicitly to the requested explorer version.
        if (config.explorerChartDirectory) {
          exploreValuesArgument += helpers.populateHelmArguments({'image.tag': config.explorerVersion});
        }

        await this.chartManager.upgrade(
          config.namespace,
          config.releaseName,
          '',
          config.explorerChartDirectory || constants.EXPLORER_CHART_URL,
          config.explorerVersion,
          exploreValuesArgument,
          config.clusterContext,
        );

        if (commandType === ExplorerCommandType.ADD) {
          this.remoteConfig.configuration.components.changeComponentPhase(
            (config as ExplorerDeployConfigClass).newExplorerComponent.metadata.id,
            ComponentTypes.Explorer,
            DeploymentPhase.DEPLOYED,
          );

          await this.remoteConfig.persist();
        } else if (commandType === ExplorerCommandType.UPGRADE) {
          // update explorer version in remote config after successful upgrade
          this.remoteConfig.updateComponentVersion(
            ComponentTypes.Explorer,
            new SemanticVersion<string>(config.explorerVersion),
          );

          await this.remoteConfig.persist();
        }

        showVersionBanner(this.logger, config.releaseName, config.explorerVersion);
      },
    };
  }

  private installExplorerIngressControllerTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Install explorer ingress controller',
      skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.enableIngress,
      task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => {
        let explorerIngressControllerValuesArgument: string = ' --install ';

        if (config.explorerStaticIp !== '') {
          explorerIngressControllerValuesArgument += ` --set controller.service.loadBalancerIP=${config.explorerStaticIp}`;
        }
        explorerIngressControllerValuesArgument += ` --set fullnameOverride=${config.ingressReleaseName}`;
        explorerIngressControllerValuesArgument += ` --set controller.ingressClass=${config.ingressReleaseName}`;
        explorerIngressControllerValuesArgument += ` --set controller.extraArgs.controller-class=${config.ingressReleaseName}`;
        if (config.tlsClusterIssuerType === 'self-signed') {
          explorerIngressControllerValuesArgument += prepareValuesFiles(config.ingressControllerValueFile);
        }

        await this.chartManager.upgrade(
          config.namespace,
          config.ingressReleaseName,
          constants.INGRESS_CONTROLLER_RELEASE_NAME,
          constants.INGRESS_CONTROLLER_RELEASE_NAME,
          INGRESS_CONTROLLER_VERSION,
          explorerIngressControllerValuesArgument,
          config.clusterContext,
        );

        showVersionBanner(this.logger, config.ingressReleaseName, INGRESS_CONTROLLER_VERSION);

        const k8: K8 = this.k8Factory.getK8(config.clusterContext);

        // patch explorer ingress to use h1 protocol, haproxy ingress controller default backend protocol is h2
        // to support grpc over http/2
        await k8.ingresses().update(config.namespace, config.releaseName, {
          metadata: {
            annotations: {
              'haproxy-ingress.github.io/backend-protocol': 'h1',
            },
          },
        });

        const ingressClasses: IngressClass[] = await k8.ingressClasses().list();
        if (ingressClasses.some((ingressClass): boolean => ingressClass.name === config.ingressReleaseName)) {
          return;
        }

        await k8
          .ingressClasses()
          .create(config.ingressReleaseName, constants.INGRESS_CONTROLLER_PREFIX + config.ingressReleaseName);
      },
    };
  }

  private checkExplorerPodIsReadyTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Check explorer pod is ready',
      task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => {
        await this.k8Factory
          .getK8(config.clusterContext)
          .pods()
          .waitForReadyStatus(
            config.namespace,
            Templates.renderExplorerLabels(config.id, config.isLegacyChartInstalled ? config.releaseName : undefined),
            constants.PODS_READY_MAX_ATTEMPTS,
            constants.PODS_READY_DELAY,
          );
      },
    };
  }

  private checkExplorerIngressControllerPodIsReadyTask(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Check haproxy ingress controller pod is ready',
      skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.enableIngress,
      task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => {
        await this.k8Factory
          .getK8(config.clusterContext)
          .pods()
          .waitForReadyStatus(
            config.namespace,
            [
              `app.kubernetes.io/name=${constants.INGRESS_CONTROLLER_RELEASE_NAME}`,
              `app.kubernetes.io/instance=${config.ingressReleaseName}`,
            ],
            constants.PODS_READY_MAX_ATTEMPTS,
            constants.PODS_READY_DELAY,
          );
      },
    };
  }

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

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

        const podReference: PodReference = pods[0].podReference;

        await this.remoteConfig.configuration.components.stopPortForwards(
          config.clusterRef,
          podReference,
          constants.EXPLORER_PORT, // Pod port
          constants.EXPLORER_LOCAL_PORT, // Local port
          this.k8Factory.getK8(config.clusterContext),
          this.logger,
          ComponentTypes.Explorer,
          'Explorer',
        );
        await this.remoteConfig.persist();

        await this.remoteConfig.configuration.components.managePortForward(
          config.clusterRef,
          podReference,
          constants.EXPLORER_PORT, // Pod port
          constants.EXPLORER_LOCAL_PORT, // Local port
          this.k8Factory.getK8(config.clusterContext),
          this.logger,
          ComponentTypes.Explorer,
          'Explorer',
          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();
      },
    };
  }

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

  private getIngressReleaseName(namespaceName: NamespaceName): string {
    return this.renderIngressReleaseName(
      this.remoteConfig.configuration.components.getNewComponentId(ComponentTypes.Explorer),
      namespaceName,
    );
  }

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

  private renderIngressReleaseName(id: ComponentId, namespaceName: NamespaceName): string {
    if (typeof id !== 'number') {
      throw new SoloError(`Invalid component id: ${id}, type: ${typeof id}`);
    }
    const maxHelmReleaseNameLength: number = 53;
    const baseReleaseName: string = `${constants.EXPLORER_INGRESS_CONTROLLER_RELEASE_NAME}-${id}-${namespaceName.name}`;
    if (baseReleaseName.length <= maxHelmReleaseNameLength) {
      return baseReleaseName;
    }

    // Keep names deterministic and short enough for Helm while preserving readability.
    const hashSuffixLength: number = 8;
    const namespaceHash: string = createHash('sha256')
      .update(namespaceName.name)
      .digest('hex')
      .slice(0, hashSuffixLength);
    const prefix: string = `${constants.EXPLORER_INGRESS_CONTROLLER_RELEASE_NAME}-${id}`;
    const availableNamespaceLength: number =
      maxHelmReleaseNameLength - prefix.length - 1 - hashSuffixLength - 1; /* - */
    if (availableNamespaceLength <= 0) {
      return `${prefix}-${namespaceHash}`;
    }

    const shortenedNamespace: string = namespaceName.name.slice(0, availableNamespaceLength);
    return `${prefix}-${shortenedNamespace}-${namespaceHash}`;
  }

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

    const tasks: SoloListr<ExplorerDeployContext> = this.taskList.newTaskList<ExplorerDeployContext>(
      [
        {
          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();
            }

            this.configManager.update(argv);

            flags.disablePrompts(ExplorerCommand.DEPLOY_FLAGS_LIST.optional);

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

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

            const config: ExplorerDeployConfigClass = this.configManager.getConfig(
              ExplorerCommand.DEPLOY_CONFIGS_NAME,
              allFlags,
              [],
            ) as ExplorerDeployConfigClass;

            // In concurrent one-shot execution, configManager may have stale data due to
            // interleaved updates from other sub-commands. Override with argv values directly.
            if (this.oneShotState.isActive() && argv[flags.explorerVersion.name]) {
              config.explorerVersion = argv[flags.explorerVersion.name] as string;
            }

            config.isLegacyChartInstalled = false;

            context_.config = config;

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

            config.releaseName = this.getReleaseName();
            config.ingressReleaseName = this.getIngressReleaseName(config.namespace);

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

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

            config.newExplorerComponent = this.componentFactory.createNewExplorerComponent(
              config.clusterRef,
              config.namespace,
            );

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

            config.id = config.newExplorerComponent.metadata.id;

            config.valuesArg = await this.prepareValuesArg(context_.config);

            await this.throwIfNamespaceIsMissing(config.clusterContext, config.namespace);

            if (!this.oneShotState.isActive()) {
              return ListrLock.newAcquireLockTask(lease, task);
            }
            return ListrLock.newSkippedLockTask(task);
          },
        },
        this.loadRemoteConfigTask(argv),
        this.addExplorerComponents(),
        this.installCertManagerTask(ExplorerCommandType.ADD),
        this.installExplorerTask(ExplorerCommandType.ADD),
        this.installExplorerIngressControllerTask(),
        this.checkExplorerPodIsReadyTask(),
        this.checkExplorerIngressControllerPodIsReadyTask(),
        this.enablePortForwardingTask(),
        {
          title: 'Show user messages',
          skip: (): boolean => !this.oneShotState.isActive(),
          task: (): void => {
            this.logger.showAllMessageGroups();
          },
        },
      ],
      constants.LISTR_DEFAULT_OPTIONS.DEFAULT,
      undefined,
      'explorer node add',
    );

    if (tasks.isRoot()) {
      try {
        await tasks.run();
        this.logger.debug('explorer deployment has completed');
      } catch (error) {
        throw new SoloError(`Error deploying explorer: ${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 upgrade(argv: ArgvStruct): Promise<boolean> {
    let lease: Lock;

    const tasks: SoloListr<ExplorerUpgradeContext> = this.taskList.newTaskList<ExplorerUpgradeContext>(
      [
        {
          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();
            }

            this.configManager.update(argv);

            flags.disablePrompts(ExplorerCommand.UPGRADE_FLAGS_LIST.optional);

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

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

            const config: ExplorerUpgradeConfigClass = this.configManager.getConfig(
              ExplorerCommand.UPGRADE_CONFIGS_NAME,
              allFlags,
              [],
            ) as ExplorerUpgradeConfigClass;

            context_.config = config;

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

            const {id, releaseName, ingressReleaseName, isChartInstalled, isLegacyChartInstalled} =
              await this.inferExplorerData(config.namespace, config.clusterContext);

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

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

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

            config.valuesArg = await this.prepareValuesArg(context_.config);

            assertUpgradeVersionNotOlder(
              'Explorer',
              config.explorerVersion,
              this.remoteConfig.getComponentVersion(ComponentTypes.Explorer),
              optionFromFlag(flags.explorerVersion),
            );

            await this.throwIfNamespaceIsMissing(config.clusterContext, config.namespace);

            if (!this.oneShotState.isActive()) {
              return ListrLock.newAcquireLockTask(lease, task);
            }
            return ListrLock.newSkippedLockTask(task);
          },
        },
        this.loadRemoteConfigTask(argv),
        this.installCertManagerTask(ExplorerCommandType.UPGRADE),
        this.installExplorerTask(ExplorerCommandType.UPGRADE),
        this.installExplorerIngressControllerTask(),
        this.checkExplorerPodIsReadyTask(),
        this.checkExplorerIngressControllerPodIsReadyTask(),
        this.enablePortForwardingTask(),
      ],
      constants.LISTR_DEFAULT_OPTIONS.DEFAULT,
      undefined,
      'explorer node upgrade',
    );

    if (tasks.isRoot()) {
      try {
        await tasks.run();
        this.logger.debug('explorer upgrading has completed');
      } catch (error) {
        throw new SoloError(`Error upgrading explorer: ${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<ExplorerDestroyContext> = this.taskList.newTaskList<ExplorerDestroyContext>(
      [
        {
          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();
            }
            if (!argv.force) {
              const confirmResult: boolean = await task.prompt(ListrInquirerPromptAdapter).run(confirmPrompt, {
                default: false,
                message: 'Are you sure you would like to destroy the explorer?',
              });

              if (!confirmResult) {
                throw new UserBreak('Aborted application by user prompt');
              }
            }

            this.configManager.update(argv);

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

            const {id, releaseName, ingressReleaseName, isChartInstalled, isLegacyChartInstalled} =
              await this.inferExplorerData(namespace, clusterContext);

            context_.config = {
              namespace,
              clusterContext,
              clusterReference,
              id,
              releaseName,
              ingressReleaseName,
              isChartInstalled,
              isLegacyChartInstalled,
            };

            await this.throwIfNamespaceIsMissing(clusterContext, namespace);

            if (!this.oneShotState.isActive()) {
              return ListrLock.newAcquireLockTask(lease, task);
            }
            return ListrLock.newSkippedLockTask(task);
          },
        },
        this.loadRemoteConfigTask(argv, true),
        this.loadRemoteConfigTask(argv),
        {
          title: 'Destroy explorer',
          task: async (context_): Promise<void> => {
            await this.chartManager.uninstall(
              context_.config.namespace,
              context_.config.releaseName,
              context_.config.clusterContext,
            );
          },
          skip: (context_): boolean => !context_.config.isChartInstalled,
        },
        {
          title: 'Uninstall explorer ingress controller',
          task: async (context_): Promise<void> => {
            await this.chartManager.uninstall(context_.config.namespace, context_.config.ingressReleaseName);
            // destroy ingress class if found one
            const existingIngressClasses: IngressClass[] = await this.k8Factory
              .getK8(context_.config.clusterContext)
              .ingressClasses()
              .list();
            existingIngressClasses.map((ingressClass: IngressClass): void => {
              if (ingressClass.name === context_.config.ingressReleaseName) {
                this.k8Factory
                  .getK8(context_.config.clusterContext)
                  .ingressClasses()
                  .delete(context_.config.ingressReleaseName);
              }
            });
          },
        },
        this.disableMirrorNodeExplorerComponents(),
      ],
      constants.LISTR_DEFAULT_OPTIONS.DEFAULT,
      undefined,
      'explorer node destroy',
    );

    if (tasks.isRoot()) {
      try {
        await tasks.run();
      } catch (error) {
        throw new SoloError(`Error destroy explorer: ${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;
  }

  private loadRemoteConfigTask(argv: ArgvStruct, safe: boolean = false): SoloListrTask<AnyListrContext> {
    return {
      title: 'Load remote config',
      task: async (): Promise<void> => {
        if (safe) {
          await this.loadRemoteConfigOrWarn(argv);
          return;
        }
        await this.remoteConfig.loadAndValidate(argv);
      },
    };
  }

  /** Removes the explorer components from remote config. */
  private disableMirrorNodeExplorerComponents(): SoloListrTask<ExplorerDestroyContext> {
    return {
      title: 'Remove explorer from remote config',
      skip: (): boolean => !this.remoteConfig.isLoaded(),
      task: async ({config}): Promise<void> => {
        this.remoteConfig.configuration.components.removeComponent(config.id, ComponentTypes.Explorer);

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

  /** Adds the explorer components to remote config. */
  private addExplorerComponents(): SoloListrTask<ExplorerDeployContext> {
    return {
      title: 'Add explorer to remote config',
      skip: (): boolean => !this.remoteConfig.isLoaded() || this.oneShotState.isActive(),
      task: async ({config}): Promise<void> => {
        this.remoteConfig.configuration.components.addNewComponent(
          config.newExplorerComponent,
          ComponentTypes.Explorer,
        );

        // update explorer version in remote config
        this.remoteConfig.updateComponentVersion(
          ComponentTypes.Explorer,
          new SemanticVersion<string>(config.explorerVersion),
        );

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

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

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

  private inferExplorerId(): ComponentId {
    const id: ComponentId = this.configManager.getFlag(flags.id);

    if (typeof id === 'number') {
      return id;
    }

    if (!this.remoteConfig.configuration.components.state.explorers[0]) {
      throw new SoloError('No explorer component found in remote config');
    }

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

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

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

    if (isLegacyChartInstalled) {
      return {
        id,
        releaseName: constants.EXPLORER_RELEASE_NAME,
        isChartInstalled: true,
        ingressReleaseName: constants.EXPLORER_INGRESS_CONTROLLER_RELEASE_NAME,
        isLegacyChartInstalled,
      };
    }

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