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

import { findBlockedNodes } from "./findBlockedNodes.js";
import { setCurrentlyInTransaction } from "./transaction.js";
import { delegateClick } from "../dom/delegateClick.js";
import {
    HTML_ATTRIBUTE_BLOCKER_CONNECTED,
    HTML_ATTRIBUTE_BLOCKER_ID,
    HTML_ATTRIBUTE_COOKIE_IDS,
    HTML_ATTRIBUTE_INLINE,
    HTML_ATTRIBUTE_RESET_PARENT_IS_RATIO_CONTAINER,
    HTML_ATTRIBUTE_UNBLOCKED_TRANSACTION_COMPLETE,
} from "../dom/htmlAttributes.js";
import { applyJQueryEventInitiator } from "../dom/initiators/jQueryEvent.js";
import { applyJQueryReadyInitiator } from "../dom/initiators/jQueryReady.js";
import { applyNativeEventListenerInitiator } from "../dom/initiators/nativeEventListener.js";
import { loadVideoSource } from "../dom/loadVideoSource.js";
import { putScriptInlineToDom } from "../dom/putScriptInlineToDom.js";
import { transformInlineStyleRules } from "../dom/transformInlineStyleRules.js";
import { transformToOriginalAttribute } from "../dom/transformToOriginalAttribute.js";
import {
    WAIT_SCRIPTS_SELECTOR_ASYNC,
    WAIT_SCRIPTS_SELECTOR_SYNC,
    WaitSynchronousScripts,
} from "../dom/waitSynchronousScripts.js";
import { OPT_IN_CONTENT_BLOCKER } from "../events/optInContentBlocker.js";
import { OPT_IN_CONTENT_BLOCKER_ALL } from "../events/optInContentBlockerAll.js";
import { dispatchResizeEvent } from "../utils/dispatchResizeEvent.js";
import { createVisual, setLastClickedConnectedCounter } from "../visual/createVisual.js";
import { detectLastClicked } from "../visual/detectLastClicked.js";
import { probablyResetParentContainerForVisual } from "../visual/probablyResetParentContainer.js";

import type { BlockerCheckerCallback } from "./decideToUnblock.js";
import type { OptInContentBlockerEvent } from "../events/optInContentBlocker.js";
import type { OptInContentBlockerAllEvent } from "../events/optInContentBlockerAll.js";
import type { BlockerMountCallback, VisualConfiguration } from "../visual/createVisual.js";

type FindAndUnblockOptions<ExtendedBlockerDefinition extends VisualConfiguration = VisualConfiguration> = {
    checker: BlockerCheckerCallback<ExtendedBlockerDefinition>;
    visual?: Omit<Parameters<typeof createVisual>[0], "node" | "blocker" | "mount"> & {
        mount: BlockerMountCallback<ExtendedBlockerDefinition>;
        unmount?: (contentBlocker: HTMLElement) => void;
        busy?: (contentBlocker: HTMLElement) => void;
    };
    /**
     * This allows to override the value of an unblocked attribute. Additionally, you can return `attribute`
     * to overwrite the name of the original attribute (e.g. `data-src` to `src`).
     */
    overwriteAttributeValue?: Parameters<typeof transformToOriginalAttribute>[0]["overwriteAttributeValue"];
    /**
     * This allows you to overwrite the original attribute name on unblocking when it is inside a given container.
     *
     * Example:
     *
     * When the unblocked node is `iframe`, has an attribute `data-src` and is inside a `.my-container>.video`
     * container, the `data-src` attribute gets exposed as `src`:
     *
     * ```
     * overwriteAttributeNameWhenMatches: [{
     *   // %s gets replaced by the unblocked node
     *   matches: ".my-container>.video>%s";
     *   node: "iframe";
     *   attribute: "data-src";
     *   to: "src";
     * }]
     * ```
     */
    overwriteAttributeNameWhenMatches?: Parameters<
        typeof transformToOriginalAttribute
    >[0]["overwriteAttributeNameWhenMatches"];
    /**
     * All transactions inclusive initiators (e.g. `$(document).ready()`) are executed.
     */
    transactionClosed?: (unblocked: ReturnType<typeof findBlockedNodes>) => void;
    /**
     * A priority step got unblocked. This allows e.g. to execute code after `div` elements got unblocked
     * and before a `script` get's included.
     */
    priorityUnblocked?: (unblocked: ReturnType<typeof findBlockedNodes>, priority: number) => void;
    customInitiators?: (ownerDocument: Document, defaultView: typeof window) => void;
    delegateClick?: Parameters<typeof delegateClick>[1];
    /**
     * `unblock`: Unblocks as expected in respect to `checker`.
     *
     * `skip`: The unblocking mechanism will be skipped and only visuals are created. This can be useful
     * if you want to time between `interactive` and `complete` `document.ready` state.
     *
     * In other words: Show visual content blockers in `interactive` state, and unblock in `complete` state.
     *
     * Use this in conjunction with `BANNER_PRE_DECISION_SHOW_INTERACTIVE_EVENT` and `APPLY_INTERACTIVE_EVENT` event.
     */
    mode?: "unblock" | "skip" | "busy";
};

