import {
	IBinaryKeyData,
	ICredentialType,
	INodeCredentialDescription,
	NodeHelpers,
	INodeParameters,
	INodeExecutionData,
	INodeIssues,
	INodeIssueData,
	INodeIssueObjectProperty,
	INodeProperties,
	INodeTypeDescription,
	IRunData,
	IRunExecutionData,
	ITaskDataConnections,
} from 'n8n-workflow';

import {
	ICredentialsResponse,
	INodeUi,
} from '../../Interface';

import { restApi } from '@/components/mixins/restApi';

import { get } from 'lodash';

import mixins from 'vue-typed-mixins';

export const nodeHelpers = mixins(
	restApi,
)
	.extend({
		methods: {

			// Returns the parameter value
			getParameterValue (nodeValues: INodeParameters, parameterName: string, path: string) {
				return get(
					nodeValues,
					path ? path + '.' + parameterName : parameterName,
				);
			},

			// Returns if the given parameter should be displayed or not
			displayParameter (nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, path: string) {
				return NodeHelpers.displayParameterPath(nodeValues, parameter, path);
			},

			// Returns all the issues of the node
			getNodeIssues (nodeType: INodeTypeDescription | null, node: INodeUi, ignoreIssues?: string[]): INodeIssues | null {
				let nodeIssues: INodeIssues | null = null;
				ignoreIssues = ignoreIssues || [];

				if (node.disabled === true) {
					// Ignore issues on disabled nodes
					return null;
				}

				if (nodeType === null) {
					// Node type is not known
					if (!ignoreIssues.includes('typeUnknown')) {
						nodeIssues = {
							typeUnknown: true,
						};
					}
				} else {
					// Node type is known

					// Add potential parameter issues
					if (!ignoreIssues.includes('parameters')) {
						nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
					}

					if (!ignoreIssues.includes('credentials')) {
						// Add potential credential issues
						const nodeCredentialIssues = this.getNodeCredentialIssues(node, nodeType);
						if (nodeIssues === null) {
							nodeIssues = nodeCredentialIssues;
						} else {
							NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
						}
					}
				}

				if (this.hasNodeExecutionIssues(node) === true && !ignoreIssues.includes('execution')) {
					if (nodeIssues === null) {
						nodeIssues = {};
					}
					nodeIssues.execution = true;
				}

				return nodeIssues;
			},

			// Set the status on all the nodes which produced an error so that it can be
			// displayed in the node-view
			hasNodeExecutionIssues (node: INodeUi): boolean {
				const workflowResultData: IRunData = this.$store.getters.getWorkflowRunData;

				if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) {
					return false;
				}

				for (const taskData of workflowResultData[node.name]) {
					if (taskData.error !== undefined) {
						return true;
					}
				}

				return false;
			},

			// Updates the execution issues.
			updateNodesExecutionIssues () {
				const nodes = this.$store.getters.allNodes;

				for (const node of nodes) {
					this.$store.commit('setNodeIssue', {
						node: node.name,
						type: 'execution',
						value: this.hasNodeExecutionIssues(node) ? true : null,
					});
				}
			},

			// Updates the credential-issues of the node
			updateNodeCredentialIssues(node: INodeUi): void {
				const fullNodeIssues: INodeIssues | null = this.getNodeCredentialIssues(node);

				let newIssues: INodeIssueObjectProperty | null = null;
				if (fullNodeIssues !== null) {
					newIssues = fullNodeIssues.credentials!;
				}

				this.$store.commit('setNodeIssue', {
					node: node.name,
					type: 'credentials',
					value: newIssues,
				} as INodeIssueData);
			},

			// Updates the parameter-issues of the node
			updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
				if (nodeType === undefined) {
					nodeType = this.$store.getters.nodeType(node.type);
				}

				if (nodeType === null) {
					// Could not find nodeType so can not update issues
					return;
				}

				// All data got updated everywhere so update now the issues
				const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(nodeType!.properties, node);

				let newIssues: INodeIssueObjectProperty | null = null;
				if (fullNodeIssues !== null) {
					newIssues = fullNodeIssues.parameters!;
				}

				this.$store.commit('setNodeIssue', {
					node: node.name,
					type: 'parameters',
					value: newIssues,
				} as INodeIssueData);
			},

			// Returns all the credential-issues of the node
			getNodeCredentialIssues (node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null {
				if (node.disabled === true) {
					// Node is disabled
					return null;
				}

				if (nodeType === undefined) {
					nodeType = this.$store.getters.nodeType(node.type);
				}

				if (nodeType === null || nodeType!.credentials === undefined) {
					// Node does not need any credentials or nodeType could not be found
					return null;
				}

				if (nodeType!.credentials === undefined) {
					// No credentials defined for node type
					return null;
				}

				const foundIssues: INodeIssueObjectProperty = {};

				let userCredentials: ICredentialsResponse[] | null;
				let credentialType: ICredentialType | null;
				let credentialDisplayName: string;
				let selectedCredentials: string;
				for (const credentialTypeDescription of nodeType!.credentials!) {
					// Check if credentials should be displayed else ignore
					if (this.displayParameter(node.parameters, credentialTypeDescription, '') !== true) {
						continue;
					}

					// Get the display name of the credential type
					credentialType = this.$store.getters.credentialType(credentialTypeDescription.name);
					if (credentialType === null) {
						credentialDisplayName = credentialTypeDescription.name;
					} else {
						credentialDisplayName = credentialType.displayName;
					}

					if (node.credentials === undefined || node.credentials[credentialTypeDescription.name] === undefined) {
						// Credentials are not set
						if (credentialTypeDescription.required === true) {
							foundIssues[credentialTypeDescription.name] = [`Credentials for "${credentialDisplayName}" are not set.`];
						}
					} else {
						// If they are set check if the value is valid
						selectedCredentials = node.credentials[credentialTypeDescription.name];
						userCredentials = this.$store.getters.credentialsByType(credentialTypeDescription.name);

						if (userCredentials === null) {
							userCredentials = [];
						}

						if (userCredentials.find((credentialData) => credentialData.name === selectedCredentials) === undefined) {
							foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials}" do not exist for "${credentialDisplayName}".`];
						}
					}
				}

				// TODO: Could later check also if the node has access to the credentials
				if (Object.keys(foundIssues).length === 0) {
					return null;
				}

				return {
					credentials: foundIssues,
				};
			},

			// Updates the node credential issues
			updateNodesCredentialsIssues () {
				const nodes = this.$store.getters.allNodes;
				let issues: INodeIssues | null;

				for (const node of nodes) {
					issues = this.getNodeCredentialIssues(node);

					this.$store.commit('setNodeIssue', {
						node: node.name,
						type: 'credentials',
						value: issues === null ? null : issues.credentials,
					});
				}
			},

			getNodeInputData (node: INodeUi | null, runIndex = 0, outputIndex = 0): INodeExecutionData[] {
				if (node === null) {
					return [];
				}

				if (this.$store.getters.getWorkflowExecution === null) {
					return [];
				}
				const executionData: IRunExecutionData = this.$store.getters.getWorkflowExecution.data;
				const runData = executionData.resultData.runData;

				if (runData === null || runData[node.name] === undefined ||
					!runData[node.name][runIndex].data ||
					runData[node.name][runIndex].data === undefined
				) {
					return [];
				}

				return this.getMainInputData(runData[node.name][runIndex].data!, outputIndex);
			},

			// Returns the data of the main input
			getMainInputData (connectionsData: ITaskDataConnections, outputIndex: number): INodeExecutionData[] {
				if (!connectionsData || !connectionsData.hasOwnProperty('main') || connectionsData.main === undefined || connectionsData.main.length < outputIndex || connectionsData.main[outputIndex] === null) {
					return [];
				}
				return connectionsData.main[outputIndex] as INodeExecutionData[];
			},

			// Returns all the binary data of all the entries
			getBinaryData (workflowRunData: IRunData | null, node: string | null, runIndex: number, outputIndex: number): IBinaryKeyData[] {
				if (node === null) {
					return [];
				}

				const runData: IRunData | null = workflowRunData;

				if (runData === null || !runData[node] || !runData[node][runIndex] ||
					!runData[node][runIndex].data
				) {
					return [];
				}

				const inputData = this.getMainInputData(runData[node][runIndex].data!, outputIndex);

				const returnData: IBinaryKeyData[] = [];
				for (let i = 0; i < inputData.length; i++) {
					if (inputData[i].hasOwnProperty('binary') && inputData[i].binary !== undefined) {
						returnData.push(inputData[i].binary!);
					}
				}

				return returnData;
			},

			disableNodes(nodes: INodeUi[]) {
				for (const node of nodes) {
					// Toggle disabled flag
					const updateInformation = {
						name: node.name,
						properties: {
							disabled: !node.disabled,
						},
					};

					this.$store.commit('updateNodeProperties', updateInformation);
					this.updateNodeParameterIssues(node);
					this.updateNodeCredentialIssues(node);
				}
			},
		},
	});
