import { getGlobalQueue } from "./global.js";
import { refreshQueue } from "./refreshQueue.js";
import { STATUS_EVENT } from "../types/events/status.js";
import { STATUS_ADDITIONAL_DATA_EVENT } from "../types/events/statusAdditionalData.js";
import { request } from "../utils/request.js";
import { locationRestStatusGet } from "../wp-api/status.get.js";

import type { StatusEvent } from "../types/events/status.js";
import type { StatusAdditionalData } from "../types/events/statusAdditionalData.js";
import type { ParamsRouteStatusGet, RequestRouteStatusGet, ResponseRouteStatusGet } from "../wp-api/status.get.js";

let timeoutInterval: ReturnType<typeof setTimeout>;
let timeoutImmediate: ReturnType<typeof setTimeout>;
let lastIntervalUsed: number;
let lastRequestStarted: number;
let currentlyFetching = false;

function setCurrentlyFetchingStatus(state: typeof currentlyFetching) {
    currentlyFetching = state;
}

function getConfigurableStatusParameters(): ParamsRouteStatusGet {
    const settings: {
        additionalData: string[];
    } = {
        additionalData: [],
    };

    document.dispatchEvent(
        new CustomEvent<StatusAdditionalData>(STATUS_ADDITIONAL_DATA_EVENT, {
            detail: {
                settings,
            },
        }),
    );

    return {
        additionalData: settings.additionalData.join(","),
    };
}

/**
 * Fetch the status of the complete queue in a given interval. In general, the queue pulls always
 * the status, but you could change the interval to provide a more "up-to-date" UI to your users.
 *
 * You need to listen to the fetch-status event.
 *
 * Additionally, if you pass an object, you can set the status from an external "fetcher", e.g. by
 * saving requests while scanning a website and use a comment at the end of the document like https://regex101.com/r/O6O1X1/1.
 */
async function fetchStatus(
    intervalOrImmediate:
        | {
              from: "html";
              html: string;
          }
        | {
              from: "object";
              status: ResponseRouteStatusGet;
          }
        | number
        | true = 60000,
    immediate = false,
) {
    const received = async (detail: ResponseRouteStatusGet) => {
        document.dispatchEvent(
            new CustomEvent<StatusEvent>(STATUS_EVENT, {
                detail,
            }),
        );

        // Check if the queue is currently empty, but there are still remaining items, try to get them
        const { remaining, errors } = detail;
        if (
            // Only the worker should refresh the queue as needed
            window.realQueueWorker &&
            getGlobalQueue().size === 0 &&
            Object.values(remaining).length > 0 &&
            // Do not load new items to the queue until the errors are resolved
            Object.values(errors.list).length === 0
        ) {
            refreshQueue(false);
        }
    };

    const nextTick = () => {
        if (lastIntervalUsed > 0) {
            clearTimeout(timeoutInterval);
            timeoutInterval = setTimeout(fn, lastIntervalUsed);
        }
    };

    // When there are UI changes which takes a bit longer, we can just wait a bit for the next render (e.g. when using React hooks `useProgress`)
    const nextImmediate = () =>
        new Promise<void>((resolve) => {
            clearTimeout(timeoutImmediate);
            timeoutImmediate = setTimeout(() => fn(false).finally(resolve), 500);
        });

    const fn = async (respectCurrentlyFetching = true) => {
        if (
            (respectCurrentlyFetching &&
                // There is currently a fetch active, we do not need to send another request
                (currentlyFetching ||
                    // The last request is very young (not longer than 5 seconds since now)
                    (lastRequestStarted > 0 && new Date().getTime() - lastRequestStarted < 5000))) ||
            // This tab is currently not working, skip status if tab is inactive
            (!window.realQueueWorker && document.visibilityState === "hidden")
        ) {
            // There is still a request, skip this interval
            return respectCurrentlyFetching ? nextTick() : undefined;
        }

        currentlyFetching = true;
        lastRequestStarted = new Date().getTime();
        try {
            const detail = await request<RequestRouteStatusGet, ParamsRouteStatusGet, ResponseRouteStatusGet>({
                location: locationRestStatusGet,
                params: getConfigurableStatusParameters(),
            });
            await received(detail);
        } finally {
            currentlyFetching = false;

            if (respectCurrentlyFetching) {
                nextTick();
            }
        }
    };

    if (typeof intervalOrImmediate === "number") {
        lastIntervalUsed = intervalOrImmediate;
        nextTick();
        immediate && (await nextImmediate());
    } else if (intervalOrImmediate === true) {
        await nextImmediate();
    } else if (typeof intervalOrImmediate === "object" && intervalOrImmediate.from) {
        // Break the previous interval
        if (lastIntervalUsed > 0) {
            fetchStatus(lastIntervalUsed);
        }

        switch (intervalOrImmediate.from) {
            case "html": {
                const { html } = intervalOrImmediate;
                const jsonString = html.match(/^\s*<!--real-queue-status:(.*)-->$/m);
                if (jsonString) {
                    await received(JSON.parse(jsonString[1]));
                } else {
                    await fetchStatus(true);
                }
                break;
            }
            case "object": {
                const { status } = intervalOrImmediate;
                await received(status);
                break;
            }
            default:
                break;
        }
    }
}

export { getConfigurableStatusParameters, fetchStatus, setCurrentlyFetchingStatus };
