/**
* Copyright Super iPaaS Integration LLC, an IBM Company 2024
*/
import yaml from 'js-yaml';
import { GatewayAssetHandler } from '../converter/gateway-asset.handler.js';
import { RefParser } from '../parsers/ref.parser.js';
import { AssertionsGenerator } from '../service/assertMapper.js';
import pkg from 'postman-collection';
const {FormParam,RequestBody}=pkg;
import {
	Environment,
	EnvironmentVariable,
	RequestItem,
	RequestParameter,
	RequestSettting,
	RequestBodyObject,
	TestSpec,
	Env_Spec,
	Assertions,
	TestMetadata,
	ExpType, ICollectionCreator,RequestHeader,RequestAuth,TestAssertion,FormDataItem,ExtendedFormParamDefinition,RawData,ExtendedRequestBodyDefinition,
	UrlEncodedFormDataItem
} from '../models/interface.js';
import JSZip from 'jszip';
import { LogWrapper } from '../service/log-wrapper.js';

export class CollectionCreator implements ICollectionCreator {

	private async getApiEndpoints(buffer: Buffer, key: string) {
		LogWrapper.logInfo('0212', key);
		const gatewayHandler = new GatewayAssetHandler(buffer);
		return gatewayHandler.getApiEndpoints(key);
	}

	async createCollection(parsedData: TestSpec, fileBuffer: Buffer) {
		LogWrapper.logInfo('0213', `${parsedData.metadata.name}`);
		const metadata = parsedData.metadata as TestMetadata;
		const collection = this.initializeCollection(metadata.name);
		const spec = await this.getReplacedItems(parsedData, fileBuffer);
		const api = spec.api;
		let endpoints=[];

		if(api.$ref) {
			endpoints = await this.getApiEndpoints(fileBuffer, api.$ref);
			if (endpoints.length === 0) {
				LogWrapper.logError('0004', `No endpoints found for reference: ${api.$ref}`);
				throw new Error('Endpoints are empty and invalid asset');
			}
		}
		else {
			endpoints=[api.$endpoint];
		}

		if (spec.request) {
			for (const requestItem of spec.request) {
				const item = await this.constructItem(requestItem, fileBuffer,parsedData);
				endpoints.forEach((endpoint: string) => {
					const updatedItem = this.updateCollectionBaseUrl(item, endpoint, requestItem.resource, requestItem.parameters as RequestParameter[]);
					// @ts-ignore
					collection.item.push(updatedItem);
				});
			}
		}
		LogWrapper.logInfo('0214', metadata.name);
		return collection;
	}

	private initializeCollection(value: string) {
		LogWrapper.logDebug('0003', `Initializing collection with name: ${value}`);
		return {
			info: {
				name: `${value} Collection`,
				version: '1.0.0',
				schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
			},
			item: []
		};
	}

	private async getReplacedItems(parsedData: TestSpec, fileBuffer: Buffer) {
		LogWrapper.logInfo('0003', 'Replacing placeholders in requests');
		if (parsedData.spec?.environment?.$ref) {
			const result = await this.resolveRef(fileBuffer, parsedData.spec.environment.$ref);
			return this.replacePlaceholders(parsedData.spec, result);
		} else if (parsedData.spec?.environment?.variables !== undefined) {
			return this.replacePlaceholders(parsedData.spec, parsedData.spec.environment?.variables as EnvironmentVariable[]);
		} else {
			return parsedData.spec;
		}
	}

	private async constructItem(requestItem: RequestItem, fileBuffer: Buffer,parsedData:any) {
		LogWrapper.logDebug('0003', `Constructing item for request: ${requestItem.method} ${requestItem.resource}`);
		return {
			name: `${requestItem.method} ${requestItem.resource}`,
			event: await this.constructAssertion(requestItem.assertions, fileBuffer,parsedData),
			protocolProfileBehavior: requestItem.settings ? this.constructSettings(requestItem.settings) : undefined,
			request: {
				url: {},
				method: requestItem.method,
				header: this.constructHeaders(requestItem.headers as RequestHeader[]),
				body: requestItem.payload ? this.constructRequestBody(requestItem.payload) : undefined,
				auth: this.constructAuth(requestItem.auth)
			}
		};
	}

	private constructHeaders(headers: RequestHeader[] | undefined) {
		LogWrapper.logDebug('0003', 'Constructing headers');
		return headers ? headers.map((header: { key: string; value: string }) => ({
			key: header.key,
			value: header.value
		})) : [];
	}

