import $ from "jquery";
import { action, computed, flow, observable, runInAction } from "mobx";

import { DoneItem } from "../models/doneItem.js";
import { Handler } from "../models/handler.js";
import { PausedError } from "../models/pausedError.js";
import { QueueItem } from "../models/queueItem.js";
import { Hooks } from "../utils/hooks.js";
import { request } from "../utils/request.js";
import { locationRestQueueItemPost } from "../wp-api/queue-item.post.js";
import { locationRestQueueGet } from "../wp-api/queue.get.js";
import { locationRestQueuePost } from "../wp-api/queue.post.js";

import type { RootStore } from "./stores.js";
import type { ActionPhysicalUpdatedParams } from "../utils/hooks.js";
import type {
    ParamsRestQueueItemPost,
    RequestRestQueueItemPost,
    ResponseRestQueueItemPost,
} from "../wp-api/queue-item.post.js";
import type { ParamsRestQueueGet, RequestRestQueueGet, ResponseRestQueueGet } from "../wp-api/queue.get.js";
import type { ParamsRestQueuePost, RequestRestQueuePost, ResponseRestQueuePost } from "../wp-api/queue.post.js";

type CancellablePromise<R> = ReturnType<ReturnType<typeof flow<R, any>>>;

class QueueStore {
    @observable
    public handlers: Handler[] = [];

    @observable
    public queue: QueueItem[] = [];

    @observable
    public done: DoneItem[] = [];

    @observable
    public countdown = 0;

    @observable
    public count = 0;

    @observable
    public pausedError: PausedError;

    @observable
    public estimate: string;

    public readonly rootStore: RootStore;

    private countdownTimeout: ReturnType<typeof window.setTimeout>;

    private forceNextProcessMethod: RequestRestQueuePost["skip"];

    private skipNextCount = false;

    @computed public get isHandlerActive() {
        return !!this.handlers.filter((h) => h.isHandler).length;
    }

    @computed public get canAutomaticQueueing() {
        return this.rootStore.optionStore.others.isAutomaticQueueing && this.isHandlerActive;
    }

    @computed public get doneUnique() {
        let key;
        let row;
        const unique: { [key: string]: 0 } = {};
        const distinct: DoneItem[] = [];
        for (let i = 0; i < this.done.length; i++) {
            row = this.done[i];
            key = `${row.filename}${row.destinationPath}`;
            if (typeof unique[key] === "undefined") {
                distinct.push(row);
            }
            unique[key] = 0;

            if (distinct.length === 3) {
                break;
            }
        }
        return distinct;
    }

    public constructor(rootStore: RootStore) {
        this.rootStore = rootStore;

        runInAction(() => {
            const { handlers, initialQueue, initialPausedError } = this.rootStore.optionStore.others;
            this.handlers = handlers.map((h) => new Handler(h, this));
            this.queue = initialQueue.rows.map((q) => new QueueItem(q));
            if (initialPausedError) {
                this.pausedError = new PausedError(initialPausedError, this);
            }
            this.estimate = initialQueue.estimate;
            this.count = initialQueue.count;
        });

        this.reinitCountdown();
    }

    public addItem: (id: number) => CancellablePromise<void> = flow(function* (this: QueueStore, id) {
        yield request<RequestRestQueueItemPost, ParamsRestQueueItemPost, ResponseRestQueueItemPost>({
            location: locationRestQueueItemPost,
            params: {
                id,
            },
        });
        this.refresh();
    });

    public handleCountdown: () => CancellablePromise<void> = flow(function* (this: QueueStore) {
        if (this.countdown <= 0 && !this.pausedError) {
            try {
                yield this.process(this.count > 0 || this.skipNextCount ? "POST" : "GET");
            } catch (e) {
                if (e.responseJSON) {
                    const { responseJSON } = e;

                    // Pause the queue and show error
                    this.pausedError = new PausedError(responseJSON, this);
                }
                console.error(e);
            } finally {
                this.reinitCountdown();
            }
        } else {
            this.countdown = this.countdown - 1;
            this.countdownTimeout = setTimeout(this.handleCountdown.bind(this), 1000);
        }
        this.skipNextCount = false;
    });

    public process: (
        method: "POST" | "GET",
    ) => CancellablePromise<ResponseRestQueueGet & Partial<ResponseRestQueuePost>> = flow(function* (
        this: QueueStore,
        method,
    ) {
        this.countdown = 0;
        clearTimeout(this.countdownTimeout);

        let response: ResponseRestQueueGet & Partial<ResponseRestQueuePost>;
        if (method === "GET") {
            response = yield request<RequestRestQueueGet, ParamsRestQueueGet, ResponseRestQueueGet>({
                location: locationRestQueueGet,
            });
        } else {
            response = yield request<RequestRestQueuePost, ParamsRestQueuePost, ResponseRestQueuePost>({
                location: locationRestQueuePost,
                request: {
                    skip: this.forceNextProcessMethod,
                },
            });
        }

        const { rows, count, estimate, done, doneUrlPrefix } = response;

        this.forceNextProcessMethod = undefined;
        this.count = count;
        this.estimate = estimate;
        this.queue = rows.map((q) => new QueueItem(q));

        // Check if files were processed and call a hook
        $.each(done, (attachment, { d, f, i, u, fid, ms }) => {
            const urls: string[] = [];
            $.each(u, (key, value) => {
                u[key] = doneUrlPrefix + value;
                urls.push(u[key]);
            });

            // Add to done
            const done = new DoneItem({
                attachment,
                isImage: i,
                currentFolderId: fid,
                filename: f,
                destinationPath: d,
                preview: typeof u["thumbnail"] === "string" ? u["thumbnail"] : "",
                duration: ms,
            });
            this.done.unshift(done);

            // Hook
            try {
                Hooks.handler.doAction(Hooks.ACTION_PHYSICAL_UPDATED, {
                    done,
                    urls,
                    newSizes: u,
                } as ActionPhysicalUpdatedParams);
            } catch (e) {
                console.error(e);
            }
        });
        return response;
    });

    @action
    public setForceNextProcessMethod(method: QueueStore["forceNextProcessMethod"]) {
        this.forceNextProcessMethod = method;
        return this;
    }

    @action
    public refresh() {
        this.pausedError = undefined;
        this.skipNextCount = true;
        this.countdown = 0;
    }

    @action
    private reinitCountdown() {
        const { pause, processing } = this.rootStore.optionStore.others.countDown;
        this.countdown = this.count > 0 ? processing : pause;
        this.handleCountdown();
    }
}

export { QueueStore };
