// SPDX-License-Identifier: Apache-2.0

import {
  type CoreV1Api,
  PatchStrategy,
  setHeaderOptions,
  V1ConfigMap,
  type V1ConfigMapList,
  V1ObjectMeta,
} from '@kubernetes/client-node';
import {type ConfigMaps} from '../../../resources/config-map/config-maps.js';
import {type NamespaceName} from '../../../../../types/namespace/namespace-name.js';
import {ResourceNotFoundError} from '../../../errors/resource-operation-errors.js';
import {ResourceType} from '../../../resources/resource-type.js';
import {ResourceOperation} from '../../../resources/resource-operation.js';
import {SoloError} from '../../../../../core/errors/solo-error.js';
import {type SoloLogger} from '../../../../../core/logging/solo-logger.js';
import {container} from 'tsyringe-neo';
import {type ConfigMap} from '../../../resources/config-map/config-map.js';
import {K8ClientConfigMap} from './k8-client-config-map.js';
import {InjectTokens} from '../../../../../core/dependency-injection/inject-tokens.js';
import {KubeApiResponse} from '../../../kube-api-response.js';

export class K8ClientConfigMaps implements ConfigMaps {
  private readonly logger: SoloLogger;

  public constructor(private readonly kubeClient: CoreV1Api) {
    this.logger = container.resolve(InjectTokens.SoloLogger);
  }

  public async create(
    namespace: NamespaceName,
    name: string,
    labels: Record<string, string>,
    data: Record<string, string>,
  ): Promise<boolean> {
    return await this.createOrReplaceWithForce(namespace, name, labels, data, false, true);
  }

  public async createOrReplace(
    namespace: NamespaceName,
    name: string,
    labels: Record<string, string>,
    data: Record<string, string>,
  ): Promise<boolean> {
    return await this.createOrReplaceWithForce(namespace, name, labels, data, false, false);
  }

  public async delete(namespace: NamespaceName, name: string): Promise<boolean> {
    try {
      await this.kubeClient.deleteNamespacedConfigMap({
        name,
        namespace: namespace.name,
      });
      return true;
    } catch (error) {
      return KubeApiResponse.isFailingStatus(error);
    }
  }

  public async read(namespace: NamespaceName, name: string): Promise<ConfigMap> {
    try {
      const body: V1ConfigMap = await this.kubeClient.readNamespacedConfigMap({name, namespace: namespace?.name});
      return K8ClientConfigMap.fromV1ConfigMap(body);
    } catch (error) {
      KubeApiResponse.throwError(error, ResourceOperation.READ, ResourceType.CONFIG_MAP, namespace, name);
    }
  }

  public async replace(
    namespace: NamespaceName,
    name: string,
    labels: Record<string, string>,
    data: Record<string, string>,
  ): Promise<boolean> {
    return await this.createOrReplaceWithForce(namespace, name, labels, data, true, false);
  }

  public async exists(namespace: NamespaceName, name: string): Promise<boolean> {
    try {
      const cm: ConfigMap = await this.read(namespace, name);
      return !!cm;
    } catch (error) {
      if (error instanceof ResourceNotFoundError) {
        return false;
      } else {
        throw error;
      }
    }
  }

  private async createOrReplaceWithForce(
    namespace: NamespaceName,
    name: string,
    labels: Record<string, string>,
    data: Record<string, string>,
    forceReplace?: boolean,
    forceCreate?: boolean,
  ): Promise<boolean> {
    const replace: boolean = await this.shouldReplace(namespace, name, forceReplace, forceCreate);
    const configMap: V1ConfigMap = new V1ConfigMap();
    configMap.data = data;

    const metadata: V1ObjectMeta = new V1ObjectMeta();
    metadata.name = name;
    metadata.namespace = namespace.name;
    metadata.labels = labels;
    configMap.metadata = metadata;
    try {
      await (replace
        ? this.kubeClient.replaceNamespacedConfigMap({name, namespace: namespace.name, body: configMap})
        : this.kubeClient.createNamespacedConfigMap({namespace: namespace.name, body: configMap}));
      return true;
    } catch (error) {
      KubeApiResponse.throwError(
        error,
        replace ? ResourceOperation.REPLACE : ResourceOperation.CREATE,
        ResourceType.CONFIG_MAP,
        namespace,
        name,
      );
    }
  }

  private async shouldReplace(
    namespace: NamespaceName,
    name: string,
    forceReplace?: boolean,
    forceCreate?: boolean,
  ): Promise<boolean> {
    if (forceReplace && !forceCreate) {
      return true;
    }

    if (forceCreate) {
      return false;
    }

    return await this.exists(namespace, name);
  }

  public async list(namespace: NamespaceName, labels: string[]): Promise<ConfigMap[]> {
    const labelSelector: string = labels ? labels.join(',') : undefined;

    let results: V1ConfigMapList;
    try {
      results = await this.kubeClient.listNamespacedConfigMap({
        namespace: namespace.name,
        labelSelector,
      });
    } catch (error) {
      KubeApiResponse.throwError(error, ResourceOperation.LIST, ResourceType.CONFIG_MAP, namespace, '');
    }

    return results?.items?.map((v1ConfigMap): ConfigMap => K8ClientConfigMap.fromV1ConfigMap(v1ConfigMap)) || [];
  }

  public async listForAllNamespaces(labels: string[]): Promise<ConfigMap[]> {
    const labelSelector: string = labels ? labels.join(',') : undefined;

    let results: V1ConfigMapList;
    try {
      results = await this.kubeClient.listConfigMapForAllNamespaces({labelSelector});
    } catch (error) {
      KubeApiResponse.throwError(error, ResourceOperation.LIST, ResourceType.CONFIG_MAP, undefined, '');
    }

    return results?.items?.map((v1ConfigMap): ConfigMap => K8ClientConfigMap.fromV1ConfigMap(v1ConfigMap)) || [];
  }

  public async update(namespace: NamespaceName, name: string, data: Record<string, string>): Promise<void> {
    if (!(await this.exists(namespace, name))) {
      throw new ResourceNotFoundError(ResourceOperation.READ, ResourceType.CONFIG_MAP, namespace, name);
    }

    const patch: {data: Record<string, string>} = {
      data: data,
    };

    let result: V1ConfigMap;
    try {
      result = await this.kubeClient.patchNamespacedConfigMap(
        {
          name,
          namespace: namespace.name,
          body: patch,
        },
        setHeaderOptions('Content-Type', PatchStrategy.MergePatch),
      );
      this.logger.info(`Patched ConfigMap ${name} in namespace ${namespace}`);
    } catch (error) {
      KubeApiResponse.throwError(error, ResourceOperation.UPDATE, ResourceType.CONFIG_MAP, namespace, name);
    }

    if (result) {
      return;
    } else {
      throw new SoloError(
        `Failed to patch ConfigMap ${name} in namespace ${namespace}, no config map returned from patch`,
      );
    }
  }
}
