import JSZip, { JSZipObject } from 'jszip';
import yaml from 'js-yaml';
import { Api_Spec_Ref, SpecObject, YamlContent, ReferenceValidationResult } from '../model/interface.js';
import {
	addErrorToResponse,
	isValidAsset, processRef,
	updateMapWithMetadata,
	validateMinAssets,
	validateYamlFiles,
	checkFileExtension
} from '../utils.js';
import { LogWrapper } from '../service/log-wrapper.js';
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> {
		LogWrapper.logInfo('0003', 'Loading ZIP from buffer.');
		const zip = new JSZip();
		return zip.loadAsync(fileBuffer);
	}

	private async createProjectAssetReferenceMap(buffer: Buffer, folderName: string, allFolderNames: Set<string>): Promise<Map<string, boolean>> {
		LogWrapper.logInfo('0351', 'asset', `${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);
					}
				}
			}
			LogWrapper.logInfo('0003', 'Successfully processed YAML files.');
		} catch (err) {
			LogWrapper.logError('0013', 'processing ZIP', `${err}`);
		}
		return refMap;
	}

	private async processYamlFiles(
		zipContent: JSZip,
		folderName: string,
		refMap: Map<string, boolean>,
		versionMap: Map<string, boolean>,
		processDependencies = true
	): Promise<void> {
		LogWrapper.logInfo('0352', folderName);
		await Promise.all(
			Object.keys(zipContent.files).map(async (fileName) => {
				const entry = zipContent.files[fileName];
				if (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> {
		LogWrapper.logInfo('0353', entry.name);
		const content = await entry.async('string');
		try {
			const yamlContents = yaml.loadAll(content) as YamlContent[];
			for (const yamlContent of yamlContents) {
				if (isValidAsset(yamlContent)) {
					this.updateReferenceMap(yamlContent, refMap, processDependencies, versionMap);
				}
			}
			LogWrapper.logInfo('0354', AppConstants.YAMLContent, entry.name);
		} catch (err) {
			LogWrapper.logError('0013', `parsing YAML in file ${entry.name}`, `${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>> {
		LogWrapper.logInfo('0351', 'path', 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 (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);
							}
						}
						LogWrapper.logInfo('0354', 'path extraction', fileName);
					} catch (err) {
						LogWrapper.logError('0013', `parsing YAML in file ${fileName}`, `${err}`);
					}
				}
			}
		} catch (err) {
			LogWrapper.logError('0013', 'loading ZIP', `${err}`);
		}

		return refMap;
	}
	private async validateApiSpecVaraible(buffer: Buffer, folderName: string): Promise<boolean> {
		LogWrapper.logInfo('0355', 'API Spec variable', folderName);
		const zipContent = await this.loadZipFromBuffer(buffer);
		for (const fileName in zipContent.files) {
			const entry = zipContent.files[fileName];
			if (!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> {
		LogWrapper.logInfo('0357', AppConstants.YAMLContent, 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;
					}
				}
			}
			LogWrapper.logInfo('0358', AppConstants.YAMLContent, fileName);
		} catch (err) {
			LogWrapper.logError('0013', `parsing YAML in file ${fileName}`, `${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> {
		LogWrapper.logInfo('0355', 'project asset references', 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) {
				LogWrapper.logError('0255', 'Some references are not valid.');
				return { isValid: false, refMap };
			}
			return { isValid: true, refMap };
		} catch (err) {
			LogWrapper.logError('0013', 'validating asset', `${err}`);
			return { isValid: false, refMap };
		}
	}

	public async validateProjectPathReference(buffer: Buffer, folderName: string, filePathsInFolder: Set<string>): Promise<boolean> {
		LogWrapper.logInfo('0355', 'project path references', 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) {
					console.log(`Validation failed for path ${key} in ${folderName}`);
					LogWrapper.logError('0003', `Validation failed for path ${key} in ${folderName}`);
					addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for path ${key} in ${folderName}`);
				}
				return value;
			});
			if (!allRefsValid) {
				LogWrapper.logError('0255', 'Some references are not valid.');
				return false;
			}
			return true;
		} catch (err) {
			LogWrapper.logError('0013', 'validating asset', `${err}`);
			return false;
		}
	}

	public async validateDeploymentAsset(buffer: Buffer) {
		try {
			const zipContent = await this.loadZipFromBuffer(buffer);
			LogWrapper.logDebug('0308', `${Object.keys(zipContent.files).length}`);

			for (const fileName in zipContent.files) {
				if (checkFileExtension(fileName)) {
					const entry = zipContent.files[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) {
		LogWrapper.logInfo('0003', 'Validating project has minimum assets.');
		return await validateMinAssets(buffer) && await this.validateDeploymentAsset(buffer);
	}

	public async validateProjectApiSpecVariable(buffer: Buffer, folderName: string) {
		LogWrapper.logInfo('0355', 'project API spec variables', folderName);
		return this.validateApiSpecVaraible(buffer, folderName);
	}

	public async validateYaml(buffer: Buffer) {
		LogWrapper.logInfo('0003', 'Validating YAML files.');
		return validateYamlFiles(buffer);
	}
}
