import { GatewayLabels } from '@apic/studio-client-model';
import JSZip from 'jszip';
import yaml from 'js-yaml';
import { checkFileExtension } from '../utils.js';
import { isValidAsset, YamlContent, SchemaHandler, Logger, toError } from '@apic/studio-shared';
import { AssetValidator } from '../service/validation-service.js';

export class AssetSchemaValidator {
  private commonAssetKinds = [
    'API',
    'CORS',
    'Properties',
    'Scope',
    'URISchemes',
    'Product',
    'Plan',
    'Quota',
    'DataPowerAssembly',
  ];

  public async validateApiFile(
    buffer: Buffer,
    gatewayTypes: GatewayLabels[]
  ): Promise<{
    valid: boolean;
    errors: string[];
  }> {
    const errors: string[] = [];

    try {
      const { lwgwReferences, wmgwReferences, dpgwReferences, dpgwv5References } =
        await this.categorizePolicySequences(buffer);

      if (gatewayTypes.includes(GatewayLabels.WMGW) && wmgwReferences.size === 0) {
        const msg = 'WebMethods gateway is selected but no staged policy sequences are referenced';
        Logger.error(msg);
        errors.push(msg);
      }

      if (gatewayTypes.includes(GatewayLabels.LWGW) && lwgwReferences.size === 0) {
        const msg =
          'DataPower Nano gateway is selected but no free flow policy sequences are referenced';
        Logger.error(msg);
        errors.push(msg);
      }

      if (gatewayTypes.includes(GatewayLabels.DPGW) && dpgwReferences.size === 0) {
        const msg = 'DataPower API gateway is selected but no DataPowerAssembly is referenced';
        Logger.error(msg);
        errors.push(msg);
      }

      if (gatewayTypes.includes(GatewayLabels.DPv5GW) && dpgwv5References.size === 0) {
        const msg =
          'Publishing failed. The policy sequence is not compatible with the selected gateway.';
        Logger.error(msg);
        errors.push(msg);
      }

      return {
        valid: errors.length === 0,
        errors,
      };
    } catch (err) {
      const msg = `Error validating API file: ${err instanceof Error ? err.message : String(err)}`;
      Logger.error(msg);
      return { valid: false, errors: [msg] };
    }
  }

  private async readAndConsolidateYamlFiles(buffer: Buffer): Promise<YamlContent[]> {
    try {
      const zip = new JSZip();
      const zipContent = await zip.loadAsync(buffer);
      const validYamlContents: YamlContent[] = [];

      await Promise.all(
        Object.keys(zipContent.files).map(async (fileName) => {
          const entry = zipContent.files[fileName];

          if (!entry || entry.dir || !checkFileExtension(entry.name)) {
            return;
          }
          const content = await entry.async('string');

          try {
            const yamlContents = yaml.loadAll(content) as YamlContent[];
            for (const yamlContent of yamlContents) {
              if (isValidAsset(yamlContent, true)) {
                validYamlContents.push(yamlContent);
              } else {
                Logger.warn(`Invalid asset found in ${entry.name}`);
              }
            }
          } catch (yamlError) {
            Logger.error(`Error parsing YAML in ${entry.name}:`, toError(yamlError));
          }
        })
      );
      return validYamlContents;
    } catch (error) {
      Logger.error('Error processing buffer:', toError(error));
      return [];
    }
  }

  public async categorizePolicySequences(buffer: Buffer): Promise<{
    lwgwReferences: Set<string>;
    wmgwReferences: Set<string>;
    dpgwReferences: Set<string>;
    dpgwv5References: Set<string>;
  }> {
    const lwgwReferences = new Set<string>();
    const wmgwReferences = new Set<string>();
    const dpgwReferences = new Set<string>();
    const dpgwv5References = new Set<string>();

    const consolidatedYaml = await this.readAndConsolidateYamlFiles(buffer);

    const apiAssets = consolidatedYaml.filter(
      (asset) =>
        (asset.kind.toUpperCase() === 'API' || asset.kind.toUpperCase() === 'GLOBALPOLICY') &&
        asset.spec &&
        asset.spec['policy-sequence'] &&
        Array.isArray(asset.spec['policy-sequence']) &&
        asset.spec['policy-sequence'].length > 0
    );
    for (const apiAsset of apiAssets) {
      const firstPolicyRef = apiAsset.spec['policy-sequence'][0].$ref;
      if (typeof firstPolicyRef === 'string') {
        const parts = firstPolicyRef.split(':');
        const namespace = parts[0];
        const name = parts[1];
        const version = parts[2];

        if (!namespace || !name || !version) continue;

        const matchingAsset = consolidatedYaml.find(
          (asset) =>
            asset.metadata &&
            asset.metadata.namespace === namespace &&
            asset.metadata.name === name &&
            asset.metadata.version === version
        );

        if (matchingAsset) {
          if (matchingAsset.kind.toLowerCase() === 'freeflowpolicysequence') {
            lwgwReferences.add(firstPolicyRef);
            this.findAllNestedReferences(matchingAsset, consolidatedYaml, lwgwReferences);
          } else if (matchingAsset.kind.toLowerCase() === 'stagedpolicysequence') {
            wmgwReferences.add(firstPolicyRef);
            this.findAllNestedReferences(matchingAsset, consolidatedYaml, wmgwReferences);
          } else if (matchingAsset?.spec?.["x-ibm-configuration"]?.gateway === 'datapower-api-gateway') {
            dpgwReferences.add(firstPolicyRef);
            this.findAllNestedReferences(matchingAsset, consolidatedYaml, dpgwReferences);
          } else if (matchingAsset?.spec?.["x-ibm-configuration"]?.gateway === 'datapower-gateway') {
            dpgwv5References.add(firstPolicyRef);
          }
        }
      }
    }

    return { lwgwReferences, wmgwReferences, dpgwReferences, dpgwv5References };
  }

