import { StandardSchemaV1 } from '@standard-schema/spec';
import { DeepPartial, IsAny, MaybePromise } from 'types';

export * from './index.js';

type ImageInputValue = { x: number; y: number };

type IsImageInputValue<T> = T extends ImageInputValue
	? Exclude<keyof T, keyof ImageInputValue> extends never
		? true
		: false
	: false;

// If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;

// Input type mappings for form fields
type InputTypeMap = {
	text: string;
	email: string;
	password: string;
	url: string;
	tel: string;
	search: string;
	number: number;
	range: number;
	date: string;
	'datetime-local': string;
	time: string;
	month: string;
	week: string;
	color: string;
	checkbox: boolean | string[];
	radio: string;
	file: File;
	hidden: string | number | boolean;
	submit: string | number | boolean;
	button: string;
	reset: string;
	image: ImageInputValue;
	select: string;
	'select multiple': string[];
	'file multiple': File[];
};

// Valid input types for a given value type
export type RemoteFormFieldType<T> = {
	[K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never;
}[keyof InputTypeMap];

// Input element properties based on type
type InputElementProps<T extends keyof InputTypeMap, Value> = T extends 'checkbox' | 'radio'
	? {
			name: string;
			type: T;
			value?: string;
			'aria-invalid': boolean | 'false' | 'true' | undefined;
			get checked(): boolean;
			set checked(value: boolean);
			readonly defaultChecked?: boolean;
		}
	: T extends 'image'
		? {
				name: string;
				type: 'image';
				'aria-invalid': boolean | 'false' | 'true' | undefined;
			}
		: T extends 'file'
			? {
					name: string;
					type: 'file';
					'aria-invalid': boolean | 'false' | 'true' | undefined;
					get files(): FileList | null;
					set files(v: FileList | null);
				}
			: T extends 'select'
				? {
						name: string;
						'aria-invalid': boolean | 'false' | 'true' | undefined;
						get value(): string;
						set value(v: string);
					}
				: T extends 'select multiple'
					? {
							name: string;
							multiple: true;
							'aria-invalid': boolean | 'false' | 'true' | undefined;
							get value(): string[];
							set value(v: string[]);
						}
					: T extends 'text'
						? {
								name: string;
								'aria-invalid': boolean | 'false' | 'true' | undefined;
								get value(): Value extends string ? string : string | number;
								set value(v: Value extends string ? string : string | number);
								readonly defaultValue?: Value extends string ? string : string | number;
							}
						: {
								name: string;
								type: T;
								'aria-invalid': boolean | 'false' | 'true' | undefined;
								get value(): string | number;
								set value(v: string | number);
								readonly defaultValue?: string | number;
							};

type RemoteFormFieldMethods<T> = {
	/** The values that will be submitted */
	value(): DeepPartial<T>;
	/** Set the values that will be submitted */
	set(input: DeepPartial<T>): DeepPartial<T>;
	/** Whether the field or any nested field has been interacted with since the form was mounted */
	touched(): boolean;
	/** Whether the field or any nested field has been edited since the form was mounted */
	dirty(): boolean;
	/** Validation issues, if any */
	issues(): RemoteFormIssue[] | undefined;
};

// These two types use "T extends unknown ? .. : .." to distribute over unions.
// Example: if "type T = A | b" then "keyof T" only contains keys that both A and B have, with "KeysOfUnion<T>" we get the keys of both A and B
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
type ValueOfUnionKey<T, K extends PropertyKey> = T extends unknown
	? K extends keyof T
		? T[K]
		: never
	: never;

export type RemoteFormFieldValue =
	| string
	| string[]
	| number
	| boolean
	| File
	| File[]
	| ImageInputValue;

type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
	? Value extends string[]
		? [type: Type, value: Value[number] | (string & {}), checked?: boolean]
		: Value extends boolean
			? [type: Type, value?: boolean]
			: [type: Type, value?: Value]
	: Type extends 'image'
		? [type: Type]
		: Type extends 'submit' | 'hidden'
			? Value extends string
				? [type: Type, value: Value | (string & {})]
				: [type: Type, value: Value]
			: Type extends 'radio'
				? [type: Type, value: Value | (string & {}), checked?: boolean]
				: Type extends 'file' | 'file multiple'
					? [type: Type]
					: [type: Type, value?: Value];

type WidenLiteralString<T> = T extends string ? (string extends T ? T : string) : T;

/**
 * Form field accessor type that provides name(), value(), and issues() methods
 */
export type RemoteFormField<Value extends RemoteFormFieldValue> = RemoteFormFieldMethods<Value> & {
	/**
	 * Returns an object that can be spread onto an input element with the correct type attribute,
	 * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
	 * @example
	 * ```svelte
	 * <input {...myForm.fields.myString.as('text')} />
	 * <input {...myForm.fields.myNumber.as('number')} />
	 * <input {...myForm.fields.myBoolean.as('checkbox')} />
	 * ```
	 */
	as<T extends RemoteFormFieldType<Value>>(
		...args: AsArgs<T, Value>
	): InputElementProps<T, WidenLiteralString<Value>>;
};

type RemoteFormFieldContainer<Value> = RemoteFormFieldMethods<Value> & {
	/** Validation issues belonging to this or any of the fields that belong to it, if any */
	allIssues(): RemoteFormIssue[] | undefined;
};

type UnknownField<Value> = RemoteFormFieldMethods<Value> & {
	/** Validation issues belonging to this or any of the fields that belong to it, if any */
	allIssues(): RemoteFormIssue[] | undefined;
	/**
	 * Returns an object that can be spread onto an input element with the correct type attribute,
	 * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters.
	 * @example
	 * ```svelte
	 * <input {...myForm.fields.myString.as('text')} />
	 * <input {...myForm.fields.myNumber.as('number')} />
	 * <input {...myForm.fields.myBoolean.as('checkbox')} />
	 * ```
	 */
	as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T, Value>;
} & {
	[key: string | number]: UnknownField<any>;
};

type RemoteFormFieldsRoot<
	Input extends RemoteFormInput | void,
	Original extends [RemoteFormInput | void] = [Input]
> =
	IsAny<Input> extends true
		? RecursiveFormFields
		: Input extends void
			? {
					/** Validation issues, if any */
					issues(): RemoteFormIssue[] | undefined;
					/** Validation issues belonging to this or any of the fields that belong to it, if any */
					allIssues(): RemoteFormIssue[] | undefined;
				}
			: WillRecurseIndefinitely<Input> extends true
				? RecursiveFormFields
				: RemoteFormFieldContainer<Original[0]> & {
						[K in KeysOfUnion<Original[0]>]-?: RemoteFormFields<ValueOfUnionKey<Original[0], K>>;
					};

/**
 * Recursive type to build form fields structure with proxy access
 */
export type RemoteFormFields<T> =
	WillRecurseIndefinitely<T> extends true
		? RecursiveFormFields
		: NonNullable<T> extends string | number | boolean | File
			? RemoteFormField<NonNullable<T>>
			: IsImageInputValue<NonNullable<T>> extends true
				? RemoteFormField<NonNullable<T> & ImageInputValue> &
						Pick<RemoteFormFieldContainer<T>, 'allIssues'> & {
							[K in KeysOfUnion<T>]-?: RemoteFormFields<ValueOfUnionKey<T, K>>;
						}
				: // [NonNullable<T>] is used to prevent distributing over union while still allowing
					// nullable wrappers (e.g. `string[] | undefined` from a schema with `.default([])`)
					// to be treated as arrays; only the last condition should distribute over unions
					[NonNullable<T>] extends [string[] | File[]]
					? RemoteFormField<NonNullable<T>> & {
							[K in number]: RemoteFormField<NonNullable<T>[number]>;
						}
					: [NonNullable<T>] extends [Array<infer U>]
						? RemoteFormFieldContainer<NonNullable<T>> & {
								[K in number]: RemoteFormFields<U>;
							}
						: RemoteFormFieldContainer<T> & {
								[K in KeysOfUnion<T>]-?: RemoteFormFields<ValueOfUnionKey<T, K>>;
							};

// By breaking this out into its own type, we avoid the TS recursion depth limit
type RecursiveFormFields = RemoteFormFieldContainer<any> & {
	[key: string | number]: UnknownField<any>;
};

type MaybeArray<T> = T | T[];

export interface RemoteFormInput {
	[key: string]: MaybeArray<string | number | boolean | File | RemoteFormInput> | undefined;
}

export interface RemoteFormIssue {
	message: string;
	path: Array<string | number>;
}

// If the schema specifies `id` as a string or number, ensure that `for(...)`
// only accepts that type. Otherwise, accept `string | number`
type ExtractId<Input> = Input extends { id: infer Id }
	? Id extends string | number
		? Id
		: string | number
	: string | number;

/**
 * A function and proxy object used to imperatively create validation errors in form handlers.
 *
 * Access properties to create field-specific issues: `issue.fieldName('message')`.
 * The type structure mirrors the input data structure for type-safe field access.
 * Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
 */
export type RemoteFormInvalidField<T> =
	WillRecurseIndefinitely<T> extends true
		? Record<string | number, any>
		: NonNullable<T> extends string | number | boolean | File
			? (message: string) => StandardSchemaV1.Issue
			: NonNullable<T> extends Array<infer U>
				? {
						[K in number]: RemoteFormInvalidField<U>;
					} & ((message: string) => StandardSchemaV1.Issue)
				: NonNullable<T> extends RemoteFormInput
					? {
							[K in keyof T]-?: RemoteFormInvalidField<T[K]>;
						} & ((message: string) => StandardSchemaV1.Issue)
					: Record<string, never>;

/**
 * The form instance as received inside an `enhance` callback. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
 */
export type RemoteFormEnhanceInstance<
	Input extends RemoteFormInput | void = RemoteFormInput | void,
	Output = any
> = Omit<RemoteForm<Input, Output>, 'enhance' | 'element'> & {
	readonly element: HTMLFormElement;
};

/**
 * The callback passed to a remote form's `enhance` method. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
 */
export type RemoteFormEnhanceCallback<
	Input extends RemoteFormInput | void = RemoteFormInput | void,
	Output = any
> = (form: RemoteFormEnhanceInstance<Input, Output>) => MaybePromise<void>;

/**
 * The type of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
 */
export type RemoteForm<Input extends RemoteFormInput | void, Output> = RemoteForm_<
	Input,
	Output,
	[Input]
>;

type RemoteForm_<
	Input extends RemoteFormInput | void,
	Output,
	Original extends [RemoteFormInput | void]
> = {
	/** Attachment that sets up an event handler that intercepts the form submission on the client to prevent a full page reload */
	[attachment: symbol]: (node: HTMLFormElement) => void;
	method: 'POST';
	/** The URL to send the form to. */
	action: string;
	/** The `<form>` element this instance is currently attached to, if any. */
	get element(): HTMLFormElement | null;
	/** Submit the currently attached form programmatically. */
	submit(): Promise<boolean> & {
		updates: (...updates: RemoteQueryUpdate[]) => Promise<boolean>;
	};
	/** Use the `enhance` method to influence what happens when the form is submitted. */
	enhance(callback: RemoteFormEnhanceCallback<Input, Output>): {
		method: 'POST';
		action: string;
		[attachment: symbol]: (node: HTMLFormElement) => void;
	};
	/**
	 * Create an instance of the form for the given `id`.
	 * The `id` is stringified and used for deduplication to potentially reuse existing instances.
	 * Useful when you have multiple forms that use the same remote form action, for example in a loop.
	 * ```svelte
	 * {#each todos as todo}
	 *	{const todoForm = updateTodo.for(todo.id)}
	 *	<form {...todoForm}>
	 *		{#if todoForm.result?.invalid}<p>Invalid data</p>{/if}
	 *		...
	 *	</form>
	 *	{/each}
	 * ```
	 */
	for(id: ExtractId<Input>): Omit<RemoteForm<Input, Output>, 'for'>;
	/** Preflight checks */
	preflight(schema: StandardSchemaV1<Input, any>): RemoteForm<Input, Output>;
	/** Validate the form contents programmatically */
	validate(options?: {
		/**
		 * Set this to `true` to also show validation issues of fields that haven't yet been
		 * edited and blurred. This option is ignored for forms that have previously been
		 * submitted, in which case all fields are always subject to validation
		 * (unless the form is reset, at which point it is treated as pristine)
		 */
		all?: boolean;
		/** Set this to `true` to only run the `preflight` validation. */
		preflightOnly?: boolean;
	}): Promise<void>;
	/** The result of the form submission */
	get result(): Output | undefined;
	/** The number of pending submissions */
	get pending(): number;
	/** True if the form has been submitted at least once, and hasn't been reset since */
	get submitted(): boolean;
	/** Access form fields using object notation */
	fields: RemoteFormFieldsRoot<Input, Original>;
};

/**
 * The type of a remote `command` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#command) for full documentation.
 */
export type RemoteCommand<Input, Output> = {
	(arg: undefined extends Input ? Input | void : Input): Promise<Output> & {
		updates(...updates: RemoteQueryUpdate[]): Promise<Output>;
	};
	/** The number of pending command executions */
	get pending(): number;
};

export type RemoteQueryUpdate =
	| RemoteQuery<any>
	| RemoteLiveQuery<any>
	| RemoteQueryFunction<any, any>
	| RemoteLiveQueryFunction<any, any>
	| RemoteQueryOverride;

export type RemoteResource<T> = Promise<T> & {
	/** The error in case the query fails. */
	get error(): App.Error | undefined;
	/** `true` before the first result is available and during refreshes */
	get loading(): boolean;
} & (
		| {
				/** The current value of the query. Undefined until `ready` is `true` */
				get current(): undefined;
				ready: false;
		  }
		| {
				/** The current value of the query. Undefined until `ready` is `true` */
				get current(): T;
				ready: true;
		  }
	);

export type RemoteQuery<T> = RemoteResource<T> & {
	/**
	 * On the client, this function will update the value of the query without re-fetching it.
	 *
	 * On the server, this can be called in the context of a `command` or `form` and the specified data will accompany the action response back to the client.
	 * This prevents SvelteKit needing to refresh all queries on the page in a second server round-trip.
	 */
	set(value: T): void;
	/**
	 * On the client, this function will re-fetch the query from the server.
	 *
	 * On the server, this can be called in the context of a `command` or `form` and the refreshed data will accompany the action response back to the client.
	 * This prevents SvelteKit needing to refresh all queries on the page in a second server round-trip.
	 */
	refresh(): Promise<void>;
	/**
	 * Temporarily override a query's value during a [single-flight mutation](https://svelte.dev/docs/kit/remote-functions#Single-flight-mutations) to provide optimistic updates.
	 *
	 * ```svelte
	 * <script>
	 *   import { getTodos, addTodo } from './todos.remote.js';
	 *   const todos = getTodos();
	 * </script>
	 *
	 * <form {...addTodo.enhance(async (form) => {
	 *   await form.submit().updates(
	 *     todos.withOverride((todos) => [...todos, { text: form.fields.text.value() }])
	 *   );
	 * })}>
	 *   <input type="text" name="text" />
	 *   <button type="submit">Add Todo</button>
	 * </form>
	 * ```
	 */
	withOverride(update: (current: T) => T): RemoteQueryOverride;
};

export type RemoteLiveQuery<T> = RemoteResource<T> &
	AsyncIterable<T> & {
		/** `true` if the live stream is currently connected. */
		readonly connected: boolean;
		/** `true` once the current live stream iterator is done. */
		readonly done: boolean;
		/** Reconnects the live stream immediately. */
		reconnect(): Promise<void>;
	};

export type RemoteQueryOverride = () => void;

/**
 * The type of a remote `prerender` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#prerender) for full documentation.
 */
export type RemotePrerenderFunction<Input, Output> = (
	arg: undefined extends Input ? Input | void : Input
) => RemoteResource<Output>;

/**
 * The return value of a remote `query` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query) for full documentation.
 *
 * The optional `Validated` generic parameter represents the argument type *after* the
 * query's schema has validated and (optionally) transformed it — this is the type the
 * query's implementation function receives on the server, and the type yielded by
 * [`requested`](https://svelte.dev/docs/kit/$app-server#requested). For queries declared
 * with [Standard Schema](https://standardschema.dev/) it differs from `Input` when the
 * schema contains a transform (e.g. `v.pipe(v.number(), v.transform(String))` has
 * `Input = number` but `Validated = string`). For `'unchecked'` validators and queries
 * without arguments it defaults to `Input`.
 */
export type RemoteQueryFunction<Input, Output, _Validated = Input> = (
	arg: undefined extends Input ? Input | void : Input
) => RemoteQuery<Output>;

/**
 * The type of a remote `query.live` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#query.live) for full documentation.
 *
 * The optional `Validated` generic parameter represents the argument type *after* the
 * query's schema has validated and (optionally) transformed it, and matches the type
 * yielded by [`requested`](https://svelte.dev/docs/kit/$app-server#requested).
 */
export type RemoteLiveQueryFunction<Input, Output, _Validated = Input> = (
	arg: undefined extends Input ? Input | void : Input
) => RemoteLiveQuery<Output>;

/**
 * A single entry yielded by [`requested`](https://svelte.dev/docs/kit/$app-server#requested)
 * when called with a regular `query`. `arg` is the validated argument (the input *after*
 * the query's schema validated and transformed it, if applicable); `query` is a
 * `RemoteQuery` bound to the client's original cache key, so `refresh()` / `set()` will
 * update the correct client entry.
 */
export type RequestedEntry<Validated, Output> = {
	arg: Validated;
	query: RemoteQuery<Output>;
	/** Explicitly ignore this requested update. */
	ignore: () => void;
};

/**
 * A single entry yielded by [`requested`](https://svelte.dev/docs/kit/$app-server#requested)
 * when called with a `query.live`. `arg` is the validated argument; `query` is a
 * `RemoteLiveQuery` bound to the client's original cache key, so `reconnect()` targets
 * the correct client subscription.
 */
export type RemoteLiveQueryRequestedEntry<Validated, Output> = {
	arg: Validated;
	query: RemoteLiveQuery<Output>;
	/** Explicitly ignore this requested update. */
	ignore: () => void;
};

export type RemoteQueryRequestedResult<Validated, Output> = Iterable<
	RequestedEntry<Validated, Output>
> &
	AsyncIterable<RequestedEntry<Validated, Output>> & {
		/**
		 * Call `refresh` on all queries selected by this `requested` invocation.
		 * This is identical to:
		 * ```ts
		 * import { requested } from '$app/server';
		 *
		 * for await (const { query } of requested(getPost, ...)) {
		 *   void query.refresh();
		 * }
		 * ```
		 */
		refreshAll: () => Promise<void>;
		/** Explicitly ignore all updates selected by this `requested` invocation. */
		ignoreAll: () => Promise<void>;
	};

export type RemoteLiveQueryRequestedResult<Validated, Output> = Iterable<
	RemoteLiveQueryRequestedEntry<Validated, Output>
> &
	AsyncIterable<RemoteLiveQueryRequestedEntry<Validated, Output>> & {
		/**
		 * Call `reconnect` on all live queries selected by this `requested` invocation.
		 * This is identical to:
		 * ```ts
		 * import { requested } from '$app/server';
		 *
		 * for await (const { query } of requested(liveQuery, ...)) {
		 *   void query.reconnect();
		 * }
		 * ```
		 */
		reconnectAll: () => Promise<void>;
		/** Explicitly ignore all updates selected by this `requested` invocation. */
		ignoreAll: () => Promise<void>;
	};

export type RequestedResult<Validated, Output> =
	| RemoteQueryRequestedResult<Validated, Output>
	| RemoteLiveQueryRequestedResult<Validated, Output>;
