import { App, Form } from "antd";
import { useCallback, useRef, useState } from "react";

import { yieldMainThread } from "@devowl-wp/react-utils";

import { Prompt } from "../components/common/prompt.js";

import type { ComponentProps } from "react";

function useFormHandler<Values extends Record<string, any>, Template extends { version?: number }>({
    isEdit,
    defaultValues,
    template,
    entityTemplateVersion,
    attributes,
    handleSave,
    i18n,
    initialHasChanges,
    trackFieldsDifferFromDefaultValues = [],
    unloadPromptWhen,
}: {
    isEdit?: boolean;
    defaultValues: Values;
    template?: Template;
    entityTemplateVersion?: number;
    /**
     * If given, it overwrites the default values (higher priority than the values from `defaultValues`).
     */
    attributes?: Partial<Values>;
    initialHasChanges?: boolean;
    trackFieldsDifferFromDefaultValues?: Array<keyof Values>;
    handleSave: (value: Values) => Promise<void | (() => Promise<void>) | (() => void)>;
    i18n: {
        successMessage: string;
        validationError: string;
        unloadConfirm: string;
        unloadConfirmInitialActive?: string;
    };
    unloadPromptWhen?: Extract<ComponentProps<typeof Prompt>["when"], (...args: any[]) => any>;
}) {
    const { message } = App.useApp();
    const useDefaultValues = { ...defaultValues, ...(attributes || {}) };
    const [form] = Form.useForm<Values>();
    const [isBusy, setIsBusy] = useState(false);
    const hasChanges = useRef(initialHasChanges || false); // use ref instead of state to avoid complete form rerender
    const isTemplateUpdate = isEdit && template ? entityTemplateVersion !== template.version : false;
    const templateCheck = template ? isTemplateUpdate || !isEdit : false;

    // Do not hold in state as it could lead to input focus loss due to reactivity. Use `hasTrackedFieldDifferenceToDefaultValue` instead.
    const trackedFieldsDifferFromDefaultValues: Array<keyof Values> = [];
    const hasTrackedFieldDifferenceToDefaultValue = (field: keyof Values) =>
        trackedFieldsDifferFromDefaultValues.indexOf(field) > -1;

    const onFinish: ComponentProps<typeof Form>["onFinish"] = useCallback(
        async (values) => {
            setIsBusy(true);
            try {
                const callback = await handleSave(values as Values);
                message.success(i18n.successMessage);

                // Wait until `initialValues` got reinitialized through antd so we do not revert
                // back to a previous form state.
                yieldMainThread().then(() => {
                    form.resetFields();
                    if (typeof callback === "function") {
                        callback();
                    }
                });

                hasChanges.current = false;
            } catch (e) {
                message.error(e);
            } finally {
                setIsBusy(false);
            }
        },
        [form, handleSave],
    );

    const onFinishFailed: ComponentProps<typeof Form>["onFinishFailed"] = useCallback(() => {
        message.error(i18n.validationError);
    }, [form, i18n]);

    const prompt = (
        <Prompt
            when={(blockerFunction) =>
                hasChanges.current && (unloadPromptWhen ? unloadPromptWhen(blockerFunction) : true)
            }
            title={
                initialHasChanges && i18n.unloadConfirmInitialActive
                    ? i18n.unloadConfirmInitialActive
                    : i18n.unloadConfirm
            }
        />
    );

    const onValuesChange = useCallback(
        (changedValues: Record<string, any>, values: Record<string, any>) => {
            if (trackFieldsDifferFromDefaultValues && useDefaultValues) {
                // Change without reactivity and keep reference so `hasTrackedFieldDifferenceToDefaultValue` keeps intact
                trackedFieldsDifferFromDefaultValues.splice(0, trackedFieldsDifferFromDefaultValues.length);

                for (const trackFieldChange of trackFieldsDifferFromDefaultValues as string[]) {
                    const currentValue = values[trackFieldChange];
                    const defaultValue = useDefaultValues[trackFieldChange];
                    if (currentValue !== defaultValue) {
                        trackedFieldsDifferFromDefaultValues.push(trackFieldChange);
                    }
                }
            }
            hasChanges.current = true;
        },
        [trackFieldsDifferFromDefaultValues, useDefaultValues],
    );

    return {
        defaultValues: useDefaultValues,
        template,
        isEdit,
        isTemplateUpdate,
        templateCheck,
        form,
        isBusy,
        setIsBusy,
        hasTrackedFieldDifferenceToDefaultValue,
        onFinish,
        /**
         * Cast to avoid this error:
         *
         * ```
         * The inferred type of 'useFormContentBlockerHandler' cannot be named without a reference to '.pnpm/rc-field-form@1.27.4_wcqkhtmu7mswc6yz4uyexck3ty/node_modules/rc-field-form/lib/interface'. This is likely not portable. A type annotation is necessary.
         * ```
         */
        onFinishFailed: onFinishFailed as (errorInfo: any) => void,
        prompt,
        onValuesChange,
    };
}

export { useFormHandler };
