import type {
    BoolIfFunction,
    CreatePureScopedStylesheetCallbackArgs,
    PureScopedStyleSheet,
    VarConsumer,
} from "@devowl-wp/web-scoped-css";
import {
    createStylesheet,
    fastdom,
    hexToRgb,
    mapAll,
    mapHex,
    mapIgnore,
    mapStringToBoolean,
    mapStringToIsEmpty,
    mapValueSuffix,
    mapValueSuffixPx,
    mapValueSuffixPxArray,
    matchesMedia,
} from "@devowl-wp/web-scoped-css";

import { SCALING_WHEN_WINDOW_WIDTH_LOWER } from "../types/customize.js";

import type { Customize, MarginOrPadding } from "../types/customize.js";

type MappedMarginOrPaddingToScalingFactorKeys = `${"l" | "m"}${0 | 1 | 2 | 3}` | "l" | "m";
type MappedMarginOrPaddingToScalingFactor = {
    [K in MappedMarginOrPaddingToScalingFactorKeys]: K extends "l" | "m" ? string[] : string;
};

type MainStyleSheetReturn = ReturnType<typeof createMainStylesheet>;

type MainStylesheetAdditionalCustomizeProperties = {
    /**
     * Indicates that TCF is enabled and active, if set to `true` you need to also
     * populate the `tcf` object.
     *
     * Attention: The visual content blocker does currently not support TCF!
     */
    isTcf: boolean;
    activeAction?: "history" | "revoke" | "change";
    /**
     * Unique request id. This is used for the unique container `div` (anti-ad-block system) and
     * should be regenerated for each page request (or x days, when using page cache).
     */
    pageRequestUuid4: string;
};

