import { convertClientJobToPromise } from "./client/clientJobToPromise.js";
import { createJobToPopulateNextJobs } from "./job/jobToPopulateNextJobs.js";
import { LOCAL_STORAGE_KEY_RESTORE_JOBS } from "./restoreFromLocalStorage.js";
import { convertServerJobToPromise } from "./server/serverJobToPromise.js";
import { JOB_DONE_EVENT_PREFIX } from "../types/events/jobDone.js";
import { JOBS_DELETED_EVENT } from "../types/events/jobsDeleted.js";
import { JOBS_RETRIED_EVENT } from "../types/events/jobsRetried.js";
import { localStorageTest } from "../utils/localStorageTest.js";
import { getStaticOptions } from "../utils/options.js";

import type { JobDoneEvent } from "../types/events/jobDone.js";
import type { JobsDeletedEvent } from "../types/events/jobsDeleted.js";
import type { JobsRetriedEvent } from "../types/events/jobsRetried.js";
import type { Job } from "../types/job.js";
import type { Queue, QueueAddOptions } from "p-queue";

interface RealQueueOptions extends QueueAddOptions {
    job?: Job;
    id?: string;
    type?: string;
}

interface DefaultAddOptions extends QueueAddOptions {
    readonly job?: Job;
}

type RunFunction = () => Promise<unknown>;

type Task = (() => PromiseLike<unknown>) | (() => unknown);

/**
 * This class should be minimal so we can control the queue items itself.
 *
 * @see https://github.com/sindresorhus/p-queue#custom-RealQueueClass
 * @see https://github.com/sindresorhus/p-queue/blob/76b81cd707a6cd31b41f25e5d7fa8abc7486c4bf/source/priority-queue.ts#L12
 */
class RealQueueClass implements Queue<RunFunction, RealQueueOptions> {
    private queue: Array<RealQueueOptions & { run: RunFunction }> = [];

    private alreadyAddedJobIds: number[] = [];

    public constructor() {
        this.queue = [];

        // This is a bit hacky, but currently the only way to access the parent instance (?!)
        window.realQueueInstance = this;

        this.listenRetries();
        this.listenDeletions();
    }

    public enqueueFromList(list: Array<Job>) {
        const tryJobIds = list.filter(({ worker }) => worker === "server").map(({ id }) => id);
        const jobs: Array<{ runs: Task[]; job: Job }> = [];
        const { handler, alreadyAddedJobIds } = this;

        for (const job of list) {
            const { id, type } = job;

            // Avoid duplicate items
            if (alreadyAddedJobIds.indexOf(id) > -1) {
                continue;
            }

            if (job.worker === "client") {
                jobs.push({
                    runs: convertClientJobToPromise(this, job),
                    job,
                });
            } else {
                // The next request is not allowed to retry the previous one
                tryJobIds.splice(tryJobIds.indexOf(id), 1);
                jobs.push({
                    runs: convertServerJobToPromise(this, job, [/* pass immutable */ ...tryJobIds]),
                    job,
                });
            }

            alreadyAddedJobIds.push(id);

            // Listen to failed job and make the job available again to retry
            const listenerFailedEvent = `${JOB_DONE_EVENT_PREFIX}${type}`;
            const listenerFailed = (({
                detail: {
                    job: { id: doneJobId },
                    success,
                },
            }: CustomEvent<JobDoneEvent>) => {
                if (doneJobId === id && !success) {
                    document.removeEventListener(listenerFailedEvent, listenerFailed);
                    const alreadyIdx = this.alreadyAddedJobIds.indexOf(doneJobId);
                    if (alreadyIdx > -1) {
                        this.alreadyAddedJobIds.splice(alreadyIdx, 1);
                    }
                }
            }) as any;
            document.addEventListener(listenerFailedEvent, listenerFailed);
        }

        // Add a refresh job to populate our queue with the next "list"
        if (jobs.length > 0) {
            const previousAt = jobs.length > 40 ? 10 : 0;
            const { job: previousJob } = jobs[previousAt];

            jobs.splice(jobs.length - previousAt, 0, {
                runs: [createJobToPopulateNextJobs(list[list.length - 1].id, this)],
                job: previousJob,
            });
        }

        // Make this list restorable from the localStorage
        if (localStorageTest()) {
            const lsKey = `${LOCAL_STORAGE_KEY_RESTORE_JOBS}-${getStaticOptions().localStorageSuffix}`;
            const restorable = localStorage.getItem(lsKey)?.split(",").map(Number) || [];
            list.forEach(({ id }) => restorable.indexOf(id) === -1 && restorable.push(id));
            localStorage.setItem(lsKey, restorable.join(","));
        }

        jobs.forEach(({ job, runs }) => {
            handler.addAll(runs, {
                job,
            });
        });
    }

    public enqueue(run: RunFunction, options?: Partial<RealQueueOptions>) {
        if (!options?.job) {
            throw new Error(`Please provide a job object!`);
        }

        const {
            job: { id, type },
        } = options;

        const element: RealQueueClass["queue"][0] = {
            ...options,
            id: id.toString(),
            type,
            run,
        };

        this.queue.push(element);
    }

    public dequeue(): RunFunction | undefined {
        const item = this.queue.shift();
        return item?.run;
    }

    public get size() {
        return this.queue.length;
    }

    public filterQueueItem(options: RealQueueOptions) {
        return this.queue.filter(({ id, type }: Readonly<RealQueueOptions>) => {
            if (typeof options.id !== "undefined" && id !== options.id) {
                return false;
            }

            if (typeof options.type !== "undefined" && type !== options.type) {
                return false;
            }

            return true;
        });
    }

    public filter(options: RealQueueOptions) {
        return this.filterQueueItem(options).map((element: Readonly<{ run: RunFunction }>) => element.run);
    }

    public get handler() {
        return window.realQueueInstancePQueue;
    }

    private removeByType(type: string, forgetAboutAlreadyAdded = true) {
        const removedIds: number[] = [];
        this.queue = this.queue.filter((item) => {
            const result = item.type !== type;

            if (!result) {
                removedIds.push(+item.id);
            }

            return result;
        });

        if (forgetAboutAlreadyAdded) {
            this.alreadyAddedJobIds = this.alreadyAddedJobIds.filter((id) => removedIds.indexOf(id) === -1);
        }
    }

    /**
     * Listen if jobs got deleted, remove them from the queue so they never get executed again.
     */
    private listenDeletions() {
        const listener = (async ({
            detail: {
                params: { type },
            },
        }: CustomEvent<JobsDeletedEvent>) => {
            this.removeByType(type, false);
        }) as any;

        document.addEventListener(JOBS_DELETED_EVENT, listener);
    }

    /**
     * Listen if jobs got retried, remove them from the queue so they can be added again.
     */
    private listenRetries() {
        const listener = (async ({
            detail: {
                request: { type },
            },
        }: CustomEvent<JobsRetriedEvent>) => {
            this.removeByType(type);
        }) as any;

        document.addEventListener(JOBS_RETRIED_EVENT, listener);
    }

    public setPriority(id: string, priority: number) {
        // Currently not implemented in real-queue
    }
}

export { RealQueueClass, type RealQueueOptions, type DefaultAddOptions, type Task };
