import { Form, Modal, Popover, Select, Tag } from "antd";
import { useCallback, useMemo, useRef, useState } from "react";
import ReactDiffViewer, { DiffMethod } from "react-diff-viewer-continued";

import { FORM_VALUE_DIFFER_FROM_PRESET_NOTICE_PSEUDO_ELEMENT_CLASS_NAME } from "./valueDifferFromTemplateNotice.js";
import { useFormContentBlocker } from "../../contexts/formContentBlocker.js";
import { useFormService } from "../../contexts/formService.js";
import { useI18n } from "../../contexts/i18n.js";
import { formContentBlockerAttributesFromTemplate } from "../../hooks/useFormContentBlockerHandler.js";
import { formServiceAttributesFromTemplate } from "../../hooks/useFormServiceHandler.js";

import type { FormContentBlockerContext } from "../../contexts/formContentBlocker.js";
import type { FormServiceContext } from "../../contexts/formService.js";
import type { CSSProperties, ComponentProps, PropsWithChildren, ReactNode } from "react";

const FORM_VALUE_DIFFER_FROM_PRESET_TAG_APPLY_EVENT = "RCB/FormValueDifferFromTemplateTag/Apply";

type FormValueDifferFromTemplateTagProps<Form, FormProps, ValueName, ValueType, ValueDifference> = {
    useModal?: boolean;
    form: Form;
    valueName: ValueName;
    /**
     * Custom difference mechanism. All values except `undefined` are considered as "diff".
     * The difference value (return value) is passed to `renderDif
     */
    difference?: (oldValue: ValueType, newValue: ValueType) => true | ValueDifference;
    apply?: (
        difference: ValueDifference,
        setFieldsValue: (values: Partial<FormProps>) => void,
        oldValue: ValueType,
    ) => void;
    newValueText?: ReactNode | false;
    splitView?: boolean;
    renderDiff?: (value: ValueType, difference: ValueDifference) => ReactNode;
    widthOfRef?: HTMLElement;
    placement?: ComponentProps<typeof Popover>["placement"];
    popoverProps?: ComponentProps<typeof Popover>;
    className?: string;
    noBr?: boolean;
    style?: CSSProperties;
    ignoreEmptyDefaultValue?: boolean;
};

const replacer = (key: string, value: any) =>
    value instanceof Object && !(value instanceof Array)
        ? Object.keys(value)
              .sort()
              .reduce((sorted, key) => {
                  sorted[key] = value[key];
                  return sorted;
              }, {})
        : value;

const diffByJsonStringify = (value: any, difference: any) =>
    JSON.stringify(value, replacer) !== JSON.stringify(difference, replacer);

function FormValueDifferFromTemplateContent<
    Form extends "service" | "blocker",
    FormProps extends Form extends "service"
        ? FormServiceContext["initialState"]["defaultTemplateValues"]
        : FormContentBlockerContext["initialState"]["defaultTemplateValues"],
    ValueDifference,
    ValueName extends keyof FormProps,
    ValueType extends FormProps[ValueName],
