import * as React from 'react';
import { getFormValues } from '@theoryofnekomata/formxtra';
import fetchPonyfill from 'fetch-ponyfill';
import {NextPage as DefaultNextPage} from 'next';
import {NextApiResponse, NextApiRequest} from './common';

const FormDerivedElementComponent = 'form' as const;

type FormDerivedElement = HTMLElementTagNameMap[typeof FormDerivedElementComponent];

type BaseProps = React.HTMLProps<FormDerivedElement>;

type EncTypeSerializer = (data: unknown) => string;

type EncTypeSerializerMap = Record<string, EncTypeSerializer>;

const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = {
	'application/json': (data: unknown) => JSON.stringify(data),
};

type GetFormValuesOptions = Parameters<typeof getFormValues>[1];

interface FormProps extends Omit<BaseProps, 'action'> {
	action?: string;
	clientAction?: string;
	clientHeaders?: HeadersInit;
	clientMethod?: BaseProps['method'];
	invalidate?: (...args: unknown[]) => unknown;
	refresh?: (response: Response) => void;
	encTypeSerializers?: EncTypeSerializerMap;
	responseEncType?: string;
	serializerOptions?: GetFormValuesOptions;
}

export const useResponse = (res: NextApiResponse) => {
	const [response, setResponse] = React.useState<Response | undefined>(
		res.body ? new Response(res.body as any) : undefined
	);

	const onStale = React.useCallback(() => {
		setResponse(undefined);
	}, []);

	const onFresh = React.useCallback((response: Response) => {
		setResponse(response);
	}, []);

	return React.useMemo(() => ({
		response,
		refresh: onFresh,
		invalidate: onStale,
	}), [
		response,
	]);
};

interface SerializeBodyParams {
	form: HTMLFormElement,
	encType: string,
	serializers: EncTypeSerializerMap,
	options?: GetFormValuesOptions,
}

const serializeBody = (params: SerializeBodyParams) => {
	const {
		form,
		encType,
		serializers,
		options
	} = params;

	if (encType === 'multipart/form-data') {
		// type error when provided a submitter element for some reason...
		const FormDataUnknown = FormData as unknown as {
			new(form?: HTMLElement, submitter?: HTMLElement ): BodyInit;
		};
		return new FormDataUnknown(form, options?.submitter);
	}

	if (encType === 'application/x-www-form-urlencoded') {
		return new URLSearchParams(form);
	}

	if (typeof serializers[encType] === 'function') {
		return serializers[encType](getFormValues(form, options));
	}

	throw new Error(`Unsupported encType: ${encType}`);
}

export const Form = React.forwardRef<FormDerivedElement, FormProps>(({
	children,
	onSubmit,
	action,
	method = 'get',
	clientAction = action,
	clientMethod = method,
	clientHeaders,
	encType = 'multipart/form-data',
	invalidate,
	refresh,
	encTypeSerializers = DEFAULT_ENCTYPE_SERIALIZERS,
	responseEncType = 'application/json',
	serializerOptions,
	...etcProps
}, forwardedRef) => {
	const handleSubmit: React.FormEventHandler<FormDerivedElement> = async (event) => {
		event.preventDefault();
		const nativeEvent = event.nativeEvent as unknown as { submitter?: HTMLElement };

		if (clientAction) {
			invalidate?.();
			const { fetch } = fetchPonyfill();
			const headers: HeadersInit = {
				...(clientHeaders ?? {}),
				'Accept': responseEncType,
			};
			if (encType !== 'multipart/form-data') {
				// browser automatically generates content-type header for multipart/form-data
				(headers as unknown as Record<string, string>)['Content-Type'] = encType;
			}
			const response = await fetch(clientAction, {
				method: clientMethod.toUpperCase(),
				body: serializeBody({
					form: event.currentTarget,
					encType,
					serializers: encTypeSerializers,
					options: {
						...serializerOptions,
						submitter: nativeEvent.submitter,
					},
				}),
				headers,
			});
			refresh?.(response);
		}

		onSubmit?.(event);
	};

	// TODO how to display put/patch method in form? HTML only supports get/post
	//   > throw error when not get/post
	// TODO handle "dialog" method as invalid

	const serverMethodRaw = method.toLowerCase();
	const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post';
	const serverEncType = 'multipart/form-data';

	return (
		<FormDerivedElementComponent
			{...etcProps}
			ref={forwardedRef}
			onSubmit={handleSubmit}
			action={action}
			method={serverMethod}
			encType={serverEncType}
		>
			{children}
		</FormDerivedElementComponent>
	);
});

export type NextPage<T = {}, U = T> = DefaultNextPage<
	T & {
		res: NextApiResponse;
		req: NextApiRequest;
	}, U
>
