import type { ExpressionBuilder, SelectQueryBuilder, SqlBool } from 'kysely';
import { type ExpressionWrapper, sql } from 'kysely';
import type { QueryOrderBy } from '../core/order-by.ts';
import type { SchemaShape } from '../core/schema.ts';
import type {
	ComparisonWhere,
	QueryWhere,
	SetComparisonOp,
	UnaryComparisonOp,
} from '../core/where.ts';
import { isComparisonWhere } from '../core/where.ts';
import { fieldToStr } from '../sql/pg.ts';
import type { TSelectQueryBuilder } from './types.ts';

const kyselyOpMap = {
	'=': '=',
	'>': '>',
	'>=': '>=',
	'<': '<',
	'<=': '<=',
	like: 'like',
	ilike: 'ilike',
	'<@': '<@',
	'@>': '@>',
	'&&': '&&',
} as const satisfies Record<UnaryComparisonOp | SetComparisonOp, string>;

type Expression<TShape, TableName extends string> = ExpressionBuilder<
	{ [k in TableName]: TShape },
	TableName
>;
type BuildResult<TShape, TableName extends string> = (
	expression: Expression<TShape, TableName>,
) => ExpressionWrapper<{ [k in TableName]: TShape }, TableName, SqlBool>;
const emptyExp = <TShape extends SchemaShape, TableName extends string>(
	exp: Expression<TShape, TableName>,
) => exp.and([]);

const build = <TShape extends SchemaShape, TableName extends string>(
	where: QueryWhere<TShape>,
	exp: Expression<TShape, TableName>,
): ExpressionWrapper<{ [k in TableName]: TShape }, TableName, SqlBool> => {
	if (where.op === 'not') {
		const inner = build(where.condition, exp);
		if (!inner) return emptyExp(exp);
		return exp.not(inner);
	}

	if (where.op === 'and' || where.op === 'or') {
		const parts = where.conditions
			.map((c) => build(c, exp))
			.filter((c): c is NonNullable<typeof c> => c !== undefined);

		if (parts.length === 0) return emptyExp(exp);

		return where.op === 'and' ? exp.and(parts) : exp.or(parts);
	}

	if (!isComparisonWhere(where)) return emptyExp(exp);

	const target = sql.raw(fieldToStr(where.field));
	if (where.op === 'is null') {
		return exp.eb(target, 'is', null);
	}
	if (where.op === 'in') {
		return exp.eb(target, 'in', where.values);
	}

	const op = kyselyOpMap[where.op];
	if (where.op === 'like' || where.op === 'ilike') {
		return exp.eb(target, op, where.value);
	}
	return exp.eb(target, op, where.value);
};

export const toKyselyWhere = <
	TShape extends Record<string, any>,
	TableName extends string,
>(
	where?: QueryWhere<TShape> | null,
): BuildResult<TShape, TableName> => {
	return (exp: Expression<TShape, TableName>) => {
		if (!where) return emptyExp(exp);
		return build<TShape, TableName>(where, exp);
	};
};

export const toKyselyOrderBy = <
	TShape extends Record<string, any>,
	TableName extends string,
>(
	q: TSelectQueryBuilder<TShape, TableName>,
	orderBy?: QueryOrderBy<TShape>[],
) => {
	if (!orderBy || orderBy.length === 0) return q;
	let currentQuery = q;

	for (const order of orderBy) {
		// 1. 使用你原生的 fieldToStr 生成 PG 語法字串
		const sqlTarget = fieldToStr(order.field);

		// 2. 使用 sql.raw 包裝，告訴 Kysely 這是一段原始 SQL
		// 3. 連續調用 .orderBy
		currentQuery = currentQuery.orderBy(sql.raw(sqlTarget), order.direction);
	}

	return currentQuery;
};
