import { StandardSchemaV1 } from '@standard-schema/spec';
import { NavigationEvent, RequestEvent } from '@sveltejs/kit';
import { MaybePromise } from 'types';

export * from './index.js';

/**
 * The [`handle`](https://svelte.dev/docs/kit/hooks#handle) hook runs every time the SvelteKit server receives a [request](https://svelte.dev/docs/kit/web-standards#Fetch-APIs-Request) and
 * determines the [response](https://svelte.dev/docs/kit/web-standards#Fetch-APIs-Response).
 * It receives an `event` object representing the request and a function called `resolve`, which renders the route and generates a `Response`.
 * This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
 */
export type Handle = (input: {
	event: RequestEvent;
	resolve: (event: RequestEvent, opts?: ResolveOptions) => Promise<Response>;
}) => MaybePromise<Response>;

type CaughtErrorMap = {
	app: App.Error;
	framework: { status: number; message: string };
	unknown: unknown;
};

type ValidationCaughtError<Issue extends StandardSchemaV1.Issue> = {
	kind: 'validation';
	error: { status: number; message: string };
	issues: Issue[];
};

/**
 * The error passed to the [`handleError`](https://svelte.dev/docs/kit/hooks#handleError) hooks.
 * Use the `kind` discriminant to distinguish errors from your app (thrown with the
 * [`error`](https://svelte.dev/docs/kit/errors#App-errors) helper), errors generated by
 * SvelteKit itself (such as 404s), validation errors, and unknown errors (thrown by your code,
 * or code it calls).
 */
export type CaughtError<Issue extends StandardSchemaV1.Issue = StandardSchemaV1.Issue> =
	| {
			[Kind in keyof CaughtErrorMap]: {
				/** Identifies the category and origin of the error */
				kind: Kind;
				/** The caught error. Its type depends on `kind` */
				error: CaughtErrorMap[Kind];
				/** Only present for validation errors */
				issues?: undefined;
			};
	  }[keyof CaughtErrorMap]
	| ValidationCaughtError<Issue>;

/** The error passed to the client-side `handleError` hook. */
export type ClientCaughtError = Exclude<CaughtError, { kind: 'validation' }>;

/**
 * The server-side [`handleError`](https://svelte.dev/docs/kit/hooks#handleError) hook runs for every error thrown while responding to a request, except redirects.
 *
 * The `kind` property discriminates between _app_ errors (thrown with the [`error`](https://svelte.dev/docs/kit/errors#App-errors) helper),
 * _framework_ errors (generated by SvelteKit itself, such as 404s), _validation_ errors (caused by invalid remote function arguments)
 * and _unknown_ errors (thrown by your code, or code it calls).
 *
 * The hook returns an object matching `App.Error`, in which `status` and `message` are optional — return them only to
 * override the defaults. Omitted properties are inherited from the caught error: the body passed to `error(...)` for app errors,
 * the status and safe message for framework and validation errors, and `500`/`'Internal Error'` for unknown errors. Return nothing to
 * keep the defaults entirely (if you augment `App.Error` with required properties, you must return those).
 *
 * Make sure that this function _never_ throws an error.
 */
export type HandleServerError<Issue extends StandardSchemaV1.Issue = StandardSchemaV1.Issue> = (
	input: CaughtError<Issue> & { event: RequestEvent }
) => MaybePromise<AppErrorWithOptionalDefaults | VoidIfNoRequiredAppErrorProperties>;

/**
 * The client-side [`handleError`](https://svelte.dev/docs/kit/hooks#handleError) hook runs for every error thrown while navigating, except redirects.
 * Errors that were already transformed by the server-side hook are not passed to it a second time.
 *
 * The `kind` property discriminates between _app_ errors (thrown with the [`error`](https://svelte.dev/docs/kit/errors#App-errors) helper),
 * _framework_ errors (generated by SvelteKit itself, such as 404s) and _unknown_ errors (thrown by your code, or code it calls).
 *
 * The hook returns an object matching `App.Error`, in which `status` and `message` are optional — return them only to
 * override the defaults. Omitted properties are inherited from the caught error: the body passed to `error(...)` for app errors,
 * the status and safe message for framework errors, and `500`/`'Internal Error'` for unknown errors. Return nothing to
 * keep the defaults entirely (if you augment `App.Error` with required properties, you must return those).
 *
 * Make sure that this function _never_ throws an error.
 */
