import { parseScript } from "esprima";
import type { Expression, Node } from "estree";

function buildObject(node: Node | Expression): unknown {
	switch (node.type) {
		case "ObjectExpression": {
			const obj: Record<string, unknown> = {};
			for (const prop of node.properties) {
				let name;
				if (prop.type === "SpreadElement") {
					throw new Error(`Expected "Property" but received: ${prop.type}`);
				}
				if (prop.key.type === "Identifier") {
					name = prop.key.name;
				} else if (prop.key.type === "Literal") {
					if (
						prop.key.value instanceof RegExp ||
						typeof prop.key.value === "bigint" ||
						prop.key.value === false ||
						prop.key.value === null ||
						prop.key.value === true ||
						prop.key.value === undefined
					) {
						throw new Error(`Expected "Identifier" for object key but received: ${prop.key.type}`);
					}
					name = prop.key.value;
				} else {
					throw new Error(`Expected "Identifier" but received: ${prop.key.type}`);
				}

				obj[name] = buildObject(prop.value);
			}
			return obj;
		}

		case "ArrayExpression": {
			const obj: unknown[] = [];
			for (const prop of node.elements) {
				if (prop === null) {
					throw new Error(`Expected "Expression" but received: ${prop}`);
				}
				obj.push(buildObject(prop));
			}
			return obj;
		}

		case "Literal": {
			if (node.value instanceof RegExp) {
				return {
					$type: "RegExp",
					$value: {
						$pattern: node.value.source,
						$flags: node.value.flags,
					},
				};
			}

			return node.value;
		}

		case "UnaryExpression": {
			if (node.operator === "-" && node.argument.type === "Literal" && typeof node.argument.value === "number") {
				return -node.argument.value;
			}
			// const arg = buildObject(node.argument);
			// const exp = node.prefix ? `${node.operator}${arg}` : `${arg}${node.operator}`;

			// return eval(exp);
			throw new Error(`${node.type} are not authorized`);
		}

		case "NewExpression":
		case "CallExpression": {
			const authorizedCalls = ["ObjectId", "Date", "RegExp", "BinData"];
			const callee = node.callee.type === "Identifier" ? node.callee.name : null;
			if (callee && authorizedCalls.includes(callee)) {
				if (callee === "RegExp") {
					const [pattern, flags] = node.arguments.map((arg) => buildObject(arg));
					return {
						$type: "RegExp",
						$value: {
							$pattern: pattern,
							$flags: flags,
						},
					};
				}

				if (callee === "BinData") {
					// BinData(subType, base64String)
					const [subType, base64] = node.arguments.map((arg) => buildObject(arg));
					return {
						$type: "Binary",
						$value: base64,
						$subType: subType,
					};
				}

				return {
					$type: callee,
					$value: buildObject(node.arguments[0]),
				};
			} else {
				throw new Error(`Unknown ${node.type}: ${callee}`);
			}
		}

		case "Identifier": {
			if (node.name === "undefined") {
				return undefined;
			}
			if (node.name === "Infinity") {
				return Infinity;
			}
			throw `Unknown identifier: ${node.name}`;
		}

		default:
			throw new Error(`Sorry but ${node.type} are not authorized`);
	}
}

export function parseJSON(text: string, opts?: { allowArray?: boolean }): unknown {
	const tree = parseScript(`var __JSON__ = ${text};`, {
		tolerant: true,
	});

	const varDeclaration = tree.body[0];
	if (varDeclaration.type !== "VariableDeclaration") {
		throw new Error("Expected VariableDeclaration but received: " + varDeclaration.type);
	}

	const objExpression = varDeclaration.declarations[0].init;

	if (opts?.allowArray && objExpression?.type === "ArrayExpression") {
		return buildObject(objExpression);
	}

	if (objExpression?.type !== "ObjectExpression") {
		throw new Error("Expected ObjectExpression but received: " + objExpression?.type);
	}

	return buildObject(objExpression);
}

/**
 * Serializes an object to a string format that can be parsed back with parseJSON.
 * Handles MongoDB special types like ObjectId, Date, and RegExp.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serializeForEditing(obj: any, depth = 0): string {
	const indent = "\t".repeat(depth);
	const nextIndent = "\t".repeat(depth + 1);

	if (obj === null) {
		return "null";
	}
	if (obj === undefined) {
		return "undefined";
	}
	if (typeof obj === "string") {
		return JSON.stringify(obj);
	}
	if (typeof obj === "number") {
		return obj.toString();
	}
	if (typeof obj === "boolean") {
		return obj.toString();
	}

	if (Array.isArray(obj)) {
		if (obj.length === 0) {
			return "[]";
		}
		const items = obj.map((item) => `${nextIndent}${serializeForEditing(item, depth + 1)}`).join(",\n");
		return `[\n${items}\n${indent}]`;
	}

	if (typeof obj === "object") {
		// Handle special MongoDB types
		if (obj.$type === "ObjectId") {
			return `new ObjectId("${obj.$value}")`;
		}
		if (obj.$type === "Date") {
			return `new Date("${obj.$value}")`;
		}
		if (obj.$type === "RegExp") {
			return `new RegExp("${obj.$value.$pattern}", "${obj.$value.$flags}")`;
		}

		// Handle regular objects
		const keys = Object.keys(obj);
		if (keys.length === 0) {
			return "{}";
		}

		const pairs = keys
			.map((key) => {
				const value = serializeForEditing(obj[key], depth + 1);
				return `${nextIndent}${JSON.stringify(key)}: ${value}`;
			})
			.join(",\n");

		return `{\n${pairs}\n${indent}}`;
	}

	return JSON.stringify(obj);
}
