import { Fragment, useCallback, useEffect, useMemo, useRef } from "react";

import type { ApplyInteractiveEvent } from "@devowl-wp/cookie-consent-web-client";
import { APPLY_INTERACTIVE_EVENT } from "@devowl-wp/cookie-consent-web-client";
import { useSuspendedComponentsMounted, yieldLazyLoad, yieldMainThread } from "@devowl-wp/react-utils";
import { extendBannerStylesheet, fastdom } from "@devowl-wp/web-cookie-banner";

import { BannerContent } from "./body/content.js";
import { useBanner } from "../../contexts/banner.js";
import { useStylesheet, useStylesheetProvider } from "../../contexts/stylesheet.js";
import { resetConsentChangeHash, useBannerActionLinks } from "../../hooks/banner/useBannerActionLinks.js";
import { useBannerAnimation } from "../../hooks/banner/useBannerAnimation.js";
import { useMainStylesheetForContext } from "../../hooks/useMainStylesheetForContext.js";
import { useResettableAnimation } from "../../hooks/useResettableAnimation.js";
import { AnimatedCss } from "../animateCss.js";

import type { FC, ReactElement } from "react";

const YieldBannerContent = yieldLazyLoad(Promise.resolve(BannerContent), "BannerContent");

const BannerSticky = yieldLazyLoad(
    import(/* webpackChunkName: "banner-lazy", webpackMode: "lazy-once" */ "./sticky/sticky.js").then(
        ({ BannerSticky }) => BannerSticky,
    ),
);

const mutateScrolling = (display: boolean, overlayActive: boolean) => {
    const { dataset, style: bodyStyle } = document.body;

    // Save first state
    if (dataset.rcbPreviousOverflow === undefined) {
        dataset.rcbPreviousOverflow = bodyStyle.overflow;
    }

    // Restrict scrolling
    bodyStyle.overflow = display && overlayActive ? "hidden" : dataset.rcbPreviousOverflow;
    document.body.parentElement.style.overflow = bodyStyle.overflow;
};

