/**
 * Copyright IBM Corp. 2024, 2025
 */
import yaml from 'js-yaml';
import JSZip from 'jszip';
import { ErrorResponse, SpecObject } from './model/interface.js';
import { AppConstants } from './constants/app.constants.js';
import path from 'path';
import { GatewayLabels, YamlContent } from '@apic/studio-shared';
import { Logger } from '@apic/studio-shared';

export const createAssetReferenceMap = async (buffer: Buffer): Promise<Map<string, boolean>> => {
  const zip = new JSZip();
  const refMap = new Map<string, boolean>();

  try {
    const zipContent = await zip.loadAsync(buffer);
    //logDebug('0308', `${Object.keys(zipContent.files).length}`);

    for (const fileName in zipContent.files) {
      const entry = zipContent.files[fileName];
      if (
        entry &&
        !entry.dir &&
        (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) &&
        !fileName.includes('resources')
      ) {
        const content = await entry.async('string');
        //logInfo('0353', fileName);

        try {
          const yamlContents = yaml.loadAll(content) as YamlContent[];
          for (const yamlContent of yamlContents) {
            extractRefs(yamlContent, refMap);
            updateMapWithMetadata(yamlContent, refMap);
          }
        } catch {
          //logError('0013', 'parsing YAML', `${err}`);
        }
      }
    }
  } catch {
    //logError('0013', 'loading ZIP', `${err}`);
  }

  return refMap;
};

export const extractRefs = (yamlContent: YamlContent, refMap: Map<string, boolean>): void => {
  const extractRef = (obj: SpecObject) => {
    for (const key in obj) {
      const value = obj[key];
      if (key === '$ref' && typeof value === 'string') {
        if (!refMap.has(value)) {
          refMap.set(value, false);
          //logDebug('0309', value);
        }
      } else if (typeof value === 'object' && value !== null) {
        extractRef(value);
      }
    }
  };
  const specOb = JSON.stringify(yamlContent.spec);
  extractRef(yaml.load(specOb) as SpecObject);
};

export const updateMapWithMetadata = (
  yamlContent: YamlContent,
  refMap: Map<string, boolean>
): void => {
  const metadata = yamlContent['metadata'];
  const keyParts = [];
  if (metadata.namespace) {
    keyParts.push(metadata.namespace);
  }
  if (metadata.name) {
    keyParts.push(metadata.name);
  }
  const version = convertNumberToString(metadata.version);
  if (metadata.version) {
    keyParts.push(version);
  }
  const key = keyParts.join(':');
  refMap.set(key, true);
  //logDebug('0310', 'Metadata', key);
};

export const convertNumberToString = (data: number | string): string => {
  if (typeof data === 'string') {
    return data.trim();
  } else if (Math.abs(data - Math.floor(data)) < 1e-7) {
    return data.toFixed(1);
  } else {
    return data.toString();
  }
};

export const isValidAsset = (yamlContent: YamlContent): boolean => {
  return !!(
    yamlContent?.kind &&
    yamlContent.kind.toLowerCase() !== AppConstants.TEST &&
    yamlContent.kind.toLowerCase() !== AppConstants.ASSERTION &&
    yamlContent.kind.toLowerCase() !== AppConstants.ENVIRONMENT &&
    yamlContent.kind.toLowerCase() !== AppConstants.MCPTOOL &&
    yamlContent.kind.toLowerCase() !== AppConstants.MCPSERVER &&
    yamlContent?.metadata?.name &&
    yamlContent?.metadata?.version &&
    yamlContent?.spec
  );
};

export const errorsArray: ErrorResponse[] = [];
export const addErrorToResponse = (errorCode: string, field: string, description: string) => {
  errorsArray.push({
    code: errorCode,
    field: field,
    description: description,
  });
  //logDebug('0363', description);
};

export const constructErrorResponse = () => {
  const tempErrorsArray = [...errorsArray];
  errorsArray.length = 0;
  return {
    respCode: 400,
    message: 'Invalid Assets or Reference in the Zip',
    Endpoints: [],
    errors: tempErrorsArray,
  };
};