export type HandleClientError = (
	input: ClientCaughtError & { event: NavigationEvent }
) => MaybePromise<AppErrorWithOptionalDefaults | VoidIfNoRequiredAppErrorProperties>;

/**
 * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#handleFetch) hook allows you to modify (or replace) the result of an [`event.fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests) call that runs on the server (or during prerendering) inside an endpoint, `load`, `action`, `handle`, `handleError` or `reroute`.
 */
export type HandleFetch = (input: {
	event: RequestEvent;
	request: Request;
	fetch: typeof fetch;
}) => MaybePromise<Response>;

/**
 * The [`init`](https://svelte.dev/docs/kit/hooks#init) will be invoked before the server responds to its first request
 * @since 2.10.0
 */
export type ServerInit = () => MaybePromise<void>;

/**
 * The [`init`](https://svelte.dev/docs/kit/hooks#init) will be invoked once the app starts in the browser
 * @since 2.10.0
 */
export type ClientInit = () => MaybePromise<void>;

/**
 * The [`reroute`](https://svelte.dev/docs/kit/hooks#reroute) hook allows you to modify the URL before it is used to determine which route to render.
 * @since 2.3.0
 */
export type Reroute = (event: { url: URL; fetch: typeof fetch }) => MaybePromise<void | string>;

/**
 * The [`transport`](https://svelte.dev/docs/kit/hooks#transport) hook allows you to transport custom types across the server/client boundary.
 *
 * Each transporter has a pair of `encode` and `decode` functions. On the server, `encode` determines whether a value is an instance of the custom type and, if so, returns a non-falsy encoding of the value which can be an object or an array (or `false` otherwise).
 *
 * In the browser, `decode` turns the encoding back into an instance of the custom type.
 *
 * ```ts
 * import type { Transport } from '@sveltejs/kit/hooks';
 *
 * declare class MyCustomType {
 * 	data: any
 * }
 *
 * // hooks.js
 * export const transport: Transport = {
 * 	MyCustomType: {
 * 		encode: (value) => value instanceof MyCustomType && [value.data],
 * 		decode: ([data]) => new MyCustomType(data)
 * 	}
 * };
 * ```
 * @since 2.11.0
 */
export type Transport = Record<string, Transporter>;

/**
 * A member of the [`transport`](https://svelte.dev/docs/kit/hooks#transport) hook.
 */
export interface Transporter<
	T = any,
	U = any /* minus falsy values, but we can't properly express that */
> {
	encode: (value: T) => false | U;
	decode: (data: U) => T;
}

export interface ResolveOptions {
	/**
	 * Applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML
	 * (they could include an element's opening tag but not its closing tag, for example)
	 * but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components.
	 * @param input the html chunk and the info if this is the last chunk
	 */
	transformPageChunk?: (input: { html: string; done: boolean }) => MaybePromise<string | undefined>;
	/**
	 * Determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`.
	 * By default, none will be included.
	 * @param name header name
	 * @param value header value
	 */
	filterSerializedResponseHeaders?: (name: string, value: string) => boolean;
	/**
	 * Determines which files should be preloaded. Files are preloaded via `<link>` tags added to the
	 * `<head>` tag; if `output.linkHeaderPreload` is enabled, dynamically rendered pages use the
	 * [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead.
	 * By default, `js` and `css` files will be preloaded.
	 *
	 * For `font` files, `input` also has a `filename` property, the source file's pathname relative
	 * to the project root, so that a filter can match on it instead of the hashed path. `js` and
	 * `css` files are bundled and have no single source file name.
	 * @param input the type of the file and its path
	 */
	preload?: (
		input:
			| { type: 'css' | 'js' | 'asset'; path: string }
			| { type: 'font'; path: string; filename: string }
	) => boolean;
}

type AppErrorWithOptionalDefaults = Omit<App.Error, 'status' | 'message'> & {
	status?: App.Error['status'];
	message?: App.Error['message'];
};

/**
 * `void` is only a valid `handleError` return when `App.Error` adds no required properties
 * beyond `status` and `message` — both of which are optional in the return, since they default
 * to those of the caught error. If `App.Error` is augmented with required properties, the hook
 * must return them, so returning nothing becomes a type error.
 */
type VoidIfNoRequiredAppErrorProperties = {
	status: number;
	message: string;
} extends App.Error
	? void
	: never;
