import { DeleteOutlined, PlusOutlined, QuestionCircleFilled } from "@ant-design/icons";
import { Form, Pagination, Tooltip } from "antd";
import { useEffect, useState } from "react";

import type { ServiceTechnicalDefinition } from "@devowl-wp/cookie-consent-web-client";
import type { VisualServiceTechnicalDefinition } from "@devowl-wp/react-cookie-banner";
import { useRunCallbackNextRender } from "@devowl-wp/react-utils";

import { FormServiceFieldTechnicalDefinitionsCookieDuration } from "./columns/duration.js";
import { FormServiceFieldTechnicalDefinitionsCookieHost } from "./columns/host.js";
import { FormServiceFieldTechnicalDefinitionsCookieName } from "./columns/name.js";
import { FormServiceFieldTechnicalDefinitionsCookiePurpose } from "./columns/purpose.js";
import { FormServiceFieldTechnicalDefinitionsCookieType } from "./columns/type.js";
import { useI18n } from "../../../../../../contexts/i18n.js";
import { useDndSortable } from "../../../../../../hooks/useDndSortable.js";
import { FormValueDifferFromTemplateTag } from "../../../../valueDifferFromTemplateTag.js";
import { TECHNICAL_DEFINITION_DEFAULTS } from "../compose.js";

import type { FormServiceValueProps } from "../../../../../../types/formService.js";
import type { ComponentProps, FC } from "react";

const FormServiceFieldTechnicalDefinitionsLayout = {
    labelCol: { span: 0 },
    wrapperCol: { span: 24 },
    style: { margin: 0 },
};

type FormListFieldData = Parameters<ComponentProps<typeof Form.List>["children"]>[0][0];

const DiffTable: FC<{ technicalDefinitions: VisualServiceTechnicalDefinition[] }> = ({ technicalDefinitions }) => {
    const [form] = Form.useForm();

    useEffect(() => {
        form.setFieldsValue({
            technicalDefinitions,
        });
    }, [technicalDefinitions]);

    return (
        <Form
            form={form}
            initialValues={{
                isEmbeddingOnlyExternalResources: false,
                technicalDefinitions,
            }}
        >
            <FormServiceFieldTechnicalDefinitionsTable isDiff />
        </Form>
    );
};

const diffCalculate = (oldValue: VisualServiceTechnicalDefinition[], newValue: VisualServiceTechnicalDefinition[]) => {
    // Compose entries by all available keys for "modified" calculation
    const numberedIndexedForPosition: string[] = [];
    const oldEntriesModified = oldValue.reduce<Record<string, string>>(
        (a, { name, host, duration, durationUnit, isSessionDuration, type, purpose }) => {
            const idx = `${type}/${name}/${host}`;
            a[idx] = `${duration}/${durationUnit}/${isSessionDuration}/${type}/${purpose || ""}`;
            numberedIndexedForPosition.push(idx);
            return a;
        },
        {},
    );
    const modified = newValue
        .map((definition) => {
            const { name, host, duration, durationUnit, isSessionDuration, type, purpose } = definition;
            const idxName = `${type}/${name}/${host}`;
            const oldModified = oldEntriesModified[idxName];
            return typeof oldModified === "undefined" ||
                oldModified === `${duration}/${durationUnit}/${isSessionDuration}/${type}/${purpose || ""}`
                ? undefined
                : { ...definition, position: numberedIndexedForPosition.indexOf(idxName) };
        })
        .filter(Boolean);

    // Compose entries by name and host for "addition" calculation
    const oldEntriesComposedAdded = oldValue.map(({ type, name, host }) => `${type}/${name}/${host}`);
    const added = newValue
        .map((definition, position) => {
            const { type, name, host } = definition;
            return !oldEntriesComposedAdded.includes(`${type}/${name}/${host}`)
                ? { ...definition, position }
                : undefined;
        })
        .filter(Boolean);

    return added.length || modified.length ? { added, modified } : undefined;
};