export const createPathReferenceMap = async (buffer: Buffer): Promise<Map<string, boolean>> => {
  const zip = new JSZip();
  const refMap = new Map<string, boolean>();

  try {
    const zipContent = await zip.loadAsync(buffer);
    //logDebug('0308', `${Object.keys(zipContent.files).length}`);

    for (const fileName in zipContent.files) {
      const entry = zipContent.files[fileName];
      if (
        entry &&
        !entry.dir &&
        (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) &&
        !fileName.includes('resources')
      ) {
        const content = await entry.async('string');
        //logInfo('0353', fileName);

        try {
          const yamlContents = yaml.loadAll(content) as YamlContent[];
          for (const yamlContent of yamlContents) {
            extractPath(yamlContent, refMap);
          }
        } catch {
          //logError('0013', 'parsing YAML', `${err}`);
        }
      }
    }
  } catch {
    //logError('0013', 'loading ZIP', `${err}`);
  }

  return refMap;
};

export const extractPath = (yamlContent: YamlContent, refMap: Map<string, boolean>): void => {
  const buildPathRefMap = (obj: SpecObject) => {
    for (const key in obj) {
      const value = obj[key];
      if (key === '$path' && typeof value === 'string') {
        if (!refMap.has(value)) {
          refMap.set(path.normalize(value), false);
          //logDebug('0309', value);
        }
      } else if (typeof value === 'object' && value !== null) {
        buildPathRefMap(value);
      }
    }
  };
  const specOb = JSON.stringify(yamlContent.spec);
  buildPathRefMap(yaml.load(specOb) as SpecObject);
};

export const updatePathRefMap = async (buffer: Buffer, refMap: Map<string, boolean>) => {
  const zip = await JSZip.loadAsync(buffer);
  //logDebug('0003', 'Updating path references from resources directory.');

  zip.forEach((relativePath) => {
    if (relativePath.startsWith('resources/')) {
      const modifiedFileName = relativePath.replace('resources/', '');
      if (refMap.has(modifiedFileName)) {
        refMap.set(modifiedFileName, true);
        //logDebug('0310', 'Path', modifiedFileName);
      }
    }
  });
};

export const validateMinAssets = async (buffer: Buffer): Promise<boolean> => {
  const zip = new JSZip();
  let valid = false;
  try {
    const zipContent = await zip.loadAsync(buffer);
    //logDebug('0308', `${Object.keys(zipContent.files).length}`);

    for (const fileName in zipContent.files) {
      if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
        valid = true;
        break;
      }
    }
  } catch (err) {
    //logError('0013', 'loading zip with minimum assets', `${err}`);
    addErrorToResponse(
      AppConstants.VALIDATION_ERROR_CODE,
      'ZIP_FILE',
      `Error loading zip with minimum assets: ${err}`
    );
    valid = false;
  }
  return valid;
};

export const updateRefs = (
  yamlContent: YamlContent,
  versionMap: Map<string, boolean>
): YamlContent => {
  const updateRef = (obj: SpecObject, verionMap: Map<string, boolean>) => {
    for (const key in obj) {
      const value = obj[key];
      if (key === '$ref' && typeof value === 'string') {
        if (!verionMap.get(value)) {
          obj[key] = processRef(value);
        } else {
          obj[key] = value;
        }
      } else if (typeof value === 'object' && value !== null) {
        updateRef(value, verionMap);
      }
    }
  };
  updateRef(yamlContent.spec, versionMap);

  return yamlContent;
};

export const processRef = (value: string) => {
  const parts = value.split(':');
  const lastPart = parts[parts.length - 1];
  if (lastPart) {
    const numberValue = parseFloat(lastPart);

    if (!isNaN(numberValue)) {
      parts[parts.length - 1] = convertNumberToString(numberValue);
    }
  }
  return parts.join(':');
};

export function checkFileExtension(name: string): boolean {
  return name.endsWith('.yml') || name.endsWith('.yaml');
}

export function isRelativePath(file: string): boolean {
  return file.startsWith('./') || file.startsWith('../');
}