  private findAllNestedReferences(
    asset: YamlContent,
    allAssets: YamlContent[],
    referenceSet: Set<string>
  ): void {
    if (!asset || !asset.spec) return;

    const currentRefs = new Set<string>();
    this.findAllRefsRecursive(asset.spec, currentRefs);
    for (const ref of currentRefs) {
      if (!referenceSet.has(ref)) {
        referenceSet.add(ref);
        const parts = ref.split(':');
        const namespace = parts[0];
        const name = parts[1];
        const version = parts[1];

        if (!namespace || !name || !version) continue;
        const referencedAsset = allAssets.find(
          (a) =>
            a.metadata &&
            a.metadata.namespace === namespace &&
            a.metadata.name === name &&
            a.metadata.version === version
        );
        if (referencedAsset) {
          this.findAllNestedReferences(referencedAsset, allAssets, referenceSet);
        }
      }
    }
  }

  private findAllRefsRecursive(data: any, referenceSet: Set<string>): void {
    if (data == null) return;

    if (Array.isArray(data)) {
      for (const item of data) {
        this.findAllRefsRecursive(item, referenceSet);
      }
    } else if (typeof data === 'object') {
      for (const key of Object.keys(data)) {
        const value = data[key];
        if (key === '$ref' && typeof value === 'string') {
          referenceSet.add(value);
        } else {
          this.findAllRefsRecursive(value, referenceSet);
        }
      }
    }
  }

  public async validateSchema(
    buffer: Buffer,
    gatewayTypes: GatewayLabels[]
  ): Promise<{
    valid: boolean;
    errors: string[];
  }> {
    const apiValidationResult = await this.validateApiFile(buffer, gatewayTypes);

    if (!apiValidationResult.valid) {
      return apiValidationResult;
    }

    const errors: string[] = [];

    try {
      const allAssets = await this.readAndConsolidateYamlFiles(buffer);
      const commonSchemaHandler = new SchemaHandler();
      const commonValidator = new AssetValidator(commonSchemaHandler);
      for (const asset of allAssets) {
        if (asset.kind && this.commonAssetKinds.includes(asset.kind)) {
          const validationResult = commonValidator.validateAssets(asset);

          if (!validationResult.valid) {
            errors.push(...validationResult.errors);
          }
        }
      }

      if (
        gatewayTypes.includes(GatewayLabels.WMGW) &&
        gatewayTypes.includes(GatewayLabels.LWGW) &&
        gatewayTypes.includes(GatewayLabels.DPGW)
      ) {
        await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.WMGW, errors, buffer);
        await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.LWGW, errors, buffer);
        await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.DPGW, errors, buffer);
      } else if (gatewayTypes.includes(GatewayLabels.WMGW)) {
        await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.WMGW, errors, buffer);
      } else if (gatewayTypes.includes(GatewayLabels.LWGW)) {
        await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.LWGW, errors, buffer);
      } else if (gatewayTypes.includes(GatewayLabels.DPGW)) {
        await this.validateGatewaySpecificAssets(allAssets, GatewayLabels.DPGW, errors, buffer);
      }

      return {
        valid: errors.length === 0,
        errors,
      };
    } catch (err) {
      const msg = `Error validating schema: ${err instanceof Error ? err.message : String(err)}`;
      Logger.error(msg);
      return { valid: false, errors: [msg] };
    }
  }

  private async validateGatewaySpecificAssets(
    assets: YamlContent[],
    gatewayType: GatewayLabels,
    errors: string[],
    buffer: Buffer
  ): Promise<void> {
    const gatewayLabel =
      gatewayType === GatewayLabels.WMGW
        ? 'webMethods'
        : gatewayType === GatewayLabels.LWGW
          ? 'nano'
          : gatewayType === GatewayLabels.DPGW
            ? 'datapower'
            : undefined;

    if (!gatewayLabel) return;

    const { lwgwReferences, wmgwReferences, dpgwReferences } =
      await this.categorizePolicySequences(buffer);
    const referenceSet =
      gatewayType === GatewayLabels.WMGW
        ? wmgwReferences
        : gatewayType === GatewayLabels.LWGW
          ? lwgwReferences
          : dpgwReferences;

    const schemaHandler = new SchemaHandler(gatewayLabel);
    const validator = new AssetValidator(schemaHandler);

    const gatewaySpecificAssets = assets.filter((asset) => {
      return !this.commonAssetKinds.includes(asset.kind);
    });

    for (const asset of gatewaySpecificAssets) {
      if (!asset.kind || !asset.metadata) continue;
      const assetRef = `${asset.metadata.namespace}:${asset.metadata.name}:${asset.metadata.version}`;
      if (referenceSet.has(assetRef)) {
        const validationResult = validator.validateAssets(asset);

        if (!validationResult.valid) {
          const gatewayErrors = validationResult.errors.map((err) => `${err}`);
          errors.push(...gatewayErrors);
        }
      }
    }
  }
}