/**
 * Refresh the DOM content depending on acceptance. It covers the following things:
 *
 * - Get all available blocked content
 * - Unblock blocked content depending on acceptance
 * - All other blocked content gets a visual content-blocker (if possible)
 */
async function findAndUnblock<ExtendedBlockerDefinition extends VisualConfiguration = VisualConfiguration>({
    checker,
    visual,
    overwriteAttributeValue,
    overwriteAttributeNameWhenMatches,
    transactionClosed,
    priorityUnblocked,
    customInitiators,
    delegateClick: delegateClickSelectors,
    mode,
}: FindAndUnblockOptions<ExtendedBlockerDefinition>) {
    setCurrentlyInTransaction(true);

    const nodes = findBlockedNodes<ExtendedBlockerDefinition>(checker);
    transformInlineStyleRules(checker);

    // A collection of all unblocked content for this "transaction"; so we can keep track a batch
    // of unblocked items to keep dependencies intact (e.g. Custom script is blocked and needs Google Maps
    // API do be available).
    const unblockedNodes: typeof nodes = [];
    let foundAnyLastClicked: (typeof nodes)[0];

    const unmount = (element: HTMLElement) => {
        visual?.unmount?.(element);
        probablyResetParentContainerForVisual(element, false);
        element.remove();
    };

    // In some cases, through custom event triggers and unblocked scripts, HTML elements could be "recreated" in our DOM
    // without our changes to mark the DOM node as "complete". Lets find those nodes and mark them correctly.
    document
        .querySelectorAll(
            `[${HTML_ATTRIBUTE_BLOCKER_ID}]:not(.rcb-content-blocker):not([${HTML_ATTRIBUTE_COOKIE_IDS}]):not([${HTML_ATTRIBUTE_UNBLOCKED_TRANSACTION_COMPLETE}])`,
        )
        .forEach((n) => n.setAttribute(HTML_ATTRIBUTE_UNBLOCKED_TRANSACTION_COMPLETE, "1"));

    // Reset all calculated and memorized results of ratio container as the CSS styles could be changed again (`probablyResetParentContainerForVisual`)
    document
        .querySelectorAll(`[${HTML_ATTRIBUTE_RESET_PARENT_IS_RATIO_CONTAINER}]`)
        .forEach((n) => n.removeAttribute(HTML_ATTRIBUTE_RESET_PARENT_IS_RATIO_CONTAINER));

    let previousPriority: number;
    let waitAsynchronousScripts: WaitSynchronousScripts;

    for (const row of nodes) {
        const { consent, node, isVisualCb, blocker, priority } = row;

        if (consent) {
            if (mode !== "unblock") {
                if (visual && isVisualCb) {
                    visual.busy?.(node);
                    continue;
                }
                continue;
            }

            // Got this node already be handled by another call?
            if (!node.hasAttribute(HTML_ATTRIBUTE_COOKIE_IDS)) {
                continue;
            } else if (isVisualCb) {
                unmount(node);
                continue;
            }

            // Allows to execute custom code when a given priority got completed
            if (previousPriority !== undefined && previousPriority !== priority) {
                priorityUnblocked?.(unblockedNodes, previousPriority);
            }
            previousPriority = priority;

            // Immediate deactivate nodes for future unblocks
            node.removeAttribute(HTML_ATTRIBUTE_COOKIE_IDS);

            const connectedBlocker = node.getAttribute(HTML_ATTRIBUTE_BLOCKER_CONNECTED);
            const isLastClicked = detectLastClicked(node);

            if (isLastClicked) {
                foundAnyLastClicked = row;
            }

            // Remove visual content blocker if not yet removed through above method
            if (connectedBlocker) {
                const contentBlockers = Array.prototype.slice.call(
                    document.querySelectorAll(`.rcb-content-blocker[consent-blocker-connected="${connectedBlocker}"]`),
                ) as HTMLElement[];
                for (const contentBlocker of contentBlockers) {
                    unmount(contentBlocker);
                }

                // Also reset parent containers stylings for nodes which not successfully created
                // a visual content blocker (e.g. duplicate exists)
                probablyResetParentContainerForVisual(node, false);
            }

            // Overwrite global listeners so they get immediate executed
            const { ownerDocument } = node;
            const { defaultView } = ownerDocument;
            applyJQueryReadyInitiator(ownerDocument);
            applyJQueryEventInitiator(ownerDocument, defaultView, "load", { isLoad: true }); // $(window).load()
            applyJQueryEventInitiator(ownerDocument, ownerDocument, "ready"); // $(document).on("ready")
            applyNativeEventListenerInitiator(defaultView, "load", { isLoad: true, definePropertySetter: "onload" });
            applyNativeEventListenerInitiator(ownerDocument, "DOMContentLoaded");
            applyNativeEventListenerInitiator(defaultView, "DOMContentLoaded");
            customInitiators?.(ownerDocument, defaultView);

            const waitSynchronousScripts = new WaitSynchronousScripts(WAIT_SCRIPTS_SELECTOR_SYNC);
            waitAsynchronousScripts =
                waitAsynchronousScripts || new WaitSynchronousScripts(WAIT_SCRIPTS_SELECTOR_ASYNC);

            // Activate node
            const hasInlineAttribute = node.hasAttribute(HTML_ATTRIBUTE_INLINE);
            const { performedClick, workWithNode } = await transformToOriginalAttribute({
                node,
                allowClickOverrides: hasInlineAttribute ? false : isLastClicked,
                onlyModifyAttributes: hasInlineAttribute,
                visualParentSelectors: visual?.visualParentSelectors,
                overwriteAttributeValue,
                overwriteAttributeNameWhenMatches,
            });

            if (hasInlineAttribute) {
                await putScriptInlineToDom(node as HTMLScriptElement);
            } else if (performedClick) {
                // Avoid auto replays between the same transaction
                setLastClickedConnectedCounter(undefined);
            }

            await Promise.all(waitSynchronousScripts.diff());

            // Allow to detach and attach again to DOM so e.g. `MutationObservers` can handle the DOM as expected
            if (workWithNode.getAttribute("consent-redom")) {
                const { parentElement } = workWithNode;
                if (parentElement) {
                    const idx = [...(parentElement.children as any)].indexOf(workWithNode);
                    parentElement.removeChild(workWithNode);
                    insertChildAtIndex(parentElement, workWithNode, idx);
                }
            }

            workWithNode.dispatchEvent(
                new CustomEvent<OptInContentBlockerEvent>(OPT_IN_CONTENT_BLOCKER, {
                    detail: {
                        blocker,
                        gotClicked: isLastClicked,
                    },
                }),
            );

            document.dispatchEvent(
                new CustomEvent<OptInContentBlockerEvent>(OPT_IN_CONTENT_BLOCKER, {
                    detail: {
                        blocker,
                        element: workWithNode,
                        gotClicked: isLastClicked,
                    },
                }),
            );

            if (isLastClicked && delegateClickSelectors && delegateClick(workWithNode, delegateClickSelectors)) {
                setLastClickedConnectedCounter(undefined);
            }

            unblockedNodes.push({
                ...row,
                node: workWithNode,
            });
        } else if (visual && !isVisualCb) {
            createVisual({
                node,
                blocker: blocker as ExtendedBlockerDefinition,
                ...visual,
            });
        }
    }

    // This transaction is "complete"
    if (unblockedNodes.length) {
        // Definitely reset now our last clicked counter to avoid double auto plays
        if (foundAnyLastClicked) {
            setLastClickedConnectedCounter(undefined);
        }

        // Do this before the events below to keep the initiators intact (e.g. jQuery.fn.ready)
        setCurrentlyInTransaction(false);

        const load = Promise.all(waitAsynchronousScripts.diff());

        document.dispatchEvent(
            new CustomEvent<OptInContentBlockerAllEvent>(OPT_IN_CONTENT_BLOCKER_ALL, {
                detail: {
                    unblockedNodes,
                    load,
                },
            }),
        );

        unblockedNodes.forEach(({ node }) => {
            node.setAttribute(HTML_ATTRIBUTE_UNBLOCKED_TRANSACTION_COMPLETE, "1");

            node.dispatchEvent(
                new CustomEvent<OptInContentBlockerAllEvent>(OPT_IN_CONTENT_BLOCKER_ALL, {
                    detail: {
                        unblockedNodes,
                        load,
                    },
                }),
            );
        });

        // The initiators (e.g. jQuery.ready) are all loaded in a new "thread" with a `setTimeout`,
        // but we need to make sure this event is dispatched afterwards.
        setTimeout(() => {
            transactionClosed?.(unblockedNodes);
            loadVideoSource(unblockedNodes);
            dispatchResizeEvent();

            // Scroll to unblocked, clicked element automatically
            if (foundAnyLastClicked) {
                const { node } = foundAnyLastClicked;
                if (!isElementInViewport(node)) {
                    node.scrollIntoView({
                        behavior: "smooth",
                    });
                }

                // Accessibility, make it focusable and focus it
                node.setAttribute("tabindex", "0");
                node.focus({ preventScroll: true });
            }
        }, 0);
    } else {
        setCurrentlyInTransaction(false);
    }
}

function insertChildAtIndex(container: HTMLElement, child: HTMLElement, index: number) {
    if (index >= container.children.length) {
        container.appendChild(child);
    } else {
        container.insertBefore(child, container.children[index]);
    }
}

export { findAndUnblock, type FindAndUnblockOptions };