>({
    form,
    difference,
    newValueText,
    renderDiff,
    valueName,
    currentValue,
    splitView,
    useModal,
}: PropsWithChildren<
    FormValueDifferFromTemplateTagProps<Form, FormProps, ValueName, ValueType, ValueDifference> & {
        currentValue: ValueType;
    }
>) {
    const { __ } = useI18n();
    const { template } = form === "service" ? useFormService() : useFormContentBlocker();
    const groups = form === "service" ? useFormService().groups || [] : [];
    const showDiff = typeof renderDiff !== "function";
    const [selectedVersionNumber, setSelectedVersionNumber] = useState(template.version);
    const { lang } = document.documentElement;

    const versionValues = useMemo(
        () =>
            [template, ...template.consumerData.versions].map((t) => ({
                ...((form === "service"
                    ? formServiceAttributesFromTemplate(t as any, { groups })
                    : formContentBlockerAttributesFromTemplate(t as any)) as unknown as FormProps),
                version: t.version,
                versionOutput: new Date(t.createdAt).toLocaleDateString(lang),
            })),
        [template, groups],
    );

    const selectOptions = useMemo(() => {
        const findVersionWithNumber = (search: number) => versionValues.filter(({ version }) => version === search)[0];

        return versionValues.map(({ version, versionOutput }, i) => {
            let leftVersion: "currentValue" | (typeof versionValues)[0] = "currentValue";
            let rightVersion: (typeof versionValues)[0] = findVersionWithNumber(version);
            let label =
                version === template.version
                    ? __("Version %s (latest)", versionOutput)
                    : __("Version %s", versionOutput);

            if (showDiff) {
                if (i === 0) {
                    label = `${__("Your value")} → ${label}`;
                } else {
                    const { version: nextVersion, versionOutput: nextVersionOutput } = versionValues[i - 1];
                    label =
                        nextVersion === template.version
                            ? `${label} → ${__("Version %s (latest)", nextVersionOutput)}`
                            : `${label} → ${__("Version %s", nextVersionOutput)}`;
                    leftVersion = findVersionWithNumber(version);
                    rightVersion = findVersionWithNumber(nextVersion);
                }
            }

            return { version, label, leftVersion, rightVersion };
        });
    }, [template.version, versionValues, showDiff, __]);

    const selectedOption = useMemo(
        () => selectOptions.filter(({ version }) => version === selectedVersionNumber)[0],
        [selectOptions, selectedVersionNumber],
    );

    const [oldValue, newValue] = useMemo(() => {
        const { leftVersion, rightVersion } = selectedOption;

        return [
            (leftVersion === "currentValue" ? currentValue : leftVersion[valueName]) as ValueType,
            rightVersion[valueName] as ValueType,
        ];
    }, [currentValue, versionValues, selectedOption]);

    const diff = difference(oldValue, newValue);

    return (
        <>
            <Select
                style={{ float: "right", width: 300, marginRight: useModal ? 15 : 0 }}
                value={selectedVersionNumber}
                onChange={setSelectedVersionNumber}
                size="small"
            >
                {selectOptions.map(({ version, label }) => (
                    <Select.Option key={version} value={version}>
                        {label}
                    </Select.Option>
                ))}
                {selectOptions[selectOptions.length - 1].version !== 1 && (
                    <Select.Option key={0} value={0} disabled>
                        <i>{__("Older versions not available")}</i>
                    </Select.Option>
                )}
            </Select>
            {diff === undefined ? (
                __("The version you selected does not show any difference to your current form value.")
            ) : (
                <>
                    <p className="description" style={{ marginBottom: 10 }}>
                        {__("The value you entered is different from the value in the template.")}
                    </p>
                    {newValueText !== false && (
                        <p>
                            <strong>{newValueText || __("Default value:")}</strong>
                        </p>
                    )}
                    {!showDiff ? (
                        renderDiff(newValue, diff as any)
                    ) : (
                        <div style={{ maxHeight: 200, overflow: "auto" }}>
                            <ReactDiffViewer
                                styles={{
                                    splitView: {
                                        fontSize: 11,
                                    },
                                    contentText: {
                                        lineHeight: "16px !important",
                                    },
                                    wordDiff: {
                                        wordBreak: "break-word !important" as any,
                                        padding: 0,
                                    },
                                    gutter: {
                                        minWidth: "auto !important",
                                    },
                                    marker: {
                                        paddingLeft: "3px !important",
                                        paddingRight: "3px !important",
                                    },
                                }}
                                oldValue={oldValue as any}
                                newValue={newValue as any}
                                compareMethod={DiffMethod.WORDS}
                                splitView={typeof splitView === "boolean" ? splitView : true}
                                showDiffOnly={false}
                            />
                        </div>
                    )}
                </>
            )}
        </>
    );
}

function FormValueDifferFromTemplateTag<
    Form extends "service" | "blocker",
    FormProps extends Form extends "service"
        ? FormServiceContext["initialState"]["defaultTemplateValues"]
        : FormContentBlockerContext["initialState"]["defaultTemplateValues"],
    ValueDifference,
    ValueName extends keyof FormProps,
    ValueType extends FormProps[ValueName],
