type ExtendedXMLHttpRequest = XMLHttpRequest & {
    _url: string;
};

type DecideToBlock = (url: string) => Promise<any>;

/**
 * Hijacks the `XMLHttpRequest` and `fetch` and sends the request as soon as consent is given.
 *
 * This works also with `jQuery.get` (inclusive other jQuery methods like `$.ajax`, `$.post`, etc.)
 * as it uses `XMLHttpRequest` under the hood.
 */
function hijackWebRequests(decideToBlock: DecideToBlock) {
    const { open: originalOpen, send: originalSend } = XMLHttpRequest.prototype;
    const { fetch: originalFetch } = window;

    XMLHttpRequest.prototype.open = function (
        this: ExtendedXMLHttpRequest,
        method: string,
        url: string,
        ...rest: any[]
    ) {
        try {
            this._url = new URL(url, window.location.href).href;
        } catch (error) {
            // Silence is golden.
        }

        // @ts-expect-error - rest is `any[]` but original function accepts a dedicated number of arguments
        originalOpen.call(this, method, url, ...rest);
    };

    XMLHttpRequest.prototype.send = function (this: ExtendedXMLHttpRequest, ...args: any[]) {
        const send = () =>
            // @ts-expect-error - rest is `any[]` but original function accepts a dedicated number of arguments
            originalSend.apply(this, args);
        const block = this._url ? decideToBlock(this._url) : undefined;
        if (block instanceof Promise) {
            // When `decideToBlock` rejects, we will still send the request
            block.then(send, send);
        } else {
            send();
        }
    };

    window.fetch = async function (input: RequestInfo | URL, init?: RequestInit) {
        const send = () => originalFetch(input, init);
        try {
            let url: string;

            if (input instanceof Request) {
                ({ url } = input);
            } else {
                url = `${input}`;
            }
            url = new URL(url, window.location.href).href;

            const block = url ? decideToBlock(url) : undefined;
            if (block instanceof Promise) {
                // When `decideToBlock` rejects, we will still send the request
                return block.then(send, send);
            } else {
                return send();
            }
        } catch (error) {
            return send();
        }
    };
}

export { hijackWebRequests };
