// SPDX-License-Identifier: Apache-2.0

import * as constants from './constants.js';
import chalk from 'chalk';
import {SoloError} from './errors/solo-error.js';
import {type SoloLogger} from './logging/solo-logger.js';
import {inject, injectable} from 'tsyringe-neo';
import {patchInject} from './dependency-injection/container-helper.js';
import {type NamespaceName} from '../types/namespace/namespace-name.js';
import {InjectTokens} from './dependency-injection/inject-tokens.js';
import {Repository} from '../integration/helm/model/repository.js';
import {type ReleaseItem} from '../integration/helm/model/release/release-item.js';
import {UpgradeChartOptions} from '../integration/helm/model/upgrade/upgrade-chart-options.js';
import {Chart} from '../integration/helm/model/chart.js';
import {type InstallChartOptions} from '../integration/helm/model/install/install-chart-options.js';
import {InstallChartOptionsBuilder} from '../integration/helm/model/install/install-chart-options-builder.js';
import {type HelmClient} from '../integration/helm/helm-client.js';
import {UnInstallChartOptionsBuilder} from '../integration/helm/model/install/un-install-chart-options-builder.js';
import {AddRepoOptionsBuilder} from '../integration/helm/model/add/add-repo-options-builder.js';
import {AddRepoOptions} from '../integration/helm/model/add/add-repo-options.js';
import {UnInstallChartOptions} from '../integration/helm/model/install/un-install-chart-options.js';

@injectable()
export class ChartManager {
  public constructor(
    @inject(InjectTokens.Helm) private readonly helm?: HelmClient,
    @inject(InjectTokens.SoloLogger) private readonly logger?: SoloLogger,
  ) {
    this.helm = patchInject(helm, InjectTokens.Helm, this.constructor.name);
    this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
  }

  /**
   * Setup chart repositories
   *
   * This must be invoked before calling other methods
   *
   * @param repoURLs - a map of name and chart repository URLs
   * @param force - whether or not to update the repo
   * @returns the urls
   */
  public async setup(
    repoURLs: Map<string, string> = constants.DEFAULT_CHART_REPO,
    force: boolean = true,
  ): Promise<string[]> {
    try {
      const promises: Promise<string>[] = [];
      for (const [name, url] of repoURLs.entries()) {
        this.logger.debug(`pushing promise for: add repo ${name} -> ${url}`);
        promises.push(this.addRepo(name, url, force));
      }

      const urls: string[] = await Promise.all(promises); // urls
      await this.helm.updateRepositories();
      return urls;
    } catch (error) {
      throw new SoloError(`failed to setup chart repositories: ${error.message}`, error);
    }
  }

  /**
   * Check if the required chart repositories are set up
   *
   * @param repoURLs - a map of name and chart repository URLs
   * @returns true if all repos are set up, false otherwise
   */
  public async isSetup(repoURLs: Map<string, string> = constants.DEFAULT_CHART_REPO): Promise<boolean> {
    try {
      const existingRepos: Repository[] = await this.helm.listRepositories();
      for (const [name, url] of repoURLs.entries()) {
        const found: Repository = existingRepos.find(
          (repo: Repository): boolean => repo.name === name && repo.url === url,
        );
        if (!found) {
          this.logger.debug(`Repo not found: ${name} -> ${url}`);
          return false;
        }
      }
      return true;
    } catch (error) {
      throw new SoloError(`failed to check chart repositories: ${error.message}`, error);
    }
  }

  public async addRepo(name: string, url: string, force: boolean): Promise<string> {
    // detect if repo already exists for name provided and the url matches, if so, exit, otherwise force update
    const repositories: Repository[] = await this.helm.listRepositories();
    const existingRepo: Repository | undefined = repositories.find((repo): boolean => repo.name === name);
    if (existingRepo) {
      if (existingRepo.url === url) {
        this.logger.debug(`Repo already exists: ${name} -> ${url}`);
        return url;
      }
      this.logger.debug(`Repo URL mismatch for ${name}: existing URL is ${existingRepo.url}, new URL is ${url}`);
    }
    this.logger.debug(`Adding repo ${name} -> ${url}`, {repoName: name, repoURL: url});
    const options: AddRepoOptions = new AddRepoOptionsBuilder().forceUpdate(force).build();
    await this.helm.addRepository(new Repository(name, url), options);
    return url;
  }