const Banner: FC = () => {
    const banner = useBanner();
    const {
        recorder,
        visible,
        activeAction,
        isConsentGiven,
        skipOverlay,
        pageRequestUuid4,
        individualPrivacyOpen,
        fetchLazyLoadedDataForSecondView,
        onClose,
        layout: { overlay, animationInDuration, animationOutDuration },
        sticky,
        keepVariablesInTexts,
    } = banner;
    const dialogRef = useRef<HTMLDialogElement>();
    const overlayRef = useRef<HTMLDivElement>();

    /**
     * Do not unmount the whole DOM when the cookie banner gets closed, instead we just hide the overlay.
     */
    const renderedOnce = useRef(false);

    const focusDialog = useCallback(() => {
        fastdom.mutate(() => {
            dialogRef.current.focus({
                preventScroll: true,
            });
        });
    }, []);

    const [useAnimationIn, useAnimationOut] = useBannerAnimation(banner.layout);
    const [stateAnimationIn, stateAnimationInDuration] = useResettableAnimation(useAnimationIn, animationInDuration);
    const [stateAnimationOut, stateAnimationOutDuration] = useResettableAnimation(
        useAnimationOut === "none" ? "fadeOut" : useAnimationOut,
        useAnimationOut === "none" ? 0 : animationOutDuration,
    );
    const [SuspendedProvider, suspendedContextValue] = useSuspendedComponentsMounted(
        ["BannerContent", "BannerHeader", "BannerBody", "BannerFooter", "BodyDescription"],
        (callback) => {
            yieldMainThread().then(callback);
        },
        () => {
            dialogRef.current.style.removeProperty("display");
            focusDialog();
        },
    );

    const stylesheet = useStylesheet();
    const {
        a11yIds: { firstButton },
        inner,
        Dialog,
        Overlay,
        individualPrivacyOpen: individualPrivacyOpenCssVariable,
        computedMobileUpdate,
    } = stylesheet.extend(...extendBannerStylesheet);
    useMemo(() => {
        individualPrivacyOpenCssVariable.update(individualPrivacyOpen);

        if (individualPrivacyOpen) {
            fetchLazyLoadedDataForSecondView?.();
        }
    }, [individualPrivacyOpen]);

    useEffect(() => {
        computedMobileUpdate();
    }, []);

    useEffect(
        () => () => {
            mutateScrolling(false, overlay);
        },
        [overlay],
    );

    useBannerActionLinks();

    // Automatically restart recorder when banner got visible again (e.g. cookie banner on first website
    // visit and then "Change privacy settings" link / dialog).
    useEffect(() => {
        if (visible && recorder) {
            fastdom.mutate(() => {
                recorder.restart();
            });
        }
    }, [visible, recorder]);

    // Show the banner with the dialog-API
    useEffect(() => {
        const dialogElement = dialogRef.current;
        const overlayElement = overlayRef.current || document.getElementById(pageRequestUuid4);

        // Listen to `Escape` and automatically focus the "Skip to consent choices" button
        const escapeListener = function (this: typeof dialogElement, e: Event) {
            if (activeAction) {
                // If we have an active action, we allow to close the banner by pressing the escape key
                onClose();
            } else {
                (this.querySelector(`a[href="#${firstButton}"]`) as HTMLAnchorElement).focus();
                e.preventDefault();
            }
        };

        if (visible) {
            renderedOnce.current = true;

            // Always show dialog in modal so screen reader users get the cookie banner as first element
            // when visiting the website when overlay is active. Also check for `isConnected` if the element is attached
            // to the current DOM. E.g. with `React.createPortal()` it is not attached at time of `useEffect`
            // to the DOM and we are getting an error "Uncaught DOMException: Failed to execute 'showModal'
            // on 'HTMLDialogElement': The element is not in a Document.". We do not need necesserily call the
            // `show` method on the dialog, as it is shown always with the `display: flex` property.
            //
            // Additionally, check if `.show()` method is available on the HTML dialog element as we do not have a
            // polyfill installed and the visibility is controlled through CSS. The fact, that in older browsers the
            // "modal" functionality is not given, is OK for us.
            if (dialogElement?.isConnected) {
                if (dialogElement.open) {
                    dialogElement.close?.();
                }
                fastdom.mutate(() => {
                    // When `keepVariablesInTexts` is true, we do not use the `showModal` method, as it does not
                    // work as expected for e.g. TranslatePress in edit screen.
                    dialogElement[overlay && !keepVariablesInTexts ? "showModal" : "show"]?.();
                });
                dialogElement.addEventListener("cancel", escapeListener);
            }
        } else if (dialogElement) {
            dialogElement.close?.();
        }

        // Toggle overlay visibility
        if (overlayElement) {
            const defaultIfNoAnimation = 0; // Avoid "hard" overlay
            const useDuration = visible
                ? useAnimationIn === "none"
                    ? defaultIfNoAnimation
                    : animationInDuration
                : useAnimationOut === "none"
                  ? defaultIfNoAnimation
                  : animationOutDuration;
            const hasAnimation = useDuration > 0;
            const mutateStyle = (display: boolean) => {
                hasAnimation && (overlayElement.style.transition = `background ${useDuration}ms`);
                overlayElement.style.display = display ? "block" : "none";

                mutateScrolling(display, overlay);

                if (display) {
                    focusDialog();
                }
            };

            if (visible) {
                fastdom.mutate(() => {
                    mutateStyle(true);
                });
            } else if (renderedOnce.current) {
                setTimeout(() => fastdom.mutate(() => mutateStyle(false)), useDuration);
                resetConsentChangeHash();
            }
        }

        return () => {
            dialogElement?.removeEventListener("keyup", escapeListener);
        };
    }, [visible, overlay, activeAction, onClose]);

    // Refocus when layer changes
    useEffect(() => {
        if (visible) {
            focusDialog();
        }
    }, [visible, individualPrivacyOpen]);

    // Automatically close the cookie banner if we got a decision in another tab
    useEffect(() => {
        const listener = (({ detail: { triggeredByOtherTab } }: CustomEvent<ApplyInteractiveEvent>) => {
            if (triggeredByOtherTab) {
                onClose();
            }
        }) as any;

        document.addEventListener(APPLY_INTERACTIVE_EVENT, listener);
        return () => {
            document.removeEventListener(APPLY_INTERACTIVE_EVENT, listener);
        };
    }, [onClose]);

    const content: ReactElement[] = [];

    if (isConsentGiven && sticky.enabled) {
        content.push(<BannerSticky key="sticky" />);
    }

    if (!visible && !renderedOnce.current) {
        // Silence is golden.
    } else {
        const dialog = (
            <Dialog
                key="dialog"
                className={`${"wp-exclude-emoji"} ${individualPrivacyOpen ? "second-layer" : ""}`}
                ref={dialogRef}
                style={{ display: "none" }}
                data-nosnippet
                data-lenis-prevent
            >
                <SuspendedProvider value={suspendedContextValue}>
                    <AnimatedCss
                        animationIn={stateAnimationIn}
                        animationInDuration={stateAnimationInDuration}
                        animationOut={stateAnimationOut}
                        animationOutDuration={stateAnimationOutDuration}
                        isVisible={visible}
                        className={inner}
                    >
                        <YieldBannerContent />
                    </AnimatedCss>
                </SuspendedProvider>
            </Dialog>
        );

        content.push(
            skipOverlay ? (
                dialog
            ) : (
                <Overlay key="overlay" id={pageRequestUuid4} className={stylesheet.className} ref={overlayRef}>
                    {dialog}
                </Overlay>
            ),
        );
    }

    return <Fragment>{content}</Fragment>;
};

/**
 * Use an yielded component for the main component as `BannerMainStylesheet` is only meant to create the
 * main stylesheet and this is very expensive in the main thread.
 */
const YieldBanner = yieldLazyLoad(Promise.resolve(Banner));

const BannerMainStylesheet: FC = () => {
    const { pageRequestUuid4 } = useBanner();
    const stylesheet = useMainStylesheetForContext();
    stylesheet.specify(pageRequestUuid4);
    const [StylesheetProvider, stylesheetContextValue] = useStylesheetProvider(stylesheet);

    return (
        <StylesheetProvider value={stylesheetContextValue}>
            <YieldBanner />
        </StylesheetProvider>
    );
};

export { BannerMainStylesheet as Banner };