function createMainStylesheet(
    customize: Partial<Customize & MainStylesheetAdditionalCustomizeProperties>,
    { rule, computed, variable, vars, meta, className, plugin }: CreatePureScopedStylesheetCallbackArgs,
) {
    // Compatibility with `@devowl-wp/web-html-element-interaction-recorder`
    plugin("modifyRule", (properties) => {
        const { pseudos, forceSelector } = properties;
        const focusVisible = ":focus-visible";
        const focusVisibleClassName = ".wheir-focus-visible";

        if (typeof forceSelector === "string" && forceSelector?.indexOf(focusVisible) > -1) {
            properties.forceSelector += `,${forceSelector.replace(
                new RegExp(focusVisible, "g"),
                focusVisibleClassName,
            )}`;
        }

        for (const pseudo in pseudos) {
            if (pseudo.indexOf(focusVisible) > -1) {
                pseudos[pseudo.replace(new RegExp(focusVisible, "g"), focusVisibleClassName)] = pseudos[pseudo];
            }
        }
    });

    const { pageRequestUuid4 } = customize;

    // Misc variables
    const isTcf = variable(customize.isTcf);

    const unsetDialogStyles = className();
    rule({
        // Reset `<dialog` styles
        background: "none",
        padding: "0px",
        margin: "0px",
        border: "none",
        maxWidth: "initial",
        maxHeight: "initial",
        position: "fixed",
        outline: "none !important",
        // all: "revert",
        pseudos: {
            "::backdrop": {
                all: "unset",
            },
        },
        // Do not allow to modify props via filterClassName
        forceSelector: `.${unsetDialogStyles}`,
    });

    // Make elements only available to screen readers and hide visually
    // See https://www.a11ymatters.com/pattern/checkbox/
    const screenReaderOnlyClass = className();
    rule({
        position: "absolute",
        clip: "rect(1px, 1px, 1px, 1px)",
        padding: "0px",
        border: "0px",
        height: "1px",
        width: "1px",
        overflow: "hidden",
        display: "block",
        // Do not allow to modify props via filterClassName
        forceSelector: `.${screenReaderOnlyClass}`,
    });
    const a11yFocusStyle = {
        outline: {
            outline: "black auto 1px !important",
            outlineOffset: "3px !important",
        },
        text: {
            textDecoration: "underline dashed !important",
            textUnderlineOffset: "3px !important",
        },
    };

    // Common styling for focused elements (links)
    rule({
        ...a11yFocusStyle.outline,
        forceSelector: `.${meta.id} :focus-visible, .${meta.id}:focus-visible, .${meta.id} *:has(+ .${screenReaderOnlyClass}:focus-visible)`,
    });

    // Create mobile experience view with hacky container queries (use `dispatchResize` on window resize event)
    const isMobileClass = className();
    const hideOnMobileClass = className();
    const [mobile, updateMobile] = vars(customize.mobile, {
        maxHeight: mapValueSuffixPx,
        alignment: mapStringToBoolean(customize.mobile.alignment, ["bottom", "center", "top"]),
    });
    const [matchesMobile, onMatchesMobileChange] = matchesMedia(`(max-width: ${SCALING_WHEN_WINDOW_WIDTH_LOWER}px)`);
    const {
        isMobile,
        isMobileWidth,
        update: computedMobileUpdate,
    } = computed(
        [mobile.enabled],
        ([enabled]) => {
            const {
                body: { classList },
            } = document;
            const overlayElement = document.getElementById(pageRequestUuid4);
            const { innerWidth } = window;
            const width = overlayElement?.clientWidth || innerWidth;
            const isMobileWidth = width <= SCALING_WHEN_WINDOW_WIDTH_LOWER || matchesMobile.matches;
            const isMobile = enabled && isMobileWidth;
            if (isMobile) {
                classList.add(isMobileClass);
            } else {
                classList.remove(isMobileClass);
            }
            return { isMobile, isMobileWidth };
        },
        "mobile",
        "raf",
    );
    onMatchesMobileChange(computedMobileUpdate);
    rule({
        forceSelector: `.${isMobileClass} .${hideOnMobileClass}`,
        display: "none",
    });
    const [{ x: scaleHorizontal, y: scaleVertical }] = vars({
        x: `calc(${mobile.scalePercent()} / 100)`,
        y: `calc((${mobile.scalePercent()} + ${mobile.scalePercentVertical()}) / 100)`,
    });
    const mapMarginOrPaddingToScalingFactorFn =
        (vertical: number[]) =>
        (padding: MarginOrPadding): MappedMarginOrPaddingToScalingFactor => {
            const pxs = mapValueSuffixPxArray(padding);
            const createCalc = (px: string, i: number) =>
                `calc(${px}*${vertical.indexOf(i) > -1 ? scaleVertical() : scaleHorizontal()})`;
            const res = {
                l: pxs,
                m: pxs.map(createCalc),
            };
            for (let i = 0; i < pxs.length; i++) {
                const px = pxs[i];
                res[`l${i}`] = px;
                res[`m${i}`] = createCalc(px, i);
            }
            return res as any;
        };
    const mapMarginOrPaddingToScalingFactor = mapMarginOrPaddingToScalingFactorFn([0, 2]);
    const mapMarginOrPaddingToScalingFactorAll = mapMarginOrPaddingToScalingFactorFn([]);
    const mapNumberToScalingFactor = (n: number) => {
        const px = mapValueSuffixPx(n);
        return {
            l: px,
            m: `calc(${px}*${scaleHorizontal()})`,
        };
    };
    const boolLargeOrMobile = (
        consumer: VarConsumer<any, MappedMarginOrPaddingToScalingFactorKeys> | VarConsumer<any, "l" | "m">,
        boolIf: BoolIfFunction,
        idx?: number,
    ) =>
        boolIf(
            isMobile,
            consumer(typeof idx === "number" ? (`m${idx}` as any) : "m"),
            consumer(typeof idx === "number" ? (`l${idx}` as any) : "l"),
        );

    // Create variables of customize settings
    const [decision, updateDecision] = vars(customize.decision, {
        acceptAll: mapStringToBoolean(customize.decision.acceptAll, ["button", "link", "hide"]),
        acceptEssentials: mapStringToBoolean(customize.decision.acceptAll, ["button", "link", "hide"]),
        acceptIndividual: mapStringToBoolean(customize.decision.acceptAll, ["button", "link", "hide"]),
    });
    const [layout, updateLayout] = vars(customize.layout, {
        maxHeight: mapValueSuffixPx,
        dialogPosition: mapStringToBoolean(customize.layout.dialogPosition, ["middleCenter"]),
        bannerPosition: mapStringToBoolean(customize.layout.bannerPosition, ["top", "bottom"]),
        borderRadius: mapValueSuffixPx,
        dialogMargin: mapValueSuffixPxArray,
        dialogBorderRadius: mapValueSuffixPx,
        dialogMaxWidth: mapValueSuffixPx,
        overlayBg: hexToRgb,
        overlayBlur: mapValueSuffixPx,
        bannerMaxWidth: mapValueSuffixPx,
    });
    const [sticky, updateSticky] = vars(customize.sticky, {
        alignment: mapStringToBoolean(customize.sticky.alignment, ["left", "center", "right"]),
        bubbleBorderRadius: mapValueSuffix("%"),
        bubblePadding: mapNumberToScalingFactor,
        bubbleMargin: mapMarginOrPaddingToScalingFactorAll,
        bubbleBorderWidth: mapValueSuffixPx,
        iconSize: mapNumberToScalingFactor,
        boxShadowBlurRadius: mapValueSuffixPx,
        boxShadowOffsetX: mapValueSuffixPx,
        boxShadowOffsetY: mapValueSuffixPx,
        boxShadowSpreadRadius: mapValueSuffixPx,
        boxShadowColor: mapHex,
        boxShadowColorAlpha: mapValueSuffix("%"),
        menuBorderRadius: mapValueSuffixPx,
        menuFontSize: mapNumberToScalingFactor,
        menuItemSpacing: mapNumberToScalingFactor,
        menuItemPadding: mapMarginOrPaddingToScalingFactorAll,
    });
    const [bodyDesign, updateBodyDesign] = vars(
        {
            // This variables can be unset in free version, so we create defaults
            accordionMargin: [0, 0, 0, 0],
            accordionPadding: [0, 0, 0, 0],
            accordionArrowType: "none",
            accordionArrowColor: "white",
            accordionBg: "white",
            accordionActiveBg: "white",
            accordionHoverBg: "white",
            accordionBorderWidth: 0,
            accordionBorderColor: "white",
            accordionTitleFontSize: 0,
            accordionTitleFontColor: "white",
            accordionTitleFontWeight: "white",
            accordionDescriptionMargin: [0, 0, 0, 0],
            accordionDescriptionFontSize: 0,
            accordionDescriptionFontColor: "white",
            accordionDescriptionFontWeight: "white",
            ...customize.bodyDesign,
        },
        {
            padding: mapMarginOrPaddingToScalingFactor,
            descriptionFontSize: mapNumberToScalingFactor,
            teachingsFontSize: mapNumberToScalingFactor,
            teachingsSeparatorWidth: mapValueSuffixPx,
            teachingsSeparatorHeight: mapValueSuffixPx,
            dottedGroupsFontSize: mapNumberToScalingFactor,
            acceptAllFontSize: mapNumberToScalingFactor,
            acceptAllPadding: mapMarginOrPaddingToScalingFactor,
            acceptAllBorderWidth: mapNumberToScalingFactor,
            acceptAllTextAlign: mapStringToBoolean(customize.bodyDesign.acceptAllTextAlign, ["center", "right"]),
            acceptEssentialsFontSize: mapNumberToScalingFactor,
            acceptEssentialsPadding: mapMarginOrPaddingToScalingFactor,
            acceptEssentialsBorderWidth: mapNumberToScalingFactor,
            acceptEssentialsTextAlign: mapStringToBoolean(customize.bodyDesign.acceptEssentialsTextAlign, [
                "center",
                "right",
            ]),
            acceptIndividualFontSize: mapNumberToScalingFactor,
            acceptIndividualPadding: mapMarginOrPaddingToScalingFactor,
            acceptIndividualBorderWidth: mapNumberToScalingFactor,
            acceptIndividualTextAlign: mapStringToBoolean(customize.bodyDesign.acceptIndividualTextAlign, [
                "center",
                "right",
            ]),
            accordionMargin: mapMarginOrPaddingToScalingFactor,
            accordionTitleFontSize: mapNumberToScalingFactor,
            accordionBorderWidth: mapNumberToScalingFactor,
            accordionPadding: mapMarginOrPaddingToScalingFactor,
            accordionDescriptionFontSize: mapNumberToScalingFactor,
            accordionDescriptionMargin: mapMarginOrPaddingToScalingFactor,
        },
    );
    const [saveButton, updateSaveButton] = vars(customize.saveButton, {
        type: mapStringToBoolean(customize.saveButton.type, ["button", "link"]),
        fontSize: mapNumberToScalingFactor,
        padding: mapMarginOrPaddingToScalingFactor,
        borderWidth: mapNumberToScalingFactor,
        textAlign: mapStringToBoolean(customize.saveButton.textAlign, ["center", "right"]),
    });
    const [design, updateDesign] = vars(customize.design, {
        fontColor: mapHex,
        fontSize: mapNumberToScalingFactor,
        borderWidth: mapNumberToScalingFactor,
        textAlign: mapStringToBoolean(customize.design.textAlign, ["center", "right"]),
        boxShadowBlurRadius: mapValueSuffixPx,
        boxShadowOffsetX: mapValueSuffixPx,
        boxShadowOffsetY: mapValueSuffixPx,
        boxShadowSpreadRadius: mapValueSuffixPx,
        boxShadowColor: mapHex,
        boxShadowColorAlpha: mapValueSuffix("%"),
    });
    const [headerDesign, updateHeaderDesign] = vars(customize.headerDesign, {
        fontSize: mapNumberToScalingFactor,
        borderWidth: mapNumberToScalingFactor,
        padding: mapMarginOrPaddingToScalingFactor,
        textAlign: mapStringToBoolean(customize.headerDesign.textAlign, ["center", "right"]),
        logoPosition: mapStringToBoolean(customize.headerDesign.logoPosition, ["left", "right"]),
        logo: mapStringToIsEmpty(false),
        logoMargin: mapMarginOrPaddingToScalingFactor,
    });
    const [footerDesign, updateFooterDesign] = vars(customize.footerDesign, {
        borderWidth: mapNumberToScalingFactor,
        padding: mapMarginOrPaddingToScalingFactor,
        fontSize: mapNumberToScalingFactor,
    });
    const [texts, updateTexts] = vars(customize.texts, {
        ...mapAll(customize.texts, mapIgnore),
        headline: mapStringToIsEmpty(),
    });
    const [individualLayout, updateIndividualLayout] = vars(customize.individualLayout, {
        dialogMaxWidth: mapValueSuffixPx,
        bannerMaxWidth: mapValueSuffixPx,
    });
    const [group, updateGroup] = vars(customize.group, {
        headlineFontSize: mapNumberToScalingFactor,
        descriptionFontSize: mapNumberToScalingFactor,
        groupPadding: mapMarginOrPaddingToScalingFactor,
        groupBorderRadius: mapValueSuffixPx,
        groupBorderWidth: mapNumberToScalingFactor,
        groupSpacing: mapNumberToScalingFactor,
        checkboxBorderWidth: mapNumberToScalingFactor,
    });

    // Add class to server-side rendered overlay
    const overlayElement = document.getElementById(pageRequestUuid4);
    if (overlayElement) {
        fastdom.mutate(() => {
            overlayElement.className = meta.id;
        });
    }

    return {
        customize,
        unsetDialogStyles,
        a11yFocusStyle,
        scaleHorizontal,
        scaleVertical,
        isTcf,
        computedMobileUpdate,
        boolLargeOrMobile,
        isMobile,
        isMobileWidth,
        isMobileClass,
        hideOnMobileClass,
        screenReaderOnlyClass,
        updater: {
            decision: updateDecision,
            layout: updateLayout,
            design: updateDesign,
            bodyDesign: updateBodyDesign,
            headerDesign: updateHeaderDesign,
            footerDesign: updateFooterDesign,
            texts: updateTexts,
            mobile: updateMobile,
            sticky: updateSticky,
            individualLayout: updateIndividualLayout,
            group: updateGroup,
            saveButton: updateSaveButton,
        },
        decision,
        layout,
        design,
        bodyDesign,
        headerDesign,
        footerDesign,
        individualLayout,
        group,
        saveButton,
        texts,
        mobile,
        sticky,
    };
}