	private constructAuth(auth: RequestAuth | undefined) {
		LogWrapper.logDebug('0003', 'Constructing auth for request');
		if (!auth) {
			return undefined;
		}
		if (auth.basicAuth) {
			return {
				type: 'basic',
				basic: [{
					key: 'username',
					value: auth.basicAuth.username
				}, {
					key: 'password',
					value: auth.basicAuth.password
				}]
			};
		} else if (auth.bearerToken) {
			return {
				type: 'bearer',
				bearer: [{
					key: 'token',
					value: auth.bearerToken
				}]
			};
		} else if (auth.noauth) {
			return { type: 'noauth' };
		}
		return undefined;
	}

	private async resolveRef(fileBuffer: Buffer, ref: string): Promise<EnvironmentVariable[]> {
		LogWrapper.logInfo('0003', `Resolving reference: ${ref}`);
		const parseRefObj = new RefParser();
		const { namespace, name, version } = parseRefObj.parseRef(ref);
		const entries = await this.loadZipEntries(fileBuffer);

		for (const data of entries) {
			const pds = yaml.loadAll(data) as Environment[];
			for (const pd of pds) {
				if (
					pd.kind?.toLowerCase() === 'environment' &&
				pd.metadata.name === name &&
				(typeof pd.metadata.version === 'number' ? Number(version) === Number(pd.metadata.version) : pd.metadata.version === version) &&
				(namespace ? pd.metadata.namespace === namespace : true)
				) {
					LogWrapper.logDebug('0003', `Reference resolved successfully: ${ref}`);
					const spec = pd.spec as Env_Spec;
					return spec.variables as EnvironmentVariable[];
				}
			}
		}
		LogWrapper.logError('0004', `Reference not found: ${ref}`);
		throw new Error(`Reference ${ref} not found in the provided files.`);
	}
	// @ts-ignore
	private async loadZipEntries(fileBuffer) {
		LogWrapper.logDebug('0003', 'Loading ZIP entries');
		const zip = new JSZip();
		const zipContent = await zip.loadAsync(fileBuffer);
		const entries = [];

		for (const fileName in zipContent.files) {
			const file = zipContent.files[fileName];
			if (!file.dir) {
				const content = await file.async('string');
				entries.push(content);
			}
		}
		LogWrapper.logInfo('0003', `Loaded ${entries.length} entries from ZIP`);
		return entries;
	}

	private replacePlaceholders(obj: any, variables: EnvironmentVariable[]){
		LogWrapper.logDebug('0003', 'Replacing placeholders in object');
		if (typeof obj === 'string') {
			variables.forEach(variable => {
				const placeholderRegex = new RegExp(`\\$\\{${variable.key}\\}`, 'g');
				// @ts-ignore
				obj = obj.replace(placeholderRegex, variable.value);
			});
		} else if (Array.isArray(obj)) {
			obj.forEach((item, index) => {
				// @ts-ignore
				obj[index] = this.replacePlaceholders(item, variables);
			});
		} else if (typeof obj === 'object' && obj !== null) {
			Object.keys(obj).forEach(key => {
				// @ts-ignore
				obj[key] = this.replacePlaceholders(obj[key], variables);
			});
		}
		return obj;
	}

	private async constructAssertion(obj: Assertions, fileBuffer: Buffer,parsedData:any) {
		LogWrapper.logDebug('0003', 'Constructing assertions');
		const exec: string[] = [];
		let asserts: ExpType[] = [];
		const assertGen = new AssertionsGenerator();

		if (obj.$ref !== undefined) {
			asserts = await this.resolveRefAssertion(fileBuffer, obj.$ref);
			if(parsedData?.spec?.environment?.$ref){
				const result = await this.resolveRef(fileBuffer, parsedData.spec.environment.$ref);
				asserts= this.replacePlaceholders(asserts, result);
			}
		}
		if (obj.expressions !== undefined) {
			asserts = asserts.concat(obj.expressions as ExpType[]);
		}
		if (asserts.length > 0) {
			exec.push(assertGen.initializeCommonScriptLibrary());
			asserts.forEach(assert => {
				exec.push(assertGen.converttoAssertionStructure(assert));
			});
		}

		return [{
			'listen': 'test',
			'script': {
				'exec': exec
			}
		}];
	}

