import {
	changeProjectName,
	clearGraph,
	deleteNodes,
} from "qrc:/js/lib/graph_manipulator";
import {
	getKeysOfObject,
	isArray,
	isNumber,
	isString,
} from "qrc:/js/lib/utils";
import {
	addUserDefinedTubeNode,
} from "qrc:/js/lib/graph_manipulator_utils";

import {
	addSimplePart,
	applyChangesToVertices,
	createShopBackendProjectData,
	isShopBackendDataConfig,
	prepareUnsafeSession,
	serializeProject,
	ShopBackendDataConfig,
} from "./cli_shop";
import {
	shopConfiguration,
} from "./cli_shop_configuration";
import {
	ConfigFlags,
	InputEntry,
	isConfigFlags,
	isInputEntry,
	isPartCreationInformation,
	isVertexKeyWithChangeData,
	PartCreationInformation,
	VertexKeyWithChangeData,
} from "./cli_shop_interface";
import {
	createProjectObj,
} from "./cli_shop_project_json";
import {
	addDataAndFlattenGraph,
	addLayereds,
	ExternalAddResult,
	isTwoDimInput,
	TwoDimInput,
} from "./cli_add_data";
import {
	computeShippingCosts,
} from "./cli_shop_shipping_costs";

export function addDataShop(entries: InputEntry[]): ExternalAddResult[] {
	return addDataAndFlattenGraph(entries.map(entry => ({
		dataSource: {
			tag: "fileContent",
			type: "base64",
			data: entry.data,
		},
		importId: entry.importId,
		articleName: entry.name,
		sheetThickness: entry.thickness,
		multiplicity: undefined,
	})));
}

// ================================

// Functions intended to be called e. g. via wsi4cli's HTTP-server-interface
// This map defines the actual functions.
// The parameters passed as part of the POST-request need to conform to Parameters<func> (aka. a tuple of arguments).
const apiFunctionMap = {
	addData: addDataShop,
	addLayereds: addLayereds,
	addSimplePart: addSimplePart,
	addUserDefinedTube: addUserDefinedTubeNode,
	changeProjectName: (name: string) => {
		changeProjectName(name);
		return true;
	},
	applyChangesToVertices: applyChangesToVertices,
	clearGraph: clearGraph,
	computeShippingCosts: computeShippingCosts,
	createProjectObject: (nodeIdKeys: string[]) => createProjectObj(nodeIdKeysToVertices(nodeIdKeys)),
	createShopBackendProjectData: (nodeIdKeys: string[], config?: ShopBackendDataConfig) => createShopBackendProjectData(nodeIdKeysToVertices(nodeIdKeys), config),
	deleteNodes: (vertexKeys: string[]) => {
		deleteNodes(vertexKeys.map(v => vertexKeyToVertex(v)));
		return true;
	},
	prepareUnsafeSession: prepareUnsafeSession,
	serializeProject: serializeProject,
	shopConfiguration: shopConfiguration,
};
export type ApiFunctionMap = typeof apiFunctionMap;

