import PostgrestBuilder from "./PostgrestBuilder";
import PostgrestFilterBuilder from "./PostgrestFilterBuilder";
import { GetResult } from "./select-query-parser";
import { Fetch, GenericSchema, GenericTable, GenericView } from "./types";

export default class PostgrestQueryBuilder<
	Schema extends GenericSchema,
	Relation extends GenericTable | GenericView,
> {
	url: URL;
	headers: Record<string, string>;
	schema?: string;
	signal?: AbortSignal;
	fetch?: Fetch;

	constructor(
		url: URL,
		{
			headers = {},
			schema,
			fetch,
		}: {
			headers?: Record<string, string>;
			schema?: string;
			fetch?: Fetch;
		},
	) {
		this.url = url;
		this.headers = headers;
		this.schema = schema;
		this.fetch = fetch;
	}

	/**
	 * Perform a SELECT query on the table or view.
	 *
	 * @param columns - The columns to retrieve, separated by commas. Columns can be renamed when returned with `customName:columnName`
	 *
	 * @param options - Named parameters
	 *
	 * @param options.head - When set to `true`, `data` will not be returned.
	 * Useful if you only need the count.
	 *
	 * @param options.count - Count algorithm to use to count rows in the table or view.
	 *
	 * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
	 * hood.
	 *
	 * `"planned"`: Approximated but fast count algorithm. Uses the Postgres
	 * statistics under the hood.
	 *
	 * `"estimated"`: Uses exact count for low numbers and planned count for high
	 * numbers.
	 */
	select<
		Query extends string = "*",
		ResultOne = GetResult<Schema, Relation["Row"], Query>,
	>(
		columns?: Query,
		{
			head = false,
			count,
		}: {
			head?: boolean;
			count?: "exact" | "planned" | "estimated";
		} = {},
	): PostgrestFilterBuilder<Schema, Relation["Row"], ResultOne[]> {
		const method = head ? "HEAD" : "GET";
		// Remove whitespaces except when quoted
		let quoted = false;
		const cleanedColumns = (columns ?? "*")
			.split("")
			.map((c) => {
				if (/\s/.test(c) && !quoted) {
					return "";
				}
				if (c === '"') {
					quoted = !quoted;
				}
				return c;
			})
			.join("");
		this.url.searchParams.set("select", cleanedColumns);
		if (count) {
			this.headers["Prefer"] = `count=${count}`;
		}

		return new PostgrestFilterBuilder({
			method,
			url: this.url,
			headers: this.headers,
			schema: this.schema,
			fetch: this.fetch,
			allowEmpty: false,
		} as unknown as PostgrestBuilder<ResultOne[]>);
	}

	/**
	 * Perform an INSERT into the table or view.
	 *
	 * By default, inserted rows are not returned. To return it, chain the call
	 * with `.select()`.
	 *
	 * @param values - The values to insert. Pass an object to insert a single row
	 * or an array to insert multiple rows.
	 *
	 * @param options - Named parameters
	 *
	 * @param options.count - Count algorithm to use to count inserted rows.
	 *
	 * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
	 * hood.
	 *
	 * `"planned"`: Approximated but fast count algorithm. Uses the Postgres
	 * statistics under the hood.
	 *
	 * `"estimated"`: Uses exact count for low numbers and planned count for high
	 * numbers.
	 */
	insert<
		Row extends Relation extends { Insert: unknown }
			? Relation["Insert"]
			: never,
	>(
		values: Row | Row[],
		{
			count,
		}: {
			count?: "exact" | "planned" | "estimated";
		} = {},
	): PostgrestFilterBuilder<Schema, Relation["Row"], null> {
		const method = "POST";

		const prefersHeaders = [];
		const body = values;
		if (count) {
			prefersHeaders.push(`count=${count}`);
		}
		if (this.headers["Prefer"]) {
			prefersHeaders.unshift(this.headers["Prefer"]);
		}
		this.headers["Prefer"] = prefersHeaders.join(",");

		if (Array.isArray(values)) {
			const columns = values.reduce(
				(acc, x) => acc.concat(Object.keys(x)),
				[] as string[],
			);
			if (columns.length > 0) {
				const uniqueColumns = [...new Set(columns)].map(
					(column) => `"${column}"`,
				);
				this.url.searchParams.set("columns", uniqueColumns.join(","));
			}
		}

		return new PostgrestFilterBuilder({
			method,
			url: this.url,
			headers: this.headers,
			schema: this.schema,
			body,
			fetch: this.fetch,
			allowEmpty: false,
		} as unknown as PostgrestBuilder<null>);
	}

	/**
	 * Perform an UPSERT on the table or view. Depending on the column(s) passed
	 * to `onConflict`, `.upsert()` allows you to perform the equivalent of
	 * `.insert()` if a row with the corresponding `onConflict` columns doesn't
	 * exist, or if it does exist, perform an alternative action depending on
	 * `ignoreDuplicates`.
	 *
	 * By default, upserted rows are not returned. To return it, chain the call
	 * with `.select()`.
	 *
	 * @param values - The values to upsert with. Pass an object to upsert a
	 * single row or an array to upsert multiple rows.
	 *
	 * @param options - Named parameters
	 *
	 * @param options.onConflict - Comma-separated UNIQUE column(s) to specify how
	 * duplicate rows are determined. Two rows are duplicates if all the
	 * `onConflict` columns are equal.
	 *
	 * @param options.ignoreDuplicates - If `true`, duplicate rows are ignored. If
	 * `false`, duplicate rows are merged with existing rows.
	 *
	 * @param options.count - Count algorithm to use to count upserted rows.
	 *
	 * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
	 * hood.
	 *
	 * `"planned"`: Approximated but fast count algorithm. Uses the Postgres
	 * statistics under the hood.
	 *
	 * `"estimated"`: Uses exact count for low numbers and planned count for high
	 * numbers.
	 */
	upsert<
		Row extends Relation extends { Insert: unknown }
			? Relation["Insert"]
			: never,
	>(
		values: Row | Row[],
		{
			onConflict,
			ignoreDuplicates = false,
			count,
		}: {
			onConflict?: string;
			ignoreDuplicates?: boolean;
			count?: "exact" | "planned" | "estimated";
		} = {},
	): PostgrestFilterBuilder<Schema, Relation["Row"], null> {
		const method = "POST";

		const prefersHeaders = [
			`resolution=${ignoreDuplicates ? "ignore" : "merge"}-duplicates`,
		];

		if (onConflict !== undefined)
			this.url.searchParams.set("on_conflict", onConflict);
		const body = values;
		if (count) {
			prefersHeaders.push(`count=${count}`);
		}
		if (this.headers["Prefer"]) {
			prefersHeaders.unshift(this.headers["Prefer"]);
		}
		this.headers["Prefer"] = prefersHeaders.join(",");

		return new PostgrestFilterBuilder({
			method,
			url: this.url,
			headers: this.headers,
			schema: this.schema,
			body,
			fetch: this.fetch,
			allowEmpty: false,
		} as unknown as PostgrestBuilder<null>);
	}

	/**
	 * Perform an UPDATE on the table or view.
	 *
	 * By default, updated rows are not returned. To return it, chain the call
	 * with `.select()` after filters.
	 *
	 * @param values - The values to update with
	 *
	 * @param options - Named parameters
	 *
	 * @param options.count - Count algorithm to use to count updated rows.
	 *
	 * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
	 * hood.
	 *
	 * `"planned"`: Approximated but fast count algorithm. Uses the Postgres
	 * statistics under the hood.
	 *
	 * `"estimated"`: Uses exact count for low numbers and planned count for high
	 * numbers.
	 */
	update<
		Row extends Relation extends { Update: unknown }
			? Relation["Update"]
			: never,
	>(
		values: Row,
		{
			count,
		}: {
			count?: "exact" | "planned" | "estimated";
		} = {},
	): PostgrestFilterBuilder<Schema, Relation["Row"], null> {
		const method = "PATCH";
		const prefersHeaders = [];
		const body = values;
		if (count) {
			prefersHeaders.push(`count=${count}`);
		}
		if (this.headers["Prefer"]) {
			prefersHeaders.unshift(this.headers["Prefer"]);
		}
		this.headers["Prefer"] = prefersHeaders.join(",");

		return new PostgrestFilterBuilder({
			method,
			url: this.url,
			headers: this.headers,
			schema: this.schema,
			body,
			fetch: this.fetch,
			allowEmpty: false,
		} as unknown as PostgrestBuilder<null>);
	}

	/**
	 * Perform a DELETE on the table or view.
	 *
	 * By default, deleted rows are not returned. To return it, chain the call
	 * with `.select()` after filters.
	 *
	 * @param options - Named parameters
	 *
	 * @param options.count - Count algorithm to use to count deleted rows.
	 *
	 * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
	 * hood.
	 *
	 * `"planned"`: Approximated but fast count algorithm. Uses the Postgres
	 * statistics under the hood.
	 *
	 * `"estimated"`: Uses exact count for low numbers and planned count for high
	 * numbers.
	 */
	delete({
		count,
	}: {
		count?: "exact" | "planned" | "estimated";
	} = {}): PostgrestFilterBuilder<Schema, Relation["Row"], null> {
		const method = "DELETE";
		const prefersHeaders = [];
		if (count) {
			prefersHeaders.push(`count=${count}`);
		}
		if (this.headers["Prefer"]) {
			prefersHeaders.unshift(this.headers["Prefer"]);
		}
		this.headers["Prefer"] = prefersHeaders.join(",");

		return new PostgrestFilterBuilder({
			method,
			url: this.url,
			headers: this.headers,
			schema: this.schema,
			fetch: this.fetch,
			allowEmpty: false,
		} as unknown as PostgrestBuilder<null>);
	}
}