export async function extractGatewayTypes(fileBuffer: Buffer): Promise<GatewayLabels[]> {
  try {
    Logger.info('Extracting gateway types');
    const zip = new JSZip();
    const zipContent = await zip.loadAsync(fileBuffer);

    const gatewaysFile = zipContent.file('gateways.json');

    if (!gatewaysFile) {
      Logger.warn('gateways.json not found in the project');
      return [];
    }
    const gatewaysContent = await gatewaysFile.async('string');
    const gatewaysData = JSON.parse(gatewaysContent);
    const gatewayTypes: GatewayLabels[] = [];

    if (gatewaysData && gatewaysData.gateways && Array.isArray(gatewaysData.gateways)) {
      for (const gateway of gatewaysData.gateways) {
        if (gateway.gatewayTypes && Array.isArray(gateway.gatewayTypes)) {
          gateway.gatewayTypes.forEach((type: GatewayLabels) => {
            if (!gatewayTypes.includes(type)) {
              gatewayTypes.push(type);
            }
          });
        }
      }
    }
    return gatewayTypes;
  } catch (error) {
    Logger.error(
      'Error extracting gateway types',
      error instanceof Error ? error : new Error(String(error))
    );
    return [];
  }
}

/**
 * Checks if an API is a SOAP API by examining its metadata type
 * @param yamlContent - The parsed YAML content of an API
 * @returns true if the API is a SOAP API, false otherwise
 */
export function isSoapApi(yamlContent: YamlContent): boolean {
  try {
    if (yamlContent.kind?.toLowerCase() !== 'api') {
      return false;
    }

    if (yamlContent.metadata?.type?.toUpperCase() === 'SOAP') {
      return true;
    }

    return false;
  } catch (error) {
    Logger.error(
      'Error checking if API is SOAP',
      error instanceof Error ? error : new Error(String(error))
    );
    return false;
  }
}

/**
 * Validates that SOAP APIs are only deployed to DataPower v5/v6 gateways
 * @param fileBuffer - The ZIP file buffer containing the project
 * @returns Object with validation result and errors
 */
export async function validateSoapGatewayRestriction(
  fileBuffer: Buffer
): Promise<{ isValid: boolean; errors: string[] }> {
  try {
    Logger.info('Validating SOAP API gateway restrictions');
    const errors: string[] = [];

    const gatewayTypes = await extractGatewayTypes(fileBuffer);

    if (!gatewayTypes || gatewayTypes.length === 0) {
      Logger.info('No gateway types found, skipping SOAP validation');
      return { isValid: true, errors: [] };
    }

    // Check if any SOAP APIs exist in the project
    const zip = new JSZip();
    const zipContent = await zip.loadAsync(fileBuffer);
    let hasSoapApi = false;

    for (const fileName in zipContent.files) {
      const entry = zipContent.files[fileName];
      if (entry && !entry.dir && checkFileExtension(fileName)) {
        const content = await entry.async('string');
        const yamlContents = yaml.loadAll(content) as YamlContent[];

        for (const yamlContent of yamlContents) {
          if (isSoapApi(yamlContent)) {
            hasSoapApi = true;
            Logger.info(`SOAP API detected in file: ${fileName}`);
            break;
          }
        }

        if (hasSoapApi) break;
      }
    }

    // If no SOAP APIs found, validation passes
    if (!hasSoapApi) {
      Logger.info('No SOAP APIs found in project');
      return { isValid: true, errors: [] };
    }

    const hasDataPowerGateway = gatewayTypes.some((type) =>
      AppConstants.SOAP_ALLOWED_GATEWAYS.includes(type.toLowerCase())
    );

    if (!hasDataPowerGateway) {
      const errorMessage = AppConstants.ERROR_SOAP_GATEWAY_RESTRICTION;
      Logger.error(errorMessage);
      errors.push(errorMessage);
      return { isValid: false, errors };
    }

    Logger.info('SOAP API gateway validation passed');
    return { isValid: true, errors: [] };
  } catch (error) {
    const errorMessage = 'Error validating SOAP API gateway restrictions';
    Logger.error(errorMessage, error instanceof Error ? error : new Error(String(error)));
    return {
      isValid: false,
      errors: [errorMessage],
    };
  }
}
