import { Service } from 'typedi';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { deepCopy, jsonParse } from 'n8n-workflow';
import config from '@/config';
import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialTypes } from '@/CredentialTypes';
import { Logger } from '@/Logger';

@Service()
export class CredentialsOverwrites {
	private overwriteData: ICredentialsOverwrite = {};

	private resolvedTypes: string[] = [];

	constructor(
		private readonly credentialTypes: CredentialTypes,
		private readonly logger: Logger,
	) {
		const data = config.getEnv('credentials.overwrite.data');
		const overwriteData = jsonParse<ICredentialsOverwrite>(data, {
			errorMessage: 'The credentials-overwrite is not valid JSON.',
		});

		this.setData(overwriteData);
	}

	setData(overwriteData: ICredentialsOverwrite) {
		// If data gets reinitialized reset the resolved types cache
		this.resolvedTypes.length = 0;

		this.overwriteData = overwriteData;

		for (const type in overwriteData) {
			const overwrites = this.getOverwrites(type);

			if (overwrites && Object.keys(overwrites).length) {
				this.overwriteData[type] = overwrites;
			}
		}
	}

	applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
		const overwrites = this.get(type);

		if (overwrites === undefined) {
			return data;
		}

		const returnData = deepCopy(data);
		// Overwrite only if there is currently no data set
		for (const key of Object.keys(overwrites)) {
			// @ts-ignore
			if ([null, undefined, ''].includes(returnData[key])) {
				returnData[key] = overwrites[key];
			}
		}

		return returnData;
	}

	private getOverwrites(type: string): ICredentialDataDecryptedObject | undefined {
		if (this.resolvedTypes.includes(type)) {
			// Type got already resolved and can so returned directly
			return this.overwriteData[type];
		}

		if (!this.credentialTypes.recognizes(type)) {
			this.logger.warn(`Unknown credential type ${type} in Credential overwrites`);
			return;
		}

		const credentialTypeData = this.credentialTypes.getByName(type);

		if (credentialTypeData.extends === undefined) {
			this.resolvedTypes.push(type);
			return this.overwriteData[type];
		}

		const overwrites: ICredentialDataDecryptedObject = {};
		for (const credentialsTypeName of credentialTypeData.extends) {
			Object.assign(overwrites, this.getOverwrites(credentialsTypeName));
		}

		if (this.overwriteData[type] !== undefined) {
			Object.assign(overwrites, this.overwriteData[type]);
		}

		this.resolvedTypes.push(type);

		return overwrites;
	}

	private get(name: string): ICredentialDataDecryptedObject | undefined {
		const parentTypes = this.credentialTypes.getParentTypes(name);
		return [name, ...parentTypes]
			.reverse()
			.map((type) => this.overwriteData[type])
			.filter((type) => !!type)
			.reduce((acc, current) => Object.assign(acc, current), {});
	}

	getAll(): ICredentialsOverwrite {
		return this.overwriteData;
	}
}
