/**
 * Copyright IBM Corp. 2024, 2025
 */
import JSZip, { JSZipObject } from 'jszip';
import yaml from 'js-yaml';
import { Api_Spec_Ref, SpecObject, ReferenceValidationResult } from '../model/interface.js';
import {
  addErrorToResponse,
  isValidAsset,
  processRef,
  updateMapWithMetadata,
  validateMinAssets,
  checkFileExtension,
  errorsArray,
  validateSoapGatewayRestriction,
} from '../utils.js';
import { YamlContent, Logger } from '@apic/studio-shared';
import { AppConstants } from '../constants/app.constants.js';
import path from 'path';
import { BuildProjectAssets } from '../build-project-assets.js';

export class ProjectAssetValidator {
  private isYamlFileForFolder(entry: JSZipObject, folderName: string): boolean {
    return (
      !entry.dir && entry.name.startsWith(folderName + path.sep) && checkFileExtension(entry.name)
    );
  }

  private async loadZipFromBuffer(fileBuffer: Buffer): Promise<JSZip> {
    Logger.info('Loading ZIP from buffer');
    const zip = new JSZip();
    return zip.loadAsync(fileBuffer);
  }

  public async validateAssetUniqueness(fileBuffer: Buffer): Promise<boolean> {
    const zipContent = await this.loadZipFromBuffer(fileBuffer);
    const newMap = new Map<string, { fileName: string; kind: string }>();
    let allValid = true;

    await Promise.all(
      Object.keys(zipContent.files).map(async (fileName) => {
        const entry = zipContent.files[fileName];
        if (entry && checkFileExtension(entry.name)) {
          const content = await entry.async('string');
          const yamlContents = yaml.loadAll(content) as YamlContent[];

          for (const yamlContent of yamlContents) {
            if (isValidAsset(yamlContent)) {
              const namespace = yamlContent.metadata.namespace || 'default';
              const name = yamlContent.metadata.name;
              const version = yamlContent.metadata.version;
              const kind = yamlContent.kind;

              const ref = `${namespace}:${name}:${version}`;

              if (newMap.has(ref)) {
                const existing = newMap.get(ref);
                addErrorToResponse(
                  AppConstants.VALIDATION_ERROR_CODE,
                  fileName,
                  `Duplicate asset detected: '${name}' in namespace '${namespace}' with version '${version}' and kind '${kind}'. First seen in file '${existing?.fileName}'.`
                );
                allValid = false;
              } else {
                newMap.set(ref, { fileName, kind });
              }
            }
          }
        }
      })
    );

    return allValid;
  }

  private async createProjectAssetReferenceMap(
    buffer: Buffer,
    folderName: string,
    allFolderNames: Set<string>
  ): Promise<Map<string, boolean>> {
    Logger.info(`Creating asset reference map for folder: ${folderName}`);
    const zipContent = await this.loadZipFromBuffer(buffer);
    const refMap = new Map<string, boolean>();

    try {
      const obj = new BuildProjectAssets();
      const versionMap = await obj.createVersionProcessingMap(buffer);
      await this.processYamlFiles(zipContent, folderName, refMap, versionMap);
      await this.processYamlFiles(zipContent, 'dependencies', refMap, versionMap, false);

      // if unresolved refs are identified after checking in the project and dependencies folders then check in other folders
      const hasUnresolvedRefs = Array.from(refMap.values()).some((value) => !value);
      if (hasUnresolvedRefs) {
        for (const otherFolderName of allFolderNames) {
          if (otherFolderName !== folderName && otherFolderName !== 'dependencies') {
            await this.processYamlFiles(zipContent, otherFolderName, refMap, versionMap, false);
          }
        }
      }
      Logger.info('Successfully processed YAML files');
    } catch (err) {
      Logger.error('Error processing ZIP', err instanceof Error ? err : new Error(String(err)));
    }
    return refMap;
  }

  private async processYamlFiles(
    zipContent: JSZip,
    folderName: string,
    refMap: Map<string, boolean>,
    versionMap: Map<string, boolean>,
    processDependencies = true
  ): Promise<void> {
    Logger.info(`Processing YAML files in folder: ${folderName}`);
    await Promise.all(
      Object.keys(zipContent.files).map(async (fileName) => {
        const entry = zipContent.files[fileName];
        if (entry && this.isYamlFileForFolder(entry, folderName)) {
          await this.processYamlFile(entry, refMap, processDependencies, versionMap);
        }
      })
    );
  }