// Typeguards for the parameter-tuples of each function
const parameterTypeGuardMap: {[index in keyof ApiFunctionMap]: (args: unknown) => args is Parameters<ApiFunctionMap[index]>} = {
	addData: (args: unknown): args is [InputEntry[]] => isSizeOneArray(args) && isArray(args[0], isInputEntry),
	addSimplePart: (args: unknown): args is [PartCreationInformation] => isSizeOneArray(args) && isPartCreationInformation(args[0]),
	addUserDefinedTube: (args: unknown): args is [string, number, string, string] => Array.isArray(args) && args.length === 4 && isString(args[0]) && isNumber(args[1]) && isString(args[2]) && isString(args[3]),
	addLayereds: (args: unknown): args is [TwoDimInput[]] => isSizeOneArray(args) && isArray(args[0], isTwoDimInput),
	changeProjectName: (args: unknown): args is [string] => isSizeOneArray(args) && isString(args[0]),
	applyChangesToVertices: (args: unknown): args is [VertexKeyWithChangeData[]] => isSizeOneArray(args) && isArray(args[0], isVertexKeyWithChangeData),
	clearGraph: isEmptyArray,
	computeShippingCosts: (args: unknown): args is [number[]] => isSizeOneArray(args) && isArray(args[0], isNumber),
	createProjectObject: (args: unknown): args is [string[]] => isSizeOneArray(args) && isArray(args[0], isString),
	createShopBackendProjectData: (args: unknown): args is [string[], ShopBackendDataConfig | undefined] => {
		let result = true;
		result = result && Array.isArray(args);
		result = result && (args as unknown[]).length >= 1;
		result = result && isArray((args as unknown[])[0], isString);
		result = result && ((args as unknown[])[1] === undefined || isShopBackendDataConfig((args as unknown[])[1]));
		return result;
	},
	deleteNodes: (args: unknown): args is [string[]] => isSizeOneArray(args) && isArray(args[0], isString),
	prepareUnsafeSession: (args: unknown): args is [number] => isSizeOneArray(args) && isArray(args[0], isNumber),
	serializeProject: isEmptyArray,
	shopConfiguration: (args: unknown): args is [ConfigFlags] => isSizeOneArray(args) && isConfigFlags(args[0]),
};
// ================================

function isSizeOneArray(arg: unknown): arg is unknown[] {
	return Array.isArray(arg) && arg.length === 1;
}

function isEmptyArray(arg: unknown): arg is [] {
	// Note: Outer array represents parameter-tuple
	return Array.isArray(arg) && arg.length === 0;
}

function vertexKeyToVertex(vertexKey: string): Vertex {
	const vertex = wsi4.graph.vertices()
		.find(v => wsi4.util.toKey(v) === vertexKey);
	return vertex === undefined ? wsi4.throwError(`Invalid vertexKey submitted: "${vertexKey}"`) : vertex;
}

function nodeIdKeysToVertices(nodeIdKeys: string[]): Vertex[] {
	if (nodeIdKeys === undefined) {
		return wsi4.throwError("Expecting array of string");
	}
	const vertices = nodeIdKeys.map(key => wsi4.graph.vertices()
		.find(vertex => wsi4.util.toKey(wsi4.node.nodeId(vertex)) === key));
	return vertices.filter((vertex): vertex is Vertex => vertex !== undefined);
}

function isMatchingArgs<Key extends keyof ApiFunctionMap>(key: Key, args: unknown): args is Parameters<ApiFunctionMap[typeof key]> {
	return parameterTypeGuardMap[key](args);
}

function isValidKey(key: unknown): key is keyof ApiFunctionMap {
	return isString(key) && getKeysOfObject(apiFunctionMap)
		.some(k => k === key);
}

/*
 * Utilized by web-ui's network broker
 */
export function callApiFunction(key: unknown, argsInput: unknown): unknown {
	if (!isValidKey(key)) {
		return wsi4.throwError(`Invalid key: "${JSON.stringify(key)}"`);
	}
	if (!isString(argsInput)) {
		return wsi4.throwError(`Invalid argsInput for function "${key}".  Expecting string`);
	}

	const args = JSON.parse(argsInput) as unknown;
	if (!isMatchingArgs(key, args)) {
		console.error("Invalid args for key \"" + key + "\": " + JSON.stringify(args));
		return wsi4.throwError(`Invalid args for function "${key}"`);
	}

	// Note: currently there are two options:
	// 1. switch(key) { ... } and handle each key explicitly
	// 2. any-cast
	// Unfortunately there seems to be no way to implement this generically without using
	// an any-cast at some point as key is a runtime-variable (coming from the front-end)
	// eslint-disable-next-line @typescript-eslint/no-unsafe-call
	return (apiFunctionMap[key] as any)(...(args as any));
}
