import type {
	KyselyPlugin,
	NoResultErrorConstructor,
	PluginTransformQueryArgs,
	PluginTransformResultArgs,
	QueryNode,
	QueryResult,
	RootOperationNode,
	UnknownRow,
} from "kysely";

// --- Type helpers ---

/** Converts `string` (and nullable/undefinable variants) to `Date` variants. */
type StringToDate<T> = T extends string ? Date : T;

/**
 * Given type T and a dot-separated path, returns T with the field at that
 * path converted from `string` to `Date`. Automatically traverses arrays.
 */
type SetNestedPath<
	T,
	Path extends string,
> = Path extends `${infer Key}.${infer Rest}`
	? {
			[K in keyof T]: K extends Key
				? NonNullable<T[K]> extends (infer U)[]
					?
							| SetNestedPath<U, Rest>[]
							| (null extends T[K] ? null : never)
							| (undefined extends T[K] ? undefined : never)
					:
							| SetNestedPath<NonNullable<T[K]>, Rest>
							| (null extends T[K] ? null : never)
							| (undefined extends T[K] ? undefined : never)
				: T[K];
		}
	: {
			[K in keyof T]: K extends Path ? StringToDate<T[K]> : T[K];
		};

/** Applies all date-path transformations in the Paths tuple to type T. */
export type WithParsedDates<
	T,
	Paths extends readonly string[],
> = Paths extends readonly [
	infer Head extends string,
	...infer Tail extends string[],
]
	? WithParsedDates<SetNestedPath<T, Head>, Tail>
	: T;

/** Extracts dot-separated paths leading to a `string` or `Date` leaf, auto-traversing arrays. */
type DateConvertiblePaths<
	T,
	Depth extends unknown[] = [],
> = Depth["length"] extends 5
	? never
	: T extends Record<string, unknown>
		? {
				[K in keyof T & string]:
					| (NonNullable<T[K]> extends string | Date ? K : never)
					| (NonNullable<T[K]> extends readonly (infer U)[]
							? `${K}.${DateConvertiblePaths<U, [...Depth, unknown]>}`
							: NonNullable<T[K]> extends Record<string, unknown>
								? `${K}.${DateConvertiblePaths<NonNullable<T[K]>, [...Depth, unknown]>}`
								: never);
			}[keyof T & string]
		: never;

// --- Runtime ---

type ParsedPath = string[];

function applyDatePath(obj: Record<string, unknown>, path: ParsedPath): void {
	const [key, ...rest] = path;
	if (key === undefined) return;

	const value = obj[key];

	if (rest.length === 0) {
		if (typeof value === "string") {
			obj[key] = new Date(value);
		}
		return;
	}

	if (Array.isArray(value)) {
		for (const item of value) {
			if (item !== null && typeof item === "object") {
				applyDatePath(item as Record<string, unknown>, rest);
			}
		}
	} else if (
		value !== null &&
		value !== undefined &&
		typeof value === "object"
	) {
		applyDatePath(value as Record<string, unknown>, rest);
	}
}

/**
 * Kysely plugin that converts date strings to `Date` objects for fields
 * returned inside JSON (e.g. from `jsonArrayFrom` / `jsonObjectFrom`).
 *
 * Note: `withPlugin` does not transform TypeScript types.
 * Use `executeWithParsedDates` for a fully type-safe result.
 */
export class DateJsonParserPlugin<const Paths extends readonly string[]>
	implements KyselyPlugin
{
	private readonly parsedPaths: ParsedPath[];

	constructor(readonly paths: Paths) {
		this.parsedPaths = paths.map((p) => p.split("."));
	}

	transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
		return args.node;
	}

	async transformResult(
		args: PluginTransformResultArgs,
	): Promise<QueryResult<UnknownRow>> {
		return {
			...args.result,
			rows: args.result.rows.map((row) => {
				const newRow = { ...row } as Record<string, unknown>;
				for (const path of this.parsedPaths) {
					applyDatePath(newRow, path);
				}
				return newRow;
			}),
		};
	}
}

type NoResultErrorArg = NoResultErrorConstructor | ((node: QueryNode) => Error);

type PluggedQuery<T> = {
	execute(): Promise<T[]>;
	executeTakeFirst(): Promise<T | undefined>;
	executeTakeFirstOrThrow(errorConstructor?: NoResultErrorArg): Promise<T>;
};

/**
 * Wraps a Kysely query with date string → `Date` parsing, exposing
 * `execute`, `executeTakeFirst`, and `executeTakeFirstOrThrow` with
 * correctly inferred TypeScript types.
 *
 * @example
 * const rows = await withParsedDates(
 *   db
 *     .selectFrom("box")
 *     .select((eb) => [
 *       jsonArrayFrom(
 *         eb.selectFrom("item").selectAll().whereRef("item.box_id", "=", "box.id"),
 *       ).as("items"),
 *     ]),
 *   ["items.created_at"],
 * ).execute();
 * // rows: { items: { id: number; box_id: number; created_at: Date }[] }[]
 */
export function withParsedDates<
	T,
	const Paths extends readonly DateConvertiblePaths<T>[],
>(
	query: {
		withPlugin(plugin: KyselyPlugin): PluggedQuery<T>;
	},
	paths: Paths,
): {
	execute(): Promise<WithParsedDates<T, Paths>[]>;
	executeTakeFirst(): Promise<WithParsedDates<T, Paths> | undefined>;
	executeTakeFirstOrThrow(
		errorConstructor?: NoResultErrorArg,
	): Promise<WithParsedDates<T, Paths>>;
} {
	type Out = WithParsedDates<T, Paths>;
	const plugged = query.withPlugin(new DateJsonParserPlugin(paths));
	return {
		execute: () => plugged.execute() as Promise<Out[]>,
		executeTakeFirst: () =>
			plugged.executeTakeFirst() as Promise<Out | undefined>,
		executeTakeFirstOrThrow: (errorConstructor) =>
			plugged.executeTakeFirstOrThrow(errorConstructor) as Promise<Out>,
	};
}