  private async processYamlFile(
    entry: JSZipObject,
    refMap: Map<string, boolean>,
    processDependencies: boolean,
    versionMap: Map<string, boolean>
  ): Promise<void> {
    Logger.info(`Processing YAML file: ${entry.name}`);
    const content = await entry.async('string');
    try {
      const yamlContents = yaml.loadAll(content) as YamlContent[];
      for (const yamlContent of yamlContents) {
        if (
          isValidAsset(yamlContent) &&
          !AppConstants.IGNORE_ASSETS_DURING_DEPLOY.includes(yamlContent.kind.toLowerCase())
        ) {
          this.updateReferenceMap(yamlContent, refMap, processDependencies, versionMap);
        }
      }
      Logger.info(`Successfully processed YAML content in file: ${entry.name}`);
    } catch (err) {
      Logger.error(
        `Error parsing YAML in file ${entry.name}`,
        err instanceof Error ? err : new Error(String(err))
      );
    }
  }

  private updateReferenceMap(
    yamlContent: YamlContent,
    refMap: Map<string, boolean>,
    processDependencies: boolean,
    versionMap: Map<string, boolean>
  ): void {
    if (processDependencies) {
      this.extractKey(yamlContent, refMap, '$ref', versionMap, processRef);
    }
    updateMapWithMetadata(yamlContent, refMap);
  }

  private extractKey(
    yamlContent: YamlContent,
    refMap: Map<string, boolean>,
    keyToExtract: string,
    versionMap: Map<string, boolean>,
    transformValue?: (value: string) => string
  ): void {
    const extract = (obj: SpecObject) => {
      for (const key in obj) {
        const value = obj[key];
        if (key === keyToExtract && typeof value === 'string') {
          const transformedValue = transformValue ? transformValue(value) : value;
          if (versionMap.has(value)) {
            if (!refMap.has(value)) {
              refMap.set(value, true);
            }
          } else {
            if (!refMap.has(transformedValue)) {
              refMap.set(transformedValue, false);
            }
          }
        } else if (typeof value === 'object' && value !== null) {
          extract(value);
        }
      }
    };
    const specOb = JSON.stringify(yamlContent.spec);
    extract(yaml.load(specOb) as SpecObject);
  }

  public async createProjectPathReferenceMap(
    buffer: Buffer,
    folderName: string
  ): Promise<Map<string, boolean>> {
    Logger.info(`Creating path reference map for folder: ${folderName}`);
    const zipContent = await this.loadZipFromBuffer(buffer);
    const refMap = new Map<string, boolean>();

    try {
      const obj = new BuildProjectAssets();
      const versionMap = await obj.createVersionProcessingMap(buffer);
      for (const fileName in zipContent.files) {
        const entry = zipContent.files[fileName];
        if (entry && this.isYamlFileForFolder(entry, folderName)) {
          const content = await entry.async('string');
          try {
            const yamlContents = yaml.loadAll(content) as YamlContent[];
            for (const yamlContent of yamlContents) {
              if (isValidAsset(yamlContent)) {
                this.extractKey(yamlContent, refMap, '$path', versionMap, path.normalize);
              }
            }
            Logger.info(`Path extraction completed for file: ${fileName}`);
          } catch (err) {
            Logger.error(
              `Error parsing YAML in file ${fileName}`,
              err instanceof Error ? err : new Error(String(err))
            );
          }
        }
      }
    } catch (err) {
      Logger.error('Error loading ZIP', err instanceof Error ? err : new Error(String(err)));
    }

    return refMap;
  }
  private async validateApiSpecVaraible(buffer: Buffer, folderName: string): Promise<boolean> {
    Logger.info(`Validating API Spec variable in folder: ${folderName}`);
    const zipContent = await this.loadZipFromBuffer(buffer);
    for (const fileName in zipContent.files) {
      const entry = zipContent.files[fileName];
      if (
        entry &&
        !entry.dir &&
        fileName.startsWith(folderName + path.sep) &&
        checkFileExtension(fileName)
      ) {
        const content = await entry.async('string');
        const isValid = await this.checkYamlContent(content, fileName);
        if (!isValid) {
          return false;
        }
      }
    }
    return true;
  }

  private async checkYamlContent(content: string, fileName: string): Promise<boolean> {
    Logger.info(`Checking YAML content in file: ${fileName}`);
    try {
      const yamlContents = yaml.loadAll(content) as YamlContent[];
      for (const yamlContent of yamlContents) {
        if (isValidAsset(yamlContent) && yamlContent.kind?.toLowerCase() === 'api') {
          const specObj = JSON.stringify(yamlContent.spec);
          const apiSpec = yaml.load(specObj) as Api_Spec_Ref;
          if (this.isInvalidApiSpec(apiSpec)) {
            addErrorToResponse(
              AppConstants.VALIDATION_ERROR_CODE,
              fileName,
              `Validation failed for api - spec field in file ${fileName} `
            );
            return false;
          }
        }
      }
      Logger.info(`Successfully validated YAML content in file: ${fileName}`);
    } catch (err) {
      Logger.error(
        `Error parsing YAML in file ${fileName}`,
        err instanceof Error ? err : new Error(String(err))
      );
      return false;
    }
    return true;
  }