	private async resolveRefAssertion(fileBuffer: Buffer, ref: string) {
		LogWrapper.logInfo('0003', `Resolving assertion reference: ${ref}`);
		const parseRefObj = new RefParser();
		const { namespace, name, version } = parseRefObj.parseRef(ref);
		const entries = await this.loadZipEntries(fileBuffer);

		for (const data of entries) {
			const pds = yaml.loadAll(data) as TestAssertion[];
			for (const pd of pds) {
				if (
					pd.kind?.toLowerCase() === 'assertion' &&
					pd.metadata.name === name &&
					(typeof pd.metadata.version === 'number' ? Number(version) === Number(pd.metadata.version) : pd.metadata.version === version) &&
					(namespace ? pd.metadata.namespace === namespace : true)
				) {
					LogWrapper.logDebug('0003', `Assertion reference resolved: ${ref}`);
					return pd.spec as ExpType[];
				}
			}
		}
		LogWrapper.logError('0003', `Assertion reference not found: ${ref}`);
		throw new Error(`Reference ${ref} not found in the provided files.`);
	}

	private constructSettings(obj: RequestSettting) {
		LogWrapper.logDebug('0003', 'Constructing request settings');
		return {
			'disableUrlEncoding': !obj.encodeURL ? false : true,
			'strictSSL': obj.sslVerification
		};
	}

	private constructRequestBody(obj: RequestBodyObject): InstanceType<typeof RequestBody> | undefined {
		LogWrapper.logDebug('0003', 'Constructing request body');
		if (obj.formData) {
			return this.constructFormDataBody(obj.formData as FormDataItem[]);
		} else if (obj.urlEncodedFormData) {
			return this.constructUrlEncodedBody(obj.urlEncodedFormData as UrlEncodedFormDataItem[]);
		} else if (obj.raw) {
			return this.constructRawBody(obj.raw);
		}
		return undefined;
	}

	private constructFormDataBody(formData: FormDataItem[]): InstanceType<typeof RequestBody> {
		LogWrapper.logDebug('0003', 'Constructing form data body');
		const formDataParams = formData.map(item => new FormParam({
			key: item.key,
			src: item.type === 'file' ? item.value : undefined,
			value: item.type !== 'file' ? item.value : undefined,
			type: item.type
		} as ExtendedFormParamDefinition));
		return new RequestBody({
			mode: 'formdata',
			formdata: formDataParams
		});
	}

	private constructUrlEncodedBody(urlEncodedFormData: Array<{
		key: string;
		value: string
	}>): InstanceType<typeof RequestBody> {
		LogWrapper.logDebug('0003', 'Constructing URL encoded body');
		const formDataParams = urlEncodedFormData.map(item => new FormParam({
			key: item.key,
			value: item.value,
			type: 'text'
		} as ExtendedFormParamDefinition));
		return new RequestBody({
			mode: 'urlencoded',
			urlencoded: formDataParams
		});
	}

	private constructRawBody(raw: RawData): InstanceType<typeof RequestBody> | undefined {
		LogWrapper.logDebug('0003', 'Constructing raw body');
		const rawTypes = ['json', 'js', 'html', 'xml'] as const;
		for (const type of rawTypes) {
			if (raw[type]) {
				return new RequestBody({
					mode: 'raw',
					raw: raw[type],
					options: {
						raw: {
							language: type === 'js' ? 'javascript' : type
						}
					}
				} as ExtendedRequestBodyDefinition);
			}
		}
		return undefined;
	}

	private constructUrl(url: string, reqPath: string, param: RequestParameter[]) {
		LogWrapper.logDebug('0003', 'Constructing URL');
		const myURL = new URL(url + reqPath);
		return {
			raw: url + reqPath,
			protocol: myURL.protocol.slice(0, -1),
			host: myURL.hostname.split('.').filter(p => p),
			path: myURL.pathname.split('/').filter(p => p),
			port: myURL.port ? myURL.port : undefined,
			query: param ? param.map(variable => ({
				key: variable.key,
				value: variable.value
			})) : []
		};
	}

	private updateCollectionBaseUrl(item: unknown, Endpoint: string, reqPath: string, Parameters: RequestParameter[]) {
		LogWrapper.logDebug('0003', 'Updating collection base URL');
		const urlObj = this.constructUrl(Endpoint, reqPath, Parameters);
		const ItemDeepCopy = JSON.parse(JSON.stringify(item));
		ItemDeepCopy.request.url = urlObj;
		return ItemDeepCopy;
	}
}
