// SPDX-License-Identifier: Apache-2.0

import {inject, injectable} from 'tsyringe-neo';
import {type ObjectMapper} from '../../../../data/mapper/api/object-mapper.js';
import {ReadRemoteConfigBeforeLoadError} from '../../../errors/read-remote-config-before-load-error.js';
import {WriteRemoteConfigBeforeLoadError} from '../../../errors/write-remote-config-before-load-error.js';
import {RemoteConfigSource} from '../../../../data/configuration/impl/remote-config-source.js';
import {YamlConfigMapStorageBackend} from '../../../../data/backend/impl/yaml-config-map-storage-backend.js';
import {type ConfigMap} from '../../../../integration/kube/resources/config-map/config-map.js';
import {LedgerPhase} from '../../../../data/schema/model/remote/ledger-phase.js';
import {ComponentsDataWrapperApi} from '../../../../core/config/remote/api/components-data-wrapper-api.js';
import {InjectTokens} from '../../../../core/dependency-injection/inject-tokens.js';
import {type K8Factory} from '../../../../integration/kube/k8-factory.js';
import {type SoloLogger} from '../../../../core/logging/solo-logger.js';
import {type ConfigManager} from '../../../../core/config-manager.js';
import {patchInject} from '../../../../core/dependency-injection/container-helper.js';
import {
  type ClusterReferenceName,
  type ClusterReferences,
  type Context,
  type DeploymentName,
  type NamespaceNameAsString,
  Optional,
} from '../../../../types/index.js';
import {
  type AnyObject,
  type ArgvStruct,
  type NodeAlias,
  type NodeAliases,
  type NodeId,
} from '../../../../types/aliases.js';
import {NamespaceName} from '../../../../types/namespace/namespace-name.js';
import {ComponentStateMetadataSchema} from '../../../../data/schema/model/remote/state/component-state-metadata-schema.js';
import {Templates} from '../../../../core/templates.js';
import {DeploymentPhase} from '../../../../data/schema/model/remote/deployment-phase.js';
import {getSoloVersion} from '../../../../../version.js';
import * as constants from '../../../../core/constants.js';
import {SoloError} from '../../../../core/errors/solo-error.js';
import {Flags as flags} from '../../../../commands/flags.js';
import {promptTheUserForDeployment} from '../../../../core/resolvers.js';
import {ConsensusNode} from '../../../../core/model/consensus-node.js';
import {RemoteConfigRuntimeStateApi} from '../../api/remote-config-runtime-state-api.js';
import {type RemoteConfigValidatorApi} from '../../../../core/config/remote/api/remote-config-validator-api.js';
import {ComponentFactoryApi} from '../../../../core/config/remote/api/component-factory-api.js';
import {ComponentTypes} from '../../../../core/config/remote/enumerations/component-types.js';
import {LocalConfigRuntimeState} from '../local/local-config-runtime-state.js';
import {RemoteConfigMetadataSchema} from '../../../../data/schema/model/remote/remote-config-metadata-schema.js';
import {ApplicationVersionsSchema} from '../../../../data/schema/model/common/application-versions-schema.js';
import {ClusterSchema} from '../../../../data/schema/model/common/cluster-schema.js';
import {DeploymentStateSchema} from '../../../../data/schema/model/remote/deployment-state-schema.js';
import {DeploymentHistorySchema} from '../../../../data/schema/model/remote/deployment-history-schema.js';
import {RemoteConfigSchemaDefinition} from '../../../../data/schema/migration/impl/remote/remote-config-schema-definition.js';
import {RemoteConfigSchema} from '../../../../data/schema/model/remote/remote-config-schema.js';
import {ConsensusNodeStateSchema} from '../../../../data/schema/model/remote/state/consensus-node-state-schema.js';
import {UserIdentitySchema} from '../../../../data/schema/model/common/user-identity-schema.js';
import {Deployment} from '../local/deployment.js';
import {RemoteConfig} from './remote-config.js';
import {ComponentIdsSchema} from '../../../../data/schema/model/remote/state/component-ids-schema.js';
import * as helpers from '../../../../core/helpers.js';
import {ResourceNotFoundError} from '../../../../integration/kube/errors/resource-operation-errors.js';
import {MissingRequiredParametersError} from '../../errors/missing-required-parameters-error.js';
import {SemanticVersion} from '../../../utils/semantic-version.js';

