import {
	and,
	type BinaryOperator,
	eq,
	gt,
	gte,
	lt,
	lte,
	ne,
	not,
	or,
	type SQL,
	sql,
} from "drizzle-orm";
import { KErrorT } from "~/models/error";
import { comment } from "~/utils";
import type { FilterDef } from "./index";
import type { Expression, Operator } from "./parser";

const opMap: Record<Operator, BinaryOperator> = {
	eq: eq,
	ne: ne,
	gt: gt,
	ge: gte,
	lt: lt,
	le: lte,
	has: eq,
};

export const toDrizzle = (expr: Expression, config: FilterDef): SQL => {
	switch (expr.type) {
		case "op": {
			const where = `${expr.property.name}${expr.property.param ? `:${expr.property.param}` : ""} ${expr.operator} ${expr.value.value}`;
			const prop = config[expr.property.name];

			if (!prop) {
				throw new KErrorT(
					comment`
						Invalid property: ${expr.property.name}.
						Expected one of ${Object.keys(config).join(", ")}.
					`,
					{ in: where },
				);
			}

			if (typeof prop.column === "function" && !expr.property.param) {
				throw new KErrorT(
					comment`
						Property ${expr.property.name} requires a parameter (e.g., ${expr.property.name}:source).
					`,
					{ in: where },
				);
			} else if (typeof prop.column !== "function" && expr.property.param) {
				throw new KErrorT(
					comment`
						Property ${expr.property.name} does not accept a parameter.
					`,
					{ in: where },
				);
			}
			const column =
				typeof prop.column === "function"
					? prop.column(expr.property.param!)
					: prop.column;

			if (expr.value.type === "enum" && prop.type === "string") {
				// promote enum to string since this is legal
				// but parser doesn't know if an enum should be a string
				expr.value = { type: "string", value: expr.value.value };
			}
			if (prop.type === "bool" && expr.value.type === "enum") {
				if (expr.value.value !== "false" && expr.value.value !== "true") {
					throw new KErrorT(
						comment`
							Invalid value for property ${expr.property.name}.
							Get ${expr.value.value} but expected true or false.
						`,
						{ in: where },
					);
				}
				expr.value = { type: "bool", value: expr.value.value === "true" };
			}
			if (prop.type !== expr.value.type) {
				throw new KErrorT(
					comment`
						Invalid value for property ${expr.property.name}.
						Got ${expr.value.type} but expected ${prop.type}.
					`,
					{ in: where },
				);
			}
			if (
				prop.type === "enum" &&
				(expr.value.type === "enum" || expr.value.type === "string") &&
				!prop.values.includes(expr.value.value)
			) {
				throw new KErrorT(
					comment`
						Invalid value ${expr.value.value} for property ${expr.property.name}.
						Expected one of ${prop.values.join(", ")} but got ${expr.value.value}.
					`,
					{ in: where },
				);
			}

			if (prop.isArray) {
				if (expr.operator !== "has" && expr.operator !== "eq") {
					throw new KErrorT(
						comment`
							Property ${expr.property.name} is an array but you wanted to use the
							operator ${expr.operator}. Only "has" is supported ("eq" is also aliased to "has")
						`,
						{ in: where },
					);
				}
				return sql`${expr.value.value} = any(${column})`;
			}
			return opMap[expr.operator](column, expr.value.value);
		}
		case "and": {
			const lhs = toDrizzle(expr.lhs, config);
			const rhs = toDrizzle(expr.rhs, config);
			return and(lhs, rhs)!;
		}
		case "or": {
			const lhs = toDrizzle(expr.lhs, config);
			const rhs = toDrizzle(expr.rhs, config);
			return or(lhs, rhs)!;
		}
		case "not": {
			const lhs = toDrizzle(expr.expression, config);
			return not(lhs);
		}
		default:
			return exhaustiveCheck(expr);
	}
};

function exhaustiveCheck(v: never): never {
	return v;
}
