// SPDX-License-Identifier: Apache-2.0

import {Listr} from 'listr2';
import {type AnyListrContext, type ArgvStruct, type ConfigBuilder} from '../../types/aliases.js';
import * as constants from '../../core/constants.js';
import chalk from 'chalk';
import {ListrLock} from '../../core/lock/listr-lock.js';
import {ErrorMessages} from '../../core/error-messages.js';
import {SoloError} from '../../core/errors/solo-error.js';
import {UserBreak} from '../../core/errors/user-break.js';
import {type K8Factory} from '../../integration/kube/k8-factory.js';
import {type Context, type ReleaseNameData, type SoloListr, type SoloListrTask} from '../../types/index.js';
import {ListrInquirerPromptAdapter} from '@listr2/prompt-adapter-inquirer';
import {confirm as confirmPrompt} from '@inquirer/prompts';
import {type NamespaceName} from '../../types/namespace/namespace-name.js';
import {inject, injectable} from 'tsyringe-neo';
import {patchInject} from '../../core/dependency-injection/container-helper.js';
import {type SoloLogger} from '../../core/logging/solo-logger.js';
import {type ChartManager} from '../../core/chart-manager.js';
import {type LockManager} from '../../core/lock/lock-manager.js';
import {type ClusterChecks} from '../../core/cluster-checks.js';
import {InjectTokens} from '../../core/dependency-injection/inject-tokens.js';
import {type ClusterReferenceConnectContext} from './config-interfaces/cluster-reference-connect-context.js';
import {type ClusterReferenceDefaultContext} from './config-interfaces/cluster-reference-default-context.js';
import {type ClusterReferenceSetupContext} from './config-interfaces/cluster-reference-setup-context.js';
import {type ClusterReferenceResetContext} from './config-interfaces/cluster-reference-reset-context.js';
import {LocalConfigRuntimeState} from '../../business/runtime-state/config/local/local-config-runtime-state.js';
import {StringFacade} from '../../business/runtime-state/facade/string-facade.js';
import {type FacadeMap} from '../../business/runtime-state/collection/facade-map.js';
import {MutableFacadeArray} from '../../business/runtime-state/collection/mutable-facade-array.js';
import {Deployment} from '../../business/runtime-state/config/local/deployment.js';
import {DeploymentSchema} from '../../data/schema/model/local/deployment-schema.js';
import {Lock} from '../../core/lock/lock.js';
import {RemoteConfigRuntimeState} from '../../business/runtime-state/config/remote/remote-config-runtime-state.js';
import {type OneShotState} from '../../core/one-shot-state.js';
import * as versions from '../../../version.js';
import {findMinioOperator} from '../../core/helpers.js';
import {K8} from '../../integration/kube/k8.js';

@injectable()
export class ClusterCommandTasks {
  public constructor(
    @inject(InjectTokens.K8Factory) private readonly k8Factory: K8Factory,
    @inject(InjectTokens.LocalConfigRuntimeState) private readonly localConfig: LocalConfigRuntimeState,
    @inject(InjectTokens.SoloLogger) private readonly logger: SoloLogger,
    @inject(InjectTokens.ChartManager) private readonly chartManager: ChartManager,
    @inject(InjectTokens.LockManager) private readonly leaseManager: LockManager,
    @inject(InjectTokens.ClusterChecks) private readonly clusterChecks: ClusterChecks,
    @inject(InjectTokens.RemoteConfigRuntimeState) private readonly remoteConfig: RemoteConfigRuntimeState,
    @inject(InjectTokens.OneShotState) private readonly oneShotState: OneShotState,
  ) {
    this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
    this.localConfig = patchInject(localConfig, InjectTokens.LocalConfigRuntimeState, this.constructor.name);
    this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
    this.chartManager = patchInject(chartManager, InjectTokens.ChartManager, this.constructor.name);
    this.leaseManager = patchInject(leaseManager, InjectTokens.LockManager, this.constructor.name);
    this.clusterChecks = patchInject(clusterChecks, InjectTokens.ClusterChecks, this.constructor.name);
    this.remoteConfig = patchInject(remoteConfig, InjectTokens.RemoteConfigRuntimeState, this.constructor.name);
    this.oneShotState = patchInject(oneShotState, InjectTokens.OneShotState, this.constructor.name);
  }

