import { ERouteHttpVerb } from "@devowl-wp/api";

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

import type { UrlBuilderArgs } from "./commonUrlBuilder.js";

type BatchRequestObject = {
    method: ERouteHttpVerb;
    path: string;
    body: Record<string, any>;
};

type BatchQueueItem = {
    resolve: (value: any) => void;
    reject: (reason?: any) => void;
    request: BatchRequestObject;
    options: UrlBuilderArgs["options"] & {
        /**
         * This method gets called when a queue item got finished (resolved or rejected).
         * It does not return the current value for the request, for this you need to use
         * the result of the original `commonRequest` call.
         *
         * This is called *before* the `commonRequest` Promise gets resolved and ignores
         * the passed `waitForPromise`.
         */
        onQueueItemFinished?: (state: boolean) => void;
        /**
         * Wait for this promise to be resolved before resolving the `commonRequest` Promise.
         * This is useful if you use a huge list in your e.g. React component and you want to
         * delay UI changes.
         */
        waitForPromise?: Promise<void>;
        signal?: AbortSignal;
    };
};

/**
 * Currently, we do not respect `rest_get_max_batch_size` as chunk size, but go with
 * a good-to-go default of 15 requests per batch request.
 *
 * @see https://developer.wordpress.org/reference/hooks/rest_get_max_batch_size/
 */
const BATCH_MAX_CHUNK_SIZE = 25;

let queue: Array<BatchQueueItem> = [];

let timeout: ReturnType<typeof setTimeout>;

const defaultWaitForPromise = Promise.resolve();

async function doBatchRequest() {
    // Remove aborted queue elements
    queue = queue.filter(
        ({ options: { signal, onQueueItemFinished, waitForPromise = defaultWaitForPromise }, reject }) => {
            if (signal?.aborted) {
                onQueueItemFinished?.(false);
                waitForPromise.then(() => reject(signal.reason));
                return false;
            }
            return true;
        },
    );

    // Create chunk for this call
    const chunk = queue.splice(0, BATCH_MAX_CHUNK_SIZE);

    if (chunk.length === 0) {
        // Nothing to do.
        return;
    }

    // Run the batch request

    try {
        const [{ options }] = chunk;
        const { signal, onQueueItemFinished, waitForPromise = defaultWaitForPromise } = options;
        const { responses } = await commonRequest<
            { requests: BatchRequestObject[] },
            never,
            {
                responses: Array<{
                    body: Record<string, any>;
                    headers: Record<string, string | string[]>;
                    status: number;
                }>;
            }
        >({
            location: {
                path: "/",
                method: ERouteHttpVerb.POST,
                namespace: "batch/v1",
            },
            options,
            request: {
                requests: chunk.map(({ request }) => request),
            },
            settings: {
                signal,
            },
        });

        for (let i = 0; i < responses.length; i++) {
            const { resolve, reject } = chunk[i];
            const { body, status } = responses[i];
            const isOk = status >= 200 && status < 400;
            onQueueItemFinished?.(isOk);

            // await waitForPromise; // we should not block the `for`
            waitForPromise.then(() => {
                if (isOk) {
                    resolve(body);
                } else {
                    reject({
                        responseJSON: body,
                    });
                }
            });
        }
    } catch (e) {
        // Something went wrong for all requests, so let them individually know
        for (const {
            reject,
            options: { onQueueItemFinished, waitForPromise = defaultWaitForPromise },
        } of chunk) {
            onQueueItemFinished?.(false);

            // await waitForPromise; // we should not block the `for`
            waitForPromise.then(() => reject(e));
        }
    }

    // Are there still requests open? So call again
    if (queue.length > 0) {
        doBatchRequest();
    }
}

/**
 * Add a request to the batch request pool and execute it accordingly. Please note, that
 * only non-GET requests *and* routes with enabled `allow_batch` *and* non-multipart request
 * are suitable for the batch requests in WordPress.
 *
 * @see https://make.wordpress.org/core/2020/11/20/rest-api-batch-framework-in-wordpress-5-6/
 */
function batchRequest<TResponse>(request: BatchRequestObject, options: BatchQueueItem["options"]) {
    return new Promise<TResponse>((resolve, reject) => {
        queue.push({
            resolve,
            reject,
            request,
            options,
        });

        clearTimeout(timeout);
        timeout = setTimeout(doBatchRequest, 100);
    });
}

export { batchRequest, type BatchQueueItem, type BatchRequestObject, BATCH_MAX_CHUNK_SIZE };