enum RuntimeStatePhase {
  Loaded = 'loaded',
  NotLoaded = 'not_loaded',
}

interface VersionField {
  value: SemanticVersion<string>;
}

@injectable()
export class RemoteConfigRuntimeState implements RemoteConfigRuntimeStateApi {
  private static readonly SOLO_REMOTE_CONFIGMAP_DATA_KEY: string = 'remote-config-data';

  private phase: RuntimeStatePhase = RuntimeStatePhase.NotLoaded;

  public clusterReferences: Map<Context, ClusterReferenceName> = new Map();
  private namespace: NamespaceName;

  private source?: RemoteConfigSource;
  private backend?: YamlConfigMapStorageBackend;

  private _remoteConfig?: RemoteConfig;

  public constructor(
    @inject(InjectTokens.K8Factory) private readonly k8Factory?: K8Factory,
    @inject(InjectTokens.SoloLogger) private readonly logger?: SoloLogger,
    @inject(InjectTokens.LocalConfigRuntimeState) private readonly localConfig?: LocalConfigRuntimeState,
    @inject(InjectTokens.ConfigManager) private readonly configManager?: ConfigManager,
    @inject(InjectTokens.RemoteConfigValidator) private readonly remoteConfigValidator?: RemoteConfigValidatorApi,
    @inject(InjectTokens.ObjectMapper) private readonly objectMapper?: ObjectMapper,
  ) {
    this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
    this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
    this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name);
    this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
    this.remoteConfigValidator = patchInject(
      remoteConfigValidator,
      InjectTokens.RemoteConfigValidator,
      this.constructor.name,
    );
    this.objectMapper = patchInject(objectMapper, InjectTokens.ObjectMapper, this.constructor.name);
  }

  public get configuration(): RemoteConfig {
    this.failIfNotLoaded();
    return this._remoteConfig;
  }

  public get components(): Readonly<ComponentsDataWrapperApi> {
    this.failIfNotLoaded();
    return this._remoteConfig.components;
  }

  public get currentCluster(): ClusterReferenceName {
    return this.k8Factory.default().clusters().readCurrent();
  }

  public async load(namespace?: NamespaceName, context?: Context): Promise<void> {
    if (this.isLoaded()) {
      return;
    }

    await this.populateFromExisting(namespace, context);
  }

  private async populateFromConfigMap(configMap: ConfigMap, remoteConfig?: RemoteConfigSchema): Promise<void> {
    this.backend = new YamlConfigMapStorageBackend(configMap);

    this.source = new RemoteConfigSource(
      new RemoteConfigSchemaDefinition(this.objectMapper),
      this.objectMapper,
      this.backend,
    );

    await this.source.load();

    if (remoteConfig) {
      this.source.setModelData(remoteConfig);
    }

    this._remoteConfig = new RemoteConfig(this.source.modelData);
    this.phase = RuntimeStatePhase.Loaded;
  }

  private async updateConfigMap(
    context: Context,
    namespace: NamespaceName,
    data: Record<string, string>,
  ): Promise<void> {
    await this.k8Factory.getK8(context).configMaps().update(namespace, constants.SOLO_REMOTE_CONFIGMAP_NAME, data);
  }

  public isLoaded(): boolean {
    return this.phase === RuntimeStatePhase.Loaded;
  }

  private failIfNotLoaded(): void {
    if (!this.isLoaded()) {
      throw new ReadRemoteConfigBeforeLoadError('Attempting to read from remote config before loading it');
    }
  }

  public async persist(): Promise<void> {
    if (!this.isLoaded()) {
      throw new WriteRemoteConfigBeforeLoadError('Attempting to persist remote config before loading it');
    }

    await this.source.persist();
    const remoteConfigDataBytes: Buffer = await this.backend.readBytes(
      RemoteConfigRuntimeState.SOLO_REMOTE_CONFIGMAP_DATA_KEY,
    );

    const remoteConfigData: Record<string, string> = {
      [RemoteConfigRuntimeState.SOLO_REMOTE_CONFIGMAP_DATA_KEY]: remoteConfigDataBytes.toString('utf8'),
    };

    const promises: Promise<void>[] = [];

    for (const context of this.clusterReferences.keys()) {
      promises.push(this.updateConfigMap(context, this.namespace, remoteConfigData));
    }

    await Promise.all(promises);
  }

  public async create(
    argv: ArgvStruct,
    ledgerPhase: LedgerPhase,
    nodeAliases: NodeAliases,
    namespace: NamespaceName,
    deploymentName: DeploymentName,
    clusterReference: ClusterReferenceName,
    context: Context,
    dnsBaseDomain: string,
    dnsConsensusNodePattern: string,
  ): Promise<void> {
    this.populateClusterReferences(deploymentName);

    const consensusNodeStates: ConsensusNodeStateSchema[] = nodeAliases.map(
      (nodeAlias: NodeAlias): ConsensusNodeStateSchema => {
        return new ConsensusNodeStateSchema(
          new ComponentStateMetadataSchema(
            Templates.renderComponentIdFromNodeAlias(nodeAlias),
            namespace.name,
            clusterReference,
            DeploymentPhase.REQUESTED,
          ),
        );
      },
    );

    const userIdentity: Readonly<UserIdentitySchema> = this.localConfig.configuration.userIdentity;
    const cliVersion: SemanticVersion<string> = new SemanticVersion<string>(getSoloVersion());
    const command: string = argv._.join(' ');

    const cluster: ClusterSchema = new ClusterSchema(
      clusterReference,
      namespace.name,
      deploymentName,
      dnsBaseDomain,
      dnsConsensusNodePattern,
    );

    const remoteConfig: RemoteConfigSchema = new RemoteConfigSchema(
      6,
      new RemoteConfigMetadataSchema(new Date(), userIdentity),
      new ApplicationVersionsSchema(cliVersion),
      [cluster],
      new DeploymentStateSchema(ledgerPhase, new ComponentIdsSchema(nodeAliases.length + 1), consensusNodeStates),
      new DeploymentHistorySchema([command], command),
    );

    const configMap: ConfigMap = await this.createConfigMap(namespace, context);
    await this.populateFromConfigMap(configMap, remoteConfig);

    await this.persist();
  }

  public async createFromExisting(
    namespace: NamespaceName,
    clusterReference: ClusterReferenceName,
    deploymentName: DeploymentName,
    componentFactory: ComponentFactoryApi,
    dnsBaseDomain: string,
    dnsConsensusNodePattern: string,
    existingClusterContext: Context,
    argv: ArgvStruct,
    nodeAliases: NodeAliases,
  ): Promise<void> {
    await this.populateFromExisting(namespace, existingClusterContext);

    this.populateClusterReferences(deploymentName);

    const newClusterContext: Context = this.localConfig.configuration.clusterRefs
      .get(clusterReference.toString())
      ?.toString();

    //? Create copy of the existing remote config inside the new cluster
    await this.createConfigMap(namespace, newClusterContext);
    await this.persist();

    //* update the command history
    this.addCommandToHistory(argv._.join(' '));

    //* add the new clusters
    this.configuration.addCluster(
      new ClusterSchema(clusterReference, namespace.name, deploymentName, dnsBaseDomain, dnsConsensusNodePattern),
    );

    //* add the new nodes to components
    for (const nodeAlias of nodeAliases) {
      this.configuration.components.addNewComponent(
        componentFactory.createNewConsensusNodeComponent(
          Templates.renderComponentIdFromNodeAlias(nodeAlias),
          clusterReference,
          namespace,
          DeploymentPhase.REQUESTED,
        ),
        ComponentTypes.ConsensusNode,
      );
    }

    await this.persist();
  }

  public addCommandToHistory(command: string): void {
    this.source.modelData.history.commands.push(command);
    this.source.modelData.history.lastExecutedCommand = command;

    if (this.source.modelData.history.commands.length > constants.SOLO_REMOTE_CONFIG_MAX_COMMAND_IN_HISTORY) {
      this.source.modelData.history.commands.shift();
    }
  }

  public async createConfigMap(namespace: NamespaceName, context: Context): Promise<ConfigMap> {
    const name: string = constants.SOLO_REMOTE_CONFIGMAP_NAME;
    const labels: Record<string, string> = constants.SOLO_REMOTE_CONFIGMAP_LABELS;
    await this.k8Factory
      .getK8(context)
      .configMaps()
      .create(namespace, name, labels, {[RemoteConfigRuntimeState.SOLO_REMOTE_CONFIGMAP_DATA_KEY]: '{}'});
    return await this.k8Factory.getK8(context).configMaps().read(namespace, name);
  }

  private async getConfigMap(namespace: NamespaceName, context: Context): Promise<ConfigMap> {
    if (!namespace || !context) {
      throw new MissingRequiredParametersError(
        `Namespace and context are required to get the remote config ConfigMap, received namespace: ${namespace}, context: ${context}`,
      );
    }

    let configMap: ConfigMap;
    try {
      configMap = await this.k8Factory
        .getK8(context)
        .configMaps()
        .read(namespace, constants.SOLO_REMOTE_CONFIGMAP_NAME);
    } catch (error) {
      throw error instanceof ResourceNotFoundError
        ? error
        : new SoloError(
            `Failed to get remote config ConfigMap for namespace: ${namespace}, context: ${context}. Error: ${error.message}`,
            error,
          );
    }
    if (!configMap) {
      throw new SoloError(`Remote config ConfigMap not found for namespace: ${namespace}, context: ${context}`);
    }

    return configMap;
  }

  public async populateFromExisting(namespace: NamespaceName, context: Context): Promise<void> {
    const remoteConfigConfigMap: ConfigMap = await this.getConfigMap(namespace, context);
    await this.populateFromConfigMap(remoteConfigConfigMap);
  }

  public async remoteConfigExists(namespace: NamespaceName, context: Context): Promise<boolean> {
    const configMap: ConfigMap = await this.getConfigMap(namespace, context);
    return !!configMap;
  }

  public populateClusterReferences(deploymentName: DeploymentName): Context {
    let deployment: Deployment;
    try {
      deployment = this.localConfig.configuration.deploymentByName(deploymentName);
    } catch {
      // Deployment not in local config — fall back to namespace/context already resolved from remote config scan.
      const namespaceFromConfig: NamespaceName = this.configManager.getFlag(flags.namespace);
      if (namespaceFromConfig) {
        this.namespace = namespaceFromConfig;
      }
      return this.configManager.getFlag<Context>(flags.context);
    }

    this.namespace = NamespaceName.of(deployment.namespace);

    for (const clusterReference of deployment.clusters) {
      const context: Context = this.localConfig.configuration.clusterRefs.get(clusterReference.toString())?.toString();
      this.clusterReferences.set(context, clusterReference.toString());
    }

    return this.localConfig.configuration.clusterRefs.get(deployment.clusters.get(0)?.toString())?.toString();
  }

  /**
   * Performs the loading of the remote configuration.
   * Checks if the configuration is already loaded, otherwise loads and adds the command to history.
   *
   * @param argv - arguments containing command input for historical reference.
   * @param validate - whether to validate the remote configuration.
   * @param [skipConsensusNodesValidation] - whether or not to validate the consensusNodes
   */
  public async loadAndValidate(
    argv: {_: string[]} & AnyObject,
    validate: boolean = true,
    skipConsensusNodesValidation: boolean = true,
  ): Promise<void> {
    await this.setDefaultNamespaceAndDeploymentIfNotSet(argv);
    await this.setDefaultContextIfNotSet();

    // Sync resolved context back to argv so subsequent configManager.update(argv) preserves it.
    argv[flags.context.name] ||= this.configManager.getFlag<Context>(flags.context);

    const deploymentName: DeploymentName = this.configManager.getFlag(flags.deployment);
    const context: Context = this.populateClusterReferences(deploymentName);

    // TODO: Compare configs from clusterReferences
    await this.load(this.namespace, context);

    this.logger.info('Remote config loaded');
    if (!validate) {
      return;
    }

    await this.remoteConfigValidator.validateComponents(
      this.namespace,
      skipConsensusNodesValidation,
      this.configuration.state,
    );

    const currentCommand: string = argv._?.join(' ');
    const commandArguments: string = flags.stringifyArgv(argv);

    this.addCommandToHistory(
      `Executed by ${this.localConfig.configuration.userIdentity.name}: ${currentCommand} ${commandArguments}`.trim(),
    );

    this.initializeComponentVersions(argv, this.source.modelData);

    await this.persist();
  }

  private initializeComponentVersions(argv: AnyObject, remoteConfig: RemoteConfigSchema): void {
    remoteConfig.versions.chart = argv[flags.soloChartVersion.name]
      ? new SemanticVersion(argv[flags.soloChartVersion.name])
      : new SemanticVersion(flags.soloChartVersion.definition.defaultValue as string);

    // set default versions if not set
    const componentTypes: ComponentTypes[] = [
      ComponentTypes.BlockNode,
      ComponentTypes.RelayNodes,
      ComponentTypes.MirrorNode,
      ComponentTypes.Explorer,
      ComponentTypes.ConsensusNode,
    ];

    for (const componentType of componentTypes) {
      const version: SemanticVersion<string> = this.getComponentVersion(componentType);
      if (version.equals('0.0.0')) {
        switch (componentType) {
          case ComponentTypes.BlockNode: {
            this.updateComponentVersion(
              componentType,
              new SemanticVersion<string>(flags.blockNodeChartVersion.definition.defaultValue as string),
            );
            break;
          }
          case ComponentTypes.RelayNodes: {
            this.updateComponentVersion(
              componentType,
              new SemanticVersion<string>(flags.relayReleaseTag.definition.defaultValue as string),
            );
            break;
          }
          case ComponentTypes.MirrorNode: {
            this.updateComponentVersion(
              componentType,
              new SemanticVersion<string>(flags.mirrorNodeVersion.definition.defaultValue as string),
            );
            break;
          }
          case ComponentTypes.Explorer: {
            this.updateComponentVersion(
              componentType,
              new SemanticVersion<string>(flags.explorerVersion.definition.defaultValue as string),
            );
            break;
          }
          case ComponentTypes.ConsensusNode: {
            this.updateComponentVersion(
              componentType,
              new SemanticVersion<string>(flags.releaseTag.definition.defaultValue as string),
            );
            break;
          }
          default: {
            throw new SoloError(`Unsupported component type: ${componentType}`);
          }
        }
      }
    }
  }

  public async deleteComponents(): Promise<void> {
    this._remoteConfig.state.consensusNodes = [];
    this._remoteConfig.state.blockNodes = [];
    this._remoteConfig.state.envoyProxies = [];
    this._remoteConfig.state.haProxies = [];
    this._remoteConfig.state.explorers = [];
    this._remoteConfig.state.mirrorNodes = [];
    this._remoteConfig.state.relayNodes = [];
    await this.persist();
  }

  private async setDefaultNamespaceAndDeploymentIfNotSet(argv: AnyObject): Promise<void> {
    let namespaceFromConfig: NamespaceNameAsString = this.configManager.getFlag(flags.namespace);
    let deploymentName: DeploymentName = this.configManager.getFlag(flags.deployment);
    const deploymentFromArgv: DeploymentName = argv[flags.deployment.name] as DeploymentName;
    const namespaceFromArgv: NamespaceNameAsString = argv[flags.namespace.name] as NamespaceNameAsString;

    // Keep config manager in sync when deployment/namespace are resolved directly in argv by caller logic.
    if (!deploymentName && deploymentFromArgv) {
      this.configManager.setFlag(flags.deployment, deploymentFromArgv);
      deploymentName = deploymentFromArgv;
    }

    // Keep config manager in sync when namespace is resolved directly in argv by caller logic.
    // argv takes precedence over the default namespace that middleware may have set from kubectl context.
    if (namespaceFromArgv) {
      this.configManager.setFlag(flags.namespace, namespaceFromArgv);
      namespaceFromConfig = namespaceFromArgv;
    }

    if (namespaceFromConfig && deploymentName) {
      return;
    }

    // TODO: Current quick fix for commands where namespace is not passed
    let currentDeployment: Deployment = this.localConfig.configuration.deploymentByName(deploymentName);

    if (!deploymentName) {
      deploymentName = await promptTheUserForDeployment(this.configManager);
      currentDeployment = this.localConfig.configuration.deploymentByName(deploymentName);
      // TODO: Fix once we have the DataManager,
      //       without this the user will be prompted a second time for the deployment
      // TODO: we should not be mutating argv
      argv[flags.deployment.name] = deploymentName;
      this.logger.warn(
        `Deployment name not found in flags or local config, setting it in argv and config manager to: ${deploymentName}`,
      );
      this.configManager.setFlag(flags.deployment, deploymentName);
    }

    if (!currentDeployment) {
      throw new SoloError(`Selected deployment name is not set in local config - ${deploymentName}`);
    }

    const namespace: NamespaceNameAsString = currentDeployment.namespace;

    this.logger.warn(`Namespace not found in flags, setting it to: ${namespace}`);
    this.configManager.setFlag(flags.namespace, namespace);
    argv[flags.namespace.name] = namespace;
  }

  private async setDefaultContextIfNotSet(): Promise<void> {
    if (this.configManager.hasFlag(flags.context)) {
      return;
    }

    let context: Context;
    try {
      context = await this.getContextForFirstCluster();
    } catch {
      context = this.k8Factory.default().contexts().readCurrent();
    }

    if (!context) {
      throw new SoloError("Context is not passed and default one can't be acquired");
    }

    this.logger.warn(`Context not found in flags, setting it to: ${context}`);
    this.configManager.setFlag(flags.context, context);
  }

  //* Common Commands

  /**
   * Get the consensus nodes from the remoteConfig and use the localConfig to get the context
   * @returns an array of ConsensusNode objects
   */
  public getConsensusNodes(): ConsensusNode[] {
    if (!this.isLoaded()) {
      throw new SoloError('Remote configuration is not loaded, and was expected to be loaded');
    }

    const consensusNodes: ConsensusNode[] = [];

    for (const node of Object.values(this.configuration.state.consensusNodes)) {
      const cluster: ClusterSchema = this.configuration.clusters.find(
        (cluster: ClusterSchema): boolean => cluster.name === node.metadata.cluster,
      );
      const context: Context =
        this.localConfig.configuration.clusterRefs.get(node.metadata.cluster)?.toString() ??
        this.configManager.getFlag(flags.context);
      const nodeAlias: NodeAlias = Templates.renderNodeAliasFromNumber(node.metadata.id);
      const nodeId: NodeId = Templates.renderNodeIdFromComponentId(node.metadata.id);

      consensusNodes.push(
        new ConsensusNode(
          nodeAlias,
          nodeId,
          node.metadata.namespace,
          node.metadata.cluster,
          context,
          cluster.dnsBaseDomain,
          cluster.dnsConsensusNodePattern,
          Templates.renderConsensusNodeFullyQualifiedDomainName(
            nodeAlias,
            nodeId,
            node.metadata.namespace,
            node.metadata.cluster,
            cluster.dnsBaseDomain,
            cluster.dnsConsensusNodePattern,
          ),
          node.blockNodeMap,
          node.externalBlockNodeMap,
        ),
      );
    }

    // return the consensus nodes
    return consensusNodes;
  }

  /**
   * Gets a list of distinct contexts from the consensus nodes.
   * @returns an array of context strings.
   */
  public getContexts(): Context[] {
    return [...new Set(this.getConsensusNodes().map((node): Context => node.context))];
  }

  /**
   * Gets a list of distinct cluster references from the consensus nodes.
   * @returns an object of cluster references.
   */
  public getClusterRefs(): ClusterReferences {
    const nodes: ConsensusNode[] = this.getConsensusNodes();
    const accumulator: ClusterReferences = new Map<string, string>();

    for (const node of nodes) {
      accumulator.set(node.cluster, node.context);
    }

    return accumulator;
  }

  private async getContextForFirstCluster(): Promise<string> {
    const deploymentName: DeploymentName = this.configManager.getFlag(flags.deployment);

    const clusterReference: ClusterReferenceName =
      this.localConfig.configuration.deploymentByName(deploymentName)?.clusters?.get(0)?.toString() ??
      this.k8Factory.default().clusters().readCurrent();

    const context: Context = this.localConfig.configuration.clusterRefs.get(clusterReference)?.toString();

    this.logger.debug(`Using context ${context} for cluster ${clusterReference} for deployment ${deploymentName}`);

    return context;
  }

  public getNamespace(): NamespaceName {
    return NamespaceName.of(this.configuration.clusters?.at(0)?.namespace);
  }

  public extractContextFromConsensusNodes(nodeAlias: NodeAlias): Optional<string> {
    return helpers.extractContextFromConsensusNodes(nodeAlias, this.getConsensusNodes());
  }

  public updateComponentVersion(type: ComponentTypes, version: SemanticVersion<string>): void {
    const updateVersionCallback: (versionField: VersionField) => void = (versionField: VersionField): void => {
      versionField.value = version;
    };

    this.applyCallbackToVersionField(type, updateVersionCallback);
  }

  /**
   * Method used to map the component type to the specific version field
   * and pass it to a callback to apply modifications
   */
  private applyCallbackToVersionField(
    componentType: ComponentTypes,
    callback: (versionField: VersionField) => void,
  ): void {
    switch (componentType) {
      case ComponentTypes.ConsensusNode: {
        const versionField: VersionField = {value: this.configuration.versions.consensusNode};
        callback(versionField);
        this.configuration.versions.consensusNode = versionField.value;
        break;
      }
      case ComponentTypes.MirrorNode: {
        const versionField: VersionField = {value: this.configuration.versions.mirrorNodeChart};
        callback(versionField);
        this.configuration.versions.mirrorNodeChart = versionField.value;
        break;
      }
      case ComponentTypes.Explorer: {
        const versionField: VersionField = {value: this.configuration.versions.explorerChart};
        callback(versionField);
        this.configuration.versions.explorerChart = versionField.value;
        break;
      }
      case ComponentTypes.RelayNodes: {
        const versionField: VersionField = {value: this.configuration.versions.jsonRpcRelayChart};
        callback(versionField);
        this.configuration.versions.jsonRpcRelayChart = versionField.value;
        break;
      }
      case ComponentTypes.BlockNode: {
        const versionField: VersionField = {value: this.configuration.versions.blockNodeChart};
        callback(versionField);
        this.configuration.versions.blockNodeChart = versionField.value;
        break;
      }
      case ComponentTypes.Cli: {
        const versionField: VersionField = {value: this.configuration.versions.cli};
        callback(versionField);
        this.configuration.versions.cli = versionField.value;
        break;
      }
      case ComponentTypes.Chart: {
        const versionField: VersionField = {value: this.configuration.versions.chart};
        callback(versionField);
        this.configuration.versions.chart = versionField.value;
        break;
      }
      default: {
        throw new SoloError(`Unsupported component type: ${componentType}`);
      }
    }
  }

  public getComponentVersion(type: ComponentTypes): SemanticVersion<string> {
    let version: SemanticVersion<string>;

    const getVersionCallback: (versionField: VersionField) => void = (versionField: VersionField): void => {
      version = versionField.value;
    };

    this.applyCallbackToVersionField(type, getVersionCallback);
    return version;
  }
}