>(
    props: PropsWithChildren<
        FormValueDifferFromTemplateTagProps<Form, FormProps, ValueName, ValueType, ValueDifference>
    >,
) {
    const {
        useModal,
        form,
        valueName,
        difference,
        apply,
        widthOfRef,
        placement = "bottomLeft",
        noBr = false,
        className,
        style,
        ignoreEmptyDefaultValue,
        popoverProps,
    } = props;
    const { __ } = useI18n();
    const {
        template,
        defaultTemplateValues: { [valueName]: defaultValue },
    } = form === "service" ? useFormService() : useFormContentBlocker();
    const [isModalVisible, setIsModalVisible] = useState(false);

    // Memoize width as it could be `undefined` due to UI lacks
    const widthRef = useRef<number>();

    const calculateDifference = useCallback(
        (value: ValueType, defaultValue: ValueType) => {
            return difference ? difference(value, defaultValue as any) : value !== defaultValue ? true : undefined;
        },
        [difference],
    );

    if (
        (ignoreEmptyDefaultValue && typeof defaultValue === "string" && defaultValue === "") ||
        ["undefined", "null", "[]", "{}"].indexOf(JSON.stringify(defaultValue)) > -1 ||
        (typeof defaultValue === "object" && Object.values(defaultValue).filter((v) => v !== "").length === 0)
    ) {
        return null;
    }

    return (
        typeof defaultValue !== "undefined" &&
        template && (
            <Form.Item<FormProps>
                noStyle
                shouldUpdate={(prevValues, nextValues) => {
                    return (
                        prevValues[valueName] !== nextValues[valueName] || widthRef.current !== widthOfRef?.clientWidth
                    );
                }}
            >
                {({ getFieldValue, setFieldsValue }) => {
                    const value = getFieldValue(valueName as string) as ValueType;
                    const diff = calculateDifference(value, defaultValue as any);

                    if (!diff) {
                        return null;
                    }

                    widthRef.current = widthOfRef?.clientWidth || widthRef.current;

                    const handleApply = () => {
                        if (typeof apply === "function") {
                            apply(diff as any, setFieldsValue as any, value);
                        } else {
                            (setFieldsValue as any)({
                                [valueName]: defaultValue,
                            });
                        }

                        document.dispatchEvent(new CustomEvent(FORM_VALUE_DIFFER_FROM_PRESET_TAG_APPLY_EVENT));

                        setIsModalVisible(false);
                    };

                    const content = (
                        <FormValueDifferFromTemplateContent
                            {...props}
                            currentValue={value}
                            difference={calculateDifference}
                        />
                    );

                    const tagComponent = (
                        <Tag
                            color="blue"
                            style={{ cursor: "pointer", ...style }}
                            className={className}
                            onClick={() => useModal && setIsModalVisible(true)}
                        >
                            {__("Differing from template")}
                            {/* Pseudo element to provide a "Apply all" button */}
                            <span
                                className={FORM_VALUE_DIFFER_FROM_PRESET_NOTICE_PSEUDO_ELEMENT_CLASS_NAME}
                                style={{ display: "none" }}
                                onClick={(e) => {
                                    handleApply();

                                    // Do not trigger the `<Tag onClick` event
                                    e.stopPropagation();
                                }}
                            ></span>
                        </Tag>
                    );

                    return (
                        <>
                            {useModal ? (
                                <>
                                    <Modal
                                        open={isModalVisible}
                                        onOk={handleApply}
                                        onCancel={() => setIsModalVisible(false)}
                                        okText={__("Use default value")}
                                        cancelText={__("Cancel")}
                                        width="calc(100% - 50px)"
                                    >
                                        {content}
                                    </Modal>
                                    {tagComponent}
                                </>
                            ) : (
                                <Popover
                                    destroyTooltipOnHide
                                    overlayStyle={{
                                        width: widthRef.current,
                                    }}
                                    content={
                                        <div className="wp-clearfix">
                                            {content}
                                            <a
                                                className="button button-primary alignright"
                                                onClick={handleApply}
                                                style={{ marginTop: 10 }}
                                            >
                                                {__("Use default value")}
                                            </a>
                                        </div>
                                    }
                                    title={undefined}
                                    placement={placement}
                                    {...popoverProps}
                                >
                                    {tagComponent}
                                </Popover>
                            )}
                            {!noBr && <br />}
                        </>
                    );
                }}
            </Form.Item>
        )
    );
}

export { diffByJsonStringify, FormValueDifferFromTemplateTag, FORM_VALUE_DIFFER_FROM_PRESET_TAG_APPLY_EVENT };