  /** List available clusters
   *
   * @param namespaceName - the namespace name
   * @param kubeContext - the kube context
   */
  public async getInstalledCharts(namespaceName: NamespaceName, kubeContext?: string): Promise<string[]> {
    try {
      const result: ReleaseItem[] = await this.helm.listReleases(!namespaceName, namespaceName?.name, kubeContext);
      // convert to string[]
      return result.map((release): string => `${release.name} [${release.chart}]`);
    } catch (error) {
      this.logger.showUserError(error);
      throw new SoloError(`failed to list installed charts: ${error.message}`, error);
    }
  }

  public async install(
    namespaceName: NamespaceName,
    chartReleaseName: string,
    chartName: string,
    repoName: string,
    version: string,
    valuesArgument: string = '',
    kubeContext: string,
    atomic: boolean = false,
    waitFor: boolean = false,
  ): Promise<boolean> {
    try {
      const isInstalled: boolean = await this.isChartInstalled(namespaceName, chartReleaseName, kubeContext);
      if (isInstalled) {
        this.logger.debug(`OK: chart is already installed:${chartReleaseName} (${chartName}) (${repoName})`);
      } else {
        this.logger.debug(`> installing chart:${chartName}`);
        const builder: InstallChartOptionsBuilder = InstallChartOptionsBuilder.builder()
          .version(version)
          .kubeContext(kubeContext)
          .atomic(atomic)
          .waitFor(waitFor)
          .extraArgs(valuesArgument);
        if (namespaceName) {
          builder.createNamespace(true);
          builder.namespace(namespaceName.name);
        }
        const options: InstallChartOptions = builder.build();
        await this.helm.installChart(chartReleaseName, new Chart(chartName, repoName), options);
        this.logger.debug(`OK: chart is installed: ${chartReleaseName} (${chartName}) (${repoName})`);
      }
    } catch (error) {
      throw new SoloError(`failed to install chart ${chartReleaseName}: ${error.message}`, error);
    }

    return true;
  }

  public async isChartInstalled(
    namespaceName: NamespaceName,
    chartReleaseName: string,
    kubeContext?: string,
  ): Promise<boolean> {
    this.logger.debug(
      `> checking if chart is installed [ chart: ${chartReleaseName}, namespace: ${namespaceName}, kubeContext: ${kubeContext} ]`,
    );
    const charts: string[] = await this.getInstalledCharts(namespaceName, kubeContext);

    let match: boolean = false;
    for (const chart of charts) {
      if (chart.split(' ')[0] === chartReleaseName) {
        match = true;
        break;
      }
    }

    return match;
  }

  public async uninstall(
    namespaceName: NamespaceName,
    chartReleaseName: string,
    kubeContext?: string,
  ): Promise<boolean> {
    try {
      const isInstalled: boolean = await this.isChartInstalled(namespaceName, chartReleaseName, kubeContext);
      if (isInstalled) {
        this.logger.debug(`uninstalling chart release: ${chartReleaseName}`);
        const options: UnInstallChartOptions = UnInstallChartOptionsBuilder.builder()
          .namespace(namespaceName.name)
          .kubeContext(kubeContext)
          .build();
        await this.helm.uninstallChart(chartReleaseName, options);
        this.logger.debug(`OK: chart release is uninstalled: ${chartReleaseName}`);
      } else {
        this.logger.debug(`OK: chart release is already uninstalled: ${chartReleaseName}`);
      }
    } catch (error) {
      throw new SoloError(`failed to uninstall chart ${chartReleaseName}: ${error.message}`, error);
    }

    return true;
  }

  public async upgrade(
    namespaceName: NamespaceName,
    chartReleaseName: string,
    chartName: string,
    repoName: string,
    version: string = '',
    valuesArgument: string = '',
    kubeContext?: string,
    reuseValues?: boolean,
  ): Promise<boolean> {
    try {
      this.logger.debug(chalk.cyan('> upgrading chart:'), chalk.yellow(`${chartReleaseName}`));
      const options: UpgradeChartOptions = new UpgradeChartOptions(
        namespaceName?.name,
        kubeContext,
        reuseValues ?? true,
        valuesArgument,
        version,
      );
      const chart: Chart = new Chart(chartName, repoName);
      await this.helm.upgradeChart(chartReleaseName, chart, options);
      this.logger.debug(chalk.green('OK'), `chart '${chartReleaseName}' is upgraded`);
    } catch (error) {
      throw new SoloError(`failed to upgrade chart ${chartReleaseName}: ${error.message}`, error);
    }

    return true;
  }
}