let updateTimeout: ReturnType<typeof setTimeout>;
function updateCustomCssStyle(mainStylesheet: HTMLStyleElement, css = "", id: string) {
    clearTimeout(updateTimeout);
    updateTimeout = setTimeout(() => {
        const pseudoCss = css.replace(/\.rcb-([A-Za-z0-9_-]+)/g, (m, rcbClassName) => {
            return `:is(.${mainStylesheet[`rcb-${rcbClassName}`]?.join(",.") || m.substring(1)})`;
        });
        createStylesheet(pseudoCss, `custom-css-${id}`, true);
    }, 0);
}

const filterClassNameForCustomCss = (
    id: string,
    forceRcbClassName: boolean,
    customCss: string,
    ...[classNames, scopedClassName, { mainElement, id: mainId }]: Parameters<
        PureScopedStyleSheet["settings"]["filterClassName"]
    >
) => {
    // The first passed classname should always be the anti-ad-block class
    if (classNames.length) {
        const rcbClassName = `rcb-${classNames[0]}`;
        if (forceRcbClassName) {
            classNames[0] = rcbClassName;
        } else {
            classNames.splice(0, 1);
        }
        mainElement[rcbClassName] = mainElement[rcbClassName] || [];
        mainElement[rcbClassName].push(scopedClassName);
        updateCustomCssStyle(mainElement, customCss, id);
    }
};

export {
    createMainStylesheet,
    type MainStyleSheetReturn,
    type MainStylesheetAdditionalCustomizeProperties,
    filterClassNameForCustomCss,
    updateCustomCssStyle,
};