  private isInvalidApiSpec(apiSpec: Api_Spec_Ref): boolean {
    const apiSpecField = AppConstants.apiSpec;
    const apiSpecPathLength = apiSpec[apiSpecField]?.$path?.length ?? 0;
    return (
      !apiSpec || !apiSpec[apiSpecField] || !apiSpec[apiSpecField].$path || apiSpecPathLength <= 0
    );
  }

  public async validateProjectAssetReference(
    buffer: Buffer,
    folderName: string,
    allFolderNames: Set<string>
  ): Promise<ReferenceValidationResult> {
    Logger.info(`Validating project asset references in folder: ${folderName}`);
    let refMap = new Map<string, boolean>();
    try {
      refMap = await this.createProjectAssetReferenceMap(buffer, folderName, allFolderNames);
      const allRefsValid = Array.from(refMap.entries()).every(([key, value]) => {
        if (!value) {
          addErrorToResponse(
            AppConstants.VALIDATION_ERROR_CODE,
            key,
            `Validation failed for reference ${key}`
          );
        }
        return value;
      });
      if (!allRefsValid) {
        Logger.error('Some references are not valid');
        return {
          isValid: false,
          refMap,
          errors: errorsArray.map((err) => err.description),
        };
      }
      return { isValid: true, refMap, errors: [] };
    } catch (err) {
      Logger.error('Error validating asset', err instanceof Error ? err : new Error(String(err)));
      return {
        isValid: false,
        refMap,
        errors: errorsArray.map((err) => err.description),
      };
    }
  }

  public async validateProjectPathReference(
    buffer: Buffer,
    folderName: string,
    filePathsInFolder: Set<string>
  ): Promise<boolean> {
    Logger.info(`Validating project path references in folder: ${folderName}`);
    try {
      const refMap = await this.createProjectPathReferenceMap(buffer, folderName);
      refMap.forEach((_, key) => {
        if (filePathsInFolder.has(path.normalize(`${folderName}/${key}`))) {
          refMap.set(key, true);
        }
      });
      const allRefsValid = Array.from(refMap.entries()).every(([key, value]) => {
        if (!value) {
          Logger.error(`Validation failed for path ${key} in ${folderName}`);
          addErrorToResponse(
            AppConstants.VALIDATION_ERROR_CODE,
            key,
            `Validation failed for path ${key} in ${folderName}`
          );
        }
        return value;
      });
      if (!allRefsValid) {
        Logger.error('Some references are not valid');
        return false;
      }
      return true;
    } catch (err) {
      Logger.error('Error validating asset', err instanceof Error ? err : new Error(String(err)));
      return false;
    }
  }

  public async validateDeploymentAsset(buffer: Buffer) {
    try {
      const zipContent = await this.loadZipFromBuffer(buffer);
      Logger.debug(`Processing ${Object.keys(zipContent.files).length} files`);

      for (const fileName in zipContent.files) {
        const entry = zipContent.files[fileName];
        if (entry && checkFileExtension(fileName)) {
          const content = await entry.async('string');
          const yamlContents = yaml.loadAll(content) as YamlContent[];
          for (const yamlContent of yamlContents) {
            if (isValidAsset(yamlContent)) {
              return true;
            }
          }
        }
      }
    } catch (err) {
      addErrorToResponse(
        AppConstants.VALIDATION_ERROR_CODE,
        'ZIP_FILE',
        `Error loading zip with minimum assets: ${err}`
      );
    }
    addErrorToResponse(
      AppConstants.VALIDATION_ERROR_CODE,
      'ZIP_FILE',
      'Error loading zip with minimum assets required for deploymnet'
    );
    return false;
  }

  public async validateProjectHasMinimumAssets(buffer: Buffer) {
    Logger.info('Validating project has minimum assets');
    return (await validateMinAssets(buffer)) && (await this.validateDeploymentAsset(buffer));
  }

  public async validateProjectApiSpecVariable(buffer: Buffer, folderName: string) {
    Logger.info(`Validating project API spec variables in folder: ${folderName}`);
    return this.validateApiSpecVaraible(buffer, folderName);
  }

  public async validateSoapApiGatewayRestriction(buffer: Buffer): Promise<boolean> {
    Logger.info('Validating SOAP API gateway restrictions');
    const result = await validateSoapGatewayRestriction(buffer);

    if (!result.isValid && result.errors.length > 0) {
      result.errors.forEach((error) => {
        addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'SOAP_API_TO_GATEWAY', error);
      });
    }

    return result.isValid;
  }
}