const FormServiceFieldTechnicalDefinitionsTable: FC<{ isDiff?: boolean }> = ({ isDiff = false }) => {
    const { __, _i } = useI18n();

    const { SortableContext, SortableRow, DragHandle } = useDndSortable();
    const runCallbackNextRender = useRunCallbackNextRender();
    const [currentPagination, setCurrentPagination] = useState<
        Pick<ComponentProps<typeof Pagination>, "current" | "pageSize">
    >({
        current: 1,
        pageSize: 10,
    });

    return (
        <Form.Item<FormServiceValueProps>
            noStyle
            shouldUpdate={(prevValues, nextValues) =>
                prevValues.isEmbeddingOnlyExternalResources !== nextValues.isEmbeddingOnlyExternalResources ||
                (isDiff
                    ? JSON.stringify(prevValues.technicalDefinitions) !==
                      JSON.stringify(nextValues.technicalDefinitions)
                    : prevValues.technicalDefinitions.length !== nextValues.technicalDefinitions.length)
            }
        >
            {({ getFieldValue, validateFields, getFieldsValue }) =>
                getFieldValue("isEmbeddingOnlyExternalResources") ? null : (
                    <Form.List name="technicalDefinitions">
                        {(fields, { add, remove, move }) => {
                            const { current, pageSize } = currentPagination;
                            const minPageItem = (current - 1) * pageSize; // => (1 - 1) * 10 = 0
                            const maxPageItem = current * pageSize; // 1 * 10 = 10
                            const lastPage = Math.ceil(fields.length / pageSize);

                            return (
                                <table
                                    className="wp-list-table widefat fixed striped table-view-list"
                                    style={{ marginBottom: 25 }}
                                >
                                    <thead>
                                        <tr>
                                            <td width={45} align="right">
                                                &nbsp;
                                            </td>
                                            <td width={150}>{__("Cookie type")}</td>
                                            <td>
                                                <Tooltip
                                                    title={_i(
                                                        __(
                                                            "Every cookie has a technical name, which you must provide. If a cookie name is dynamically composed, please use an asterisk ({{code}}*{{/code}}) as a wildcard (placeholder).",
                                                        ),
                                                        {
                                                            code: <code />,
                                                        },
                                                    )}
                                                >
                                                    <span>
                                                        {__("Technical cookie name")} <QuestionCircleFilled />
                                                    </span>
                                                </Tooltip>
                                            </td>
                                            <td>
                                                <Tooltip
                                                    title={__("Every cookie is associated to a domain or hostname.")}
                                                >
                                                    <span>
                                                        {__("Technical cookie host")} <QuestionCircleFilled />
                                                    </span>
                                                </Tooltip>
                                            </td>
                                            <td width={290}>
                                                <Tooltip
                                                    title={__(
                                                        "A HTTP cookie is only valid for a certain time, which is defined when the cookie is set.",
                                                    )}
                                                >
                                                    <span>
                                                        {__("Cookie duration")} <QuestionCircleFilled />
                                                    </span>
                                                </Tooltip>
                                            </td>
                                            <td>
                                                <Tooltip
                                                    title={__(
                                                        "Each cookie serves a purpose (e.g. user identification for tracking), which should be explained.",
                                                    )}
                                                >
                                                    <span>
                                                        {__("Purpose")} <QuestionCircleFilled />
                                                    </span>
                                                </Tooltip>
                                            </td>
                                            <td width={70} align="right">
                                                &nbsp;
                                            </td>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <SortableContext
                                            items={fields.map(({ key }) => `${key}`)}
                                            onDragEnd={({ active, over }) => {
                                                const from = fields.findIndex((field) => field.key === +active.id);
                                                const to = fields.findIndex((field) => field.key === +over?.id);
                                                move(from, to);
                                            }}
                                        >
                                            {fields.map((field, index) => {
                                                const currentPageItem = index + 1; // => 1
                                                const isInPage =
                                                    currentPageItem > minPageItem && currentPageItem <= maxPageItem;

                                                return (
                                                    <SortableRow
                                                        key={field.key}
                                                        data-row-key={`${field.key}`}
                                                        style={{ display: isInPage ? undefined : "none" }}
                                                    >
                                                        <td>
                                                            {fields.length > 1 && !isDiff ? <DragHandle /> : undefined}
                                                        </td>
                                                        <td>
                                                            <FormServiceFieldTechnicalDefinitionsCookieType
                                                                field={field}
                                                                readOnly={isDiff}
                                                            />
                                                        </td>
                                                        <td>
                                                            <FormServiceFieldTechnicalDefinitionsCookieName
                                                                field={field}
                                                                readOnly={isDiff}
                                                            />
                                                        </td>
                                                        <td>
                                                            <FormServiceFieldTechnicalDefinitionsCookieHost
                                                                field={field}
                                                                readOnly={isDiff}
                                                            />
                                                        </td>
                                                        <td>
                                                            <FormServiceFieldTechnicalDefinitionsCookieDuration
                                                                field={field}
                                                                readOnly={isDiff}
                                                            />
                                                        </td>
                                                        <td>
                                                            <FormServiceFieldTechnicalDefinitionsCookiePurpose
                                                                field={field}
                                                                readOnly={isDiff}
                                                            />
                                                        </td>
                                                        <td>
                                                            {fields.length > 1 && !isDiff ? (
                                                                <a
                                                                    className="button button-small"
                                                                    onClick={() => {
                                                                        remove(field.name);

                                                                        // Automatically select the previous page when necessery
                                                                        if (
                                                                            current > 1 &&
                                                                            current === lastPage &&
                                                                            fields.length % pageSize === 1
                                                                        ) {
                                                                            setCurrentPagination({
                                                                                current: current - 1,
                                                                                pageSize,
                                                                            });
                                                                        }
                                                                    }}
                                                                >
                                                                    <DeleteOutlined />
                                                                </a>
                                                            ) : null}
                                                        </td>
                                                    </SortableRow>
                                                );
                                            })}
                                        </SortableContext>
                                    </tbody>
                                    {!isDiff && (
                                        <tfoot>
                                            <tr>
                                                <td colSpan={7} align="right">
                                                    <a
                                                        className="button button-primary alignright"
                                                        onClick={() => {
                                                            add(TECHNICAL_DEFINITION_DEFAULTS);

                                                            // Automatically select the new page when necessery
                                                            const newCurrent =
                                                                lastPage +
                                                                (fields.length > 0 && fields.length % pageSize === 0
                                                                    ? 1
                                                                    : 0);

                                                            if (current !== newCurrent) {
                                                                setCurrentPagination({
                                                                    current: newCurrent,
                                                                    pageSize,
                                                                });
                                                            }
                                                        }}
                                                    >
                                                        <PlusOutlined /> {__("Add another cookie definition")}
                                                    </a>
                                                    <Pagination
                                                        onChange={(page, pageSize) => {
                                                            const current =
                                                                currentPagination.pageSize !== pageSize ? 1 : page;
                                                            setCurrentPagination({
                                                                current,
                                                                pageSize,
                                                            });

                                                            const minPageItem = (current - 1) * pageSize; // => (1 - 1) * 10 = 0
                                                            const maxPageItem = current * pageSize; // 1 * 10 = 10

                                                            // Antd cannot render the validation errors in a hidden field, so we need to wait for
                                                            // the next page is rendered and validate again those errors
                                                            const fieldsToValidate = (
                                                                getFieldsValue(["technicalDefinitions"])
                                                                    .technicalDefinitions as ServiceTechnicalDefinition[]
                                                            )
                                                                .map((td, index) => {
                                                                    const currentPageItem = index + 1; // => 1
                                                                    const isInNextPage =
                                                                        currentPageItem > minPageItem &&
                                                                        currentPageItem <= maxPageItem;

                                                                    return (
                                                                        isInNextPage &&
                                                                        Object.keys(td).map((key) => [
                                                                            "technicalDefinitions",
                                                                            index,
                                                                            key,
                                                                        ])
                                                                    );
                                                                })
                                                                .filter(Boolean)
                                                                .flat();

                                                            runCallbackNextRender(() =>
                                                                validateFields(fieldsToValidate),
                                                            );
                                                        }}
                                                        {...currentPagination}
                                                        showLessItems
                                                        showTotal={(total) => __("Total %d items", total)}
                                                        size="small"
                                                        total={fields.length}
                                                        className="alignright"
                                                        pageSizeOptions={[10, 20, 50, 100, 1000]}
                                                        style={{ marginRight: 10 }}
                                                        showSizeChanger
                                                    />
                                                    <FormValueDifferFromTemplateTag
                                                        useModal
                                                        form="service"
                                                        valueName="technicalDefinitions"
                                                        noBr
                                                        style={{ marginTop: 4 }}
                                                        difference={diffCalculate}
                                                        apply={({ added, modified }, setFieldsValue, oldValue) => {
                                                            let technicalDefinitions = oldValue as Array<
                                                                VisualServiceTechnicalDefinition & {
                                                                    _duplicate?: boolean;
                                                                }
                                                            >;
                                                            modified.forEach(({ position, ...rest }) => {
                                                                technicalDefinitions[position] = rest;
                                                            });
                                                            added.forEach(({ position, ...rest }) => {
                                                                technicalDefinitions.splice(position, 0, rest);
                                                            });

                                                            // Remove empty entries (caused by switching from `isEmbeddingOnlyExternalResources=true` to `false`)
                                                            technicalDefinitions = technicalDefinitions.filter(
                                                                ({ host, name, duration }) =>
                                                                    host !== "" &&
                                                                    name !== "" &&
                                                                    duration !== undefined,
                                                            );

                                                            // Remove duplicates (from bottom to top)
                                                            technicalDefinitions.forEach((a) => {
                                                                const {
                                                                    type: typeA,
                                                                    host: hostA,
                                                                    name: nameA,
                                                                    _duplicate,
                                                                } = a;

                                                                if (!_duplicate) {
                                                                    const duplicate = technicalDefinitions.find((b) => {
                                                                        const {
                                                                            type: typeB,
                                                                            host: hostB,
                                                                            name: nameB,
                                                                        } = b;
                                                                        return (
                                                                            a !== b &&
                                                                            typeA === typeB &&
                                                                            hostA === hostB &&
                                                                            nameA === nameB
                                                                        );
                                                                    });

                                                                    if (duplicate) {
                                                                        duplicate._duplicate = true;
                                                                    }
                                                                }
                                                            });
                                                            technicalDefinitions = technicalDefinitions.filter(
                                                                ({ _duplicate }) => !_duplicate,
                                                            );

                                                            setFieldsValue({
                                                                technicalDefinitions,
                                                            });
                                                        }}
                                                        newValueText={false}
                                                        renderDiff={(value, { added, modified }) => (
                                                            <>
                                                                {added.length > 0 && (
                                                                    <>
                                                                        <p>
                                                                            <strong>{__("Missing entries:")}</strong>
                                                                        </p>
                                                                        <DiffTable technicalDefinitions={added} />
                                                                    </>
                                                                )}
                                                                {modified.length > 0 && (
                                                                    <>
                                                                        <p>
                                                                            <strong>{__("Modified entries:")}</strong>
                                                                        </p>
                                                                        <DiffTable technicalDefinitions={modified} />
                                                                    </>
                                                                )}
                                                            </>
                                                        )}
                                                    />
                                                </td>
                                            </tr>
                                        </tfoot>
                                    )}
                                </table>
                            );
                        }}
                    </Form.List>
                )
            }
        </Form.Item>
    );
};

export {
    FormServiceFieldTechnicalDefinitionsLayout,
    type FormListFieldData,
    FormServiceFieldTechnicalDefinitionsTable,
};
