import { htmlDecode, yieldMainThread } from "@devowl-wp/react-utils";

import { applyDynamicsToHtml } from "./applyDynamicsToHtml.js";

type PostscribeToken = {
    attrs: Record<string, string>;
    booleanAttrs?: Record<string, string>;
    src?: string;
    href?: string;
    content?: string;
    tagName: string;
    /**
     * @see https://app.clickup.com/t/869brj1eb
     */
    releaseImmediately?: boolean;
};

/**
 * Put HTML code to the current DOM. `script` tags gets automatically
 * executed instead of pushing to DOM.
 *
 * @see https://github.com/krux/postscribe
 * @see https://stackoverflow.com/a/49724894/5506547
 */
function putHtmlCodeToDom(
    html: string,
    dynamics: Parameters<typeof applyDynamicsToHtml>[1],
    referenceNode: Element = document.body,
) {
    return new Promise<void>((resolve) => {
        if (html) {
            yieldMainThread().then(() =>
                import(/* webpackChunkName: "banner-lazy", webpackMode: "lazy-once" */ "postscribe").then(
                    ({ default: postscribe }) =>
                        postscribe(referenceNode, applyDynamicsToHtml(html, dynamics), {
                            done: resolve,
                            error: (e: Error) => {
                                // We do ignore all errors as we handle like the usual browser
                                // E.g. a script blocked by an ad-blocker should not break the execution
                                // for upcoming scripts.
                                console.error(e);
                            },
                            beforeWriteToken: (token: PostscribeToken) => {
                                const { attrs, booleanAttrs, src, href, content, tagName } = token;
                                let useSrc = src;

                                // Probably skip this token? (do not use `disabled` tag as it should still be written to DOM)
                                if (booleanAttrs?.["skip-write"]) {
                                    return false;
                                }

                                // Automatically unescape html entities in all attributes (https://github.com/krux/postscribe/issues/346#issuecomment-310227387)
                                for (const attr in attrs) {
                                    attrs[attr] = htmlDecode(attrs[attr]);

                                    // Allow to skip complete HTML tag when it already got loaded by unique ID (e.g. Google Tag Manager gtag.js)
                                    if (
                                        attr === "unique-write-name" &&
                                        document.querySelector(`[unique-write-name="${attrs[attr]}"]`)
                                    ) {
                                        return false;
                                    }
                                }

                                // Address scripts that have a "corrupted" `src` and are not inline, as this may result in `/undefined` requests.
                                // This applies only to scripts that have been altered by lazy-load scripts that set `script.src=script.dataset.src`.
                                // It is **not** applicable to scripts that have not been modified by lazy-load scripts, since attempting to "fix" them would disrupt the script execution
                                // order. As a workaround, we look for another attribute that is defined and contains a `.js`.
                                if (tagName === "script" && content?.trim() === "" && useSrc === "undefined") {
                                    useSrc = Object.entries(attrs).find(([key, value]) => {
                                        try {
                                            if (["id", "src", "type"].indexOf(key) === -1) {
                                                const { pathname } = new URL(value, window.location.href);

                                                if (
                                                    pathname.indexOf(".js") > -1 ||
                                                    // e.g. external scripts like https://maps.googleapis.com/maps/api/js?key=
                                                    value.startsWith("http")
                                                ) {
                                                    return value;
                                                }
                                            }
                                        } catch (e) {
                                            // Silence is golden.
                                        }
                                        return undefined;
                                    })?.[1];
                                }

                                // If the script is loading a resource, we need to wait for the resource to load and this is done
                                // in `postscribe` via `_writeScriptToken` -> `_scriptLoadHandler`.
                                // @see https://github.com/krux/postscribe/blob/70cde6b9d21d345a3e2e8eb10a6de0b17b330595/src/write-stream.js#L489
                                // This leads to issues when we add a script which does not load a resource, but rather
                                // adds content to the DOM, e.g. `<script type="text/plain"></script>`.
                                // See also `ScriptInlineMatch#isJavascript` in `@devowl-wp/fast-html-tag`.
                                if (tagName === "script" && useSrc && !isJavaScript(attrs.type)) {
                                    useSrc = undefined;
                                    token.src = useSrc;

                                    // Make it trigger the `async` release
                                    // @see https://github.com/krux/postscribe/blob/70cde6b9d21d345a3e2e8eb10a6de0b17b330595/src/write-stream.js#L500
                                    // For this, we need to patch `postscribe` and add `releaseImmediately` to the token.
                                    token.releaseImmediately = true;
                                }

                                // Scripts
                                if (useSrc) {
                                    token.src = htmlDecode(useSrc);
                                }

                                // Styles
                                if (href) {
                                    token.href = htmlDecode(href);
                                }

                                return token;
                            },
                        }),
                ),
            );
        } else {
            resolve();
        }
    });
}

// Keep this synchronized with `ScriptInlineMatch#isJavascript` in `@devowl-wp/fast-html-tag`.
function isJavaScript(type: string) {
    if (["module"].includes(type)) {
        return true;
    }
    return !type ? true : type.includes("javascript");
}

export { putHtmlCodeToDom, isJavaScript };