  public findMinioOperator(context: Context): Promise<ReleaseNameData> {
    return findMinioOperator(context, this.k8Factory);
  }

  public connectClusterRef(): SoloListrTask<ClusterReferenceConnectContext> {
    return {
      title: 'Associate a context with a cluster reference: ',
      task: async (context_, task): Promise<void> => {
        task.title += context_.config.clusterRef;

        this.localConfig.configuration.clusterRefs.set(
          context_.config.clusterRef,
          new StringFacade(context_.config.context),
        );

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

  public disconnectClusterRef(): SoloListrTask<ClusterReferenceDefaultContext> {
    return {
      title: 'Remove cluster reference ',
      task: async (context_, task): Promise<void> => {
        task.title += context_.config.clusterRef;

        this.localConfig.configuration.clusterRefs.delete(context_.config.clusterRef);
        await this.localConfig.persist();
      },
    };
  }

  public testConnectionToCluster(): SoloListrTask<ClusterReferenceConnectContext> {
    return {
      title: 'Test connection to cluster: ',
      task: async ({config: {context, clusterRef}}, task): Promise<void> => {
        task.title += context;
        try {
          await this.k8Factory.getK8(context).namespaces().list();
        } catch {
          task.title = `${task.title} - ${chalk.red('Cluster connection failed')}`;
          throw new SoloError(ErrorMessages.INVALID_CONTEXT_FOR_CLUSTER_DETAILED(context, clusterRef));
        }
      },
    };
  }

  public validateClusterRefs(): SoloListrTask<ClusterReferenceConnectContext> {
    return {
      title: 'Validating cluster ref: ',
      task: async ({config: {clusterRef}}, task): Promise<void> => {
        task.title += clusterRef;

        if (this.localConfig.configuration.clusterRefs.get(clusterRef)) {
          this.logger.showUser(chalk.yellow(`Cluster ref ${clusterRef} already exists inside local config`));
        }
      },
    };
  }

  /** Show list of installed chart */
  private async showInstalledChartList(clusterSetupNamespace: NamespaceName, context?: string): Promise<void> {
    // TODO convert to logger.addMessageGroup() & logger.addMessageGroupMessage()
    this.logger.showList(
      'Installed Charts',
      await this.chartManager.getInstalledCharts(clusterSetupNamespace, context),
    );
  }

  public initialize(
    argv: ArgvStruct,
    configInit: ConfigBuilder,
    loadRemoteConfig: boolean = false,
  ): SoloListrTask<AnyListrContext> {
    const {required, optional} = argv;

    argv.flags = [...required, ...optional];

    return {
      title: 'Initialize',
      task: async (context_, task): Promise<void> => {
        await this.localConfig.load();

        if (loadRemoteConfig) {
          await this.remoteConfig.loadAndValidate(argv);
        }
        context_.config = await configInit(argv, context_, task);
      },
    };
  }

  public showClusterList(): SoloListrTask<AnyListrContext> {
    return {
      title: 'List all available clusters',
      task: async (): Promise<void> => {
        await this.localConfig.load();

        const clusterReferences: FacadeMap<string, StringFacade, string> = this.localConfig.configuration.clusterRefs;
        const clusterList: string[] = [];
        for (const [clusterName, clusterContext] of clusterReferences) {
          clusterList.push(`${clusterName}:${clusterContext.toString()}`);
        }
        this.logger.showList('Cluster references and the respective contexts', clusterList);
      },
    };
  }

  public getClusterInfo(): SoloListrTask<AnyListrContext> {
    return {
      title: 'Get cluster info',
      task: async (context_, task) => {
        const clusterReference: string = context_.config.clusterRef;
        const clusterReferences: FacadeMap<string, StringFacade, string> = this.localConfig.configuration.clusterRefs;
        const deployments: MutableFacadeArray<Deployment, DeploymentSchema> =
          this.localConfig.configuration.deployments;
        const context: StringFacade | undefined = clusterReferences.get(clusterReference);

        if (!context) {
          throw new Error(`Cluster "${clusterReference}" not found in the LocalConfig`);
        }

        const deploymentsWithSelectedCluster: {name: string; namespace: string}[] = [...deployments]
          .filter((deployment): boolean =>
            deployment.clusters.some((cluster): boolean => cluster.toString() === clusterReference),
          )
          .map((deployment): {name: string; namespace: string} => ({
            name: deployment.name,
            namespace: deployment.namespace || 'default',
          }));

        task.output =
          `Cluster Reference: ${clusterReference}\n` +
          `Associated Context: ${context}\n` +
          'Deployments using this Cluster:';

        task.output +=
          deploymentsWithSelectedCluster.length > 0
            ? '\n' +
              deploymentsWithSelectedCluster
                .map(
                  (dep: {name: string; namespace: string}): string => `  - ${dep.name} [Namespace: ${dep.namespace}]`,
                )
                .join('\n')
            : '\n  - None';

        this.logger.showUser(task.output);
      },
    };
  }

  public installMinioOperator(): SoloListrTask<ClusterReferenceSetupContext> {
    return {
      title: 'Install MinIO Operator chart',
      task: async ({config: {clusterSetupNamespace, context}}): Promise<void> => {
        const {exists: isMinioInstalled}: ReleaseNameData = await this.findMinioOperator(context);

        if (isMinioInstalled) {
          this.logger.showUser(`⏭️  MinIO Operator chart already installed in context ${context}, skipping`);
          return;
        }

        try {
          await this.chartManager.install(
            clusterSetupNamespace,
            constants.MINIO_OPERATOR_RELEASE_NAME,
            constants.MINIO_OPERATOR_CHART,
            constants.MINIO_OPERATOR_CHART,
            versions.MINIO_OPERATOR_VERSION,
            '--set operator.replicaCount=1',
            context,
          );

          this.logger.showUser(`✅ MinIO Operator chart installed successfully on context ${context}`);
        } catch (error) {
          this.logger.debug('Error installing MinIO Operator chart', error);
          try {
            await this.chartManager.uninstall(clusterSetupNamespace, constants.MINIO_OPERATOR_RELEASE_NAME, context);
          } catch (uninstallError) {
            this.logger.showUserError(uninstallError);
          }
          throw new SoloError('Error installing MinIO Operator chart', error);
        }
      },
      skip: ({config: {deployMinio}}): boolean => !deployMinio,
    };
  }

  public installPrometheusStack(): SoloListrTask<ClusterReferenceSetupContext> {
    return {
      title: 'Install Prometheus Stack chart',
      task: async (context_): Promise<void> => {
        const clusterSetupNamespace: NamespaceName = context_.config.clusterSetupNamespace;

        const isPrometheusInstalled: boolean = await this.chartManager.isChartInstalled(
          clusterSetupNamespace,
          constants.PROMETHEUS_RELEASE_NAME,
          context_.config.context,
        );

        if (isPrometheusInstalled) {
          this.logger.showUser('⏭️  Prometheus Stack chart already installed, skipping');
        } else {
          try {
            await this.chartManager.install(
              clusterSetupNamespace,
              constants.PROMETHEUS_RELEASE_NAME,
              constants.PROMETHEUS_STACK_CHART,
              constants.PROMETHEUS_STACK_CHART,
              versions.PROMETHEUS_STACK_VERSION,
              '',
              context_.config.context,
            );
            this.logger.showUser('✅ Prometheus Stack chart installed successfully');
          } catch (error) {
            this.logger.debug('Error installing Prometheus Stack chart', error);
            try {
              await this.chartManager.uninstall(
                clusterSetupNamespace,
                constants.PROMETHEUS_RELEASE_NAME,
                context_.config.context,
              );
            } catch (uninstallError) {
              this.logger.showUserError(uninstallError);
            }
            throw new SoloError('Error installing Prometheus Stack chart', error);
          }
        }
      },
      skip: (context_: ClusterReferenceSetupContext): boolean => !context_.config.deployPrometheusStack,
    };
  }

  public installMetricsServer(): SoloListrTask<ClusterReferenceSetupContext> {
    return {
      title: 'Install metrics-server chart',
      task: async ({config: {context}}): Promise<void> => {
        const isMetricsServerInstalled: boolean = await this.chartManager.isChartInstalled(
          constants.METRICS_SERVER_NAMESPACE,
          constants.METRICS_SERVER_RELEASE_NAME,
          context,
        );

        if (isMetricsServerInstalled) {
          this.logger.showUser('⏭️  metrics-server chart already installed, skipping');
          return;
        }

        try {
          await this.chartManager.install(
            constants.METRICS_SERVER_NAMESPACE,
            constants.METRICS_SERVER_RELEASE_NAME,
            constants.METRICS_SERVER_CHART,
            constants.METRICS_SERVER_CHART,
            versions.METRICS_SERVER_VERSION,
            constants.METRICS_SERVER_INSTALL_ARGS,
            context,
          );
          this.logger.showUser('metrics-server chart installed successfully');
        } catch (error) {
          this.logger.debug('Error installing metrics-server chart', error);
          try {
            await this.chartManager.uninstall(
              constants.METRICS_SERVER_NAMESPACE,
              constants.METRICS_SERVER_RELEASE_NAME,
              context,
            );
          } catch (uninstallError) {
            this.logger.showUserError(uninstallError);
          }
          throw new SoloError('Error installing metrics-server chart', error);
        }
      },
      skip: ({config: {deployMetricsServer}}): boolean => !deployMetricsServer,
    };
  }

  public installPodMonitorRole(): SoloListrTask<ClusterReferenceSetupContext> {
    return {
      title: 'Install pod-monitor-role ClusterRole',
      task: async (context_: ClusterReferenceSetupContext): Promise<void> => {
        const k8: K8 = this.k8Factory.getK8(context_.config.context);

        try {
          // Check if ClusterRole already exists using Kubernetes JavaScript API
          await k8.rbac().clusterRoleExists(constants.POD_MONITOR_ROLE);
          this.logger.showUser(
            `⏭️  ClusterRole pod-monitor-role already exists in context ${context_.config.context}, skipping`,
          );
        } catch {
          // ClusterRole doesn't exist, create it
          try {
            await k8.rbac().createClusterRole(
              constants.POD_MONITOR_ROLE,
              [
                {
                  apiGroups: [''],
                  resources: ['pods', 'services', 'clusterroles', 'pods/log', 'secrets'],
                  verbs: ['get', 'list'],
                },
                {
                  apiGroups: [''],
                  resources: ['pods/exec'],
                  verbs: ['create'],
                },
              ],
              {'solo.hedera.com/type': 'cluster-role'},
            );
            this.logger.showUser(
              `✅ ClusterRole pod-monitor-role installed successfully in context ${context_.config.context}`,
            );
          } catch (installError) {
            this.logger.debug('Error installing pod-monitor-role ClusterRole', installError);
            throw new SoloError('Error installing pod-monitor-role ClusterRole', installError);
          }
        }
      },
    };
  }

  public uninstallPodMonitorRole(): SoloListrTask<ClusterReferenceResetContext> {
    return {
      title: 'Uninstall pod-monitor-role ClusterRole',
      task: async ({config: {context}}): Promise<void> => {
        try {
          // Check if ClusterRole exists using Kubernetes JavaScript API
          await this.k8Factory.getK8(context).rbac().clusterRoleExists(constants.POD_MONITOR_ROLE);

          // ClusterRole exists, delete it
          await this.k8Factory.getK8(context).rbac().deleteClusterRole(constants.POD_MONITOR_ROLE);
          this.logger.showUser('✅ ClusterRole pod-monitor-role uninstalled successfully');
        } catch {
          // ClusterRole doesn't exist, skip
          this.logger.showUser('⏭️  ClusterRole pod-monitor-role not found, skipping');
        }
      },
    };
  }

  public installClusterChart(argv: ArgvStruct): SoloListrTask<ClusterReferenceSetupContext> {
    return {
      title: 'Install cluster charts',
      task: async (context_, task): Promise<SoloListr<ClusterReferenceSetupContext>> => {
        // switch to the correct cluster context first
        const k8: K8 = this.k8Factory.getK8(context_.config.context);
        k8.contexts().updateCurrent(context_.config.context);

        // Always install pod-monitor-role ClusterRole first
        const subtasks: SoloListrTask<ClusterReferenceSetupContext>[] = [this.installPodMonitorRole()];

        if (context_.config.deployMinio) {
          subtasks.push(this.installMinioOperator());
        }

        if (context_.config.deployPrometheusStack) {
          subtasks.push(this.installPrometheusStack());
        }

        if (context_.config.deployMetricsServer) {
          subtasks.push(this.installMetricsServer());
        }

        const result: SoloListr<ClusterReferenceSetupContext> = await task.newListr(subtasks, {concurrent: false});

        if (argv.dev) {
          await this.showInstalledChartList(context_.config.clusterSetupNamespace, context_.config.context);
        }
        return result;
      },
    };
  }

  public acquireNewLease(): SoloListrTask<ClusterReferenceResetContext> {
    return {
      title: 'Acquire new lease',
      task: async (_, task): Promise<Listr<AnyListrContext>> => {
        if (!this.oneShotState.isActive()) {
          const lease: Lock = await this.leaseManager.create();
          return ListrLock.newAcquireLockTask(lease, task);
        }
        return ListrLock.newSkippedLockTask(task);
      },
    };
  }

  public uninstallMinioOperator(): SoloListrTask<ClusterReferenceResetContext> {
    return {
      title: 'Uninstall MinIO Operator chart',
      task: async ({config: {clusterSetupNamespace: namespace, context}}): Promise<void> => {
        const {exists: isMinioInstalled, releaseName}: ReleaseNameData = await this.findMinioOperator(context);

        if (isMinioInstalled) {
          await this.chartManager.uninstall(namespace, releaseName, context);

          this.logger.showUser('✅ MinIO Operator chart uninstalled successfully');
        } else {
          this.logger.showUser('⏭️  MinIO Operator chart not installed, skipping');
        }
      },
    };
  }

  public uninstallPrometheusStack(): SoloListrTask<ClusterReferenceResetContext> {
    return {
      title: 'Uninstall Prometheus Stack chart',
      task: async ({config: {clusterSetupNamespace, context}}): Promise<void> => {
        const isPrometheusInstalled: boolean = await this.chartManager.isChartInstalled(
          clusterSetupNamespace,
          constants.PROMETHEUS_RELEASE_NAME,
          context,
        );

        if (isPrometheusInstalled) {
          await this.chartManager.uninstall(clusterSetupNamespace, constants.PROMETHEUS_RELEASE_NAME, context);
          this.logger.showUser('✅ Prometheus Stack chart uninstalled successfully');
        } else {
          this.logger.showUser('⏭️  Prometheus Stack chart not installed, skipping');
        }
      },
    };
  }

  public uninstallMetricsServer(): SoloListrTask<ClusterReferenceResetContext> {
    return {
      title: 'Uninstall metrics-server chart',
      task: async ({config: {context}}): Promise<void> => {
        const isMetricsServerInstalled: boolean = await this.chartManager.isChartInstalled(
          constants.METRICS_SERVER_NAMESPACE,
          constants.METRICS_SERVER_RELEASE_NAME,
          context,
        );

        if (isMetricsServerInstalled) {
          await this.chartManager.uninstall(
            constants.METRICS_SERVER_NAMESPACE,
            constants.METRICS_SERVER_RELEASE_NAME,
            context,
          );
          this.logger.showUser('Metrics-server chart uninstalled successfully');
        } else {
          this.logger.showUser('Metrics-server chart not installed, skipping');
        }
      },
    };
  }

  public uninstallClusterChart(argv: ArgvStruct): SoloListrTask<ClusterReferenceResetContext> {
    return {
      title: 'Uninstall cluster charts',
      task: async (
        {config: {clusterSetupNamespace, context}},
        task,
      ): Promise<SoloListr<ClusterReferenceResetContext>> => {
        if (!argv.force && (await this.clusterChecks.isRemoteConfigPresentInAnyNamespace(context))) {
          const confirm: boolean = await task.prompt(ListrInquirerPromptAdapter).run(confirmPrompt, {
            default: false,
            message:
              'There is remote config for one of the deployments' +
              'Are you sure you would like to uninstall the cluster?',
          });

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

        if (argv.dev) {
          await this.showInstalledChartList(clusterSetupNamespace);
        }

        return task.newListr(
          [
            this.uninstallMetricsServer(),
            this.uninstallPrometheusStack(),
            this.uninstallMinioOperator(),
            this.uninstallPodMonitorRole(),
          ],
          {concurrent: false},
        );
      },
    };
  }
}
