import { action, computed, flow, observable, reaction } from "mobx";

import type { TreeNodeProps } from "@devowl-wp/react-folder-tree";

import { CategoryNode } from "../models/categoryNode.js";
import { __ } from "../utils/i18n.js";
import { request } from "../utils/request.js";
import { locationRestHierarchyPut } from "../wp-api/hierarchy.put.js";
import { locationRestTermsPost } from "../wp-api/terms.post.js";
import { locationRestTreeGet } from "../wp-api/tree.get.js";

import type { RootStore } from "./stores.js";
import type {
    ParamsRouteHierarchyPut,
    RequestRouteHierarchyPut,
    ResponseRouteHierarchyPut,
} from "../wp-api/hierarchy.put.js";
import type { ParamsRouteTermsPost, RequestRouteTermsPost, ResponseRouteTermsPost } from "../wp-api/terms.post.js";
import type { ParamsRouteTreeGet, RequestRouteTreeGet, ResponseRouteTreeGet } from "../wp-api/tree.get.js";

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

class TreeStore {
    public static ID_ALL = "ALL";

    @observable
    public staticTree: CategoryNode[] = [];

    @observable
    public tree: CategoryNode[] = [];

    @observable
    public selected: CategoryNode;

    @observable
    public busy = false;

    @observable
    public createRoot: TreeNodeProps;

    public refs: Map<CategoryNode["id"], CategoryNode> = new Map();

    public readonly rootStore: RootStore;

    @computed public get selectedId() {
        return this.selected?.id;
    }

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

        // Listen to taxonomy changes and refetch tree
        reaction(
            () => this.rootStore.optionStore.others.taxnow,
            () =>
                this.fetchTree({
                    remember: true,
                }),
        );

        // Make the init async to avoid stack overflow
        setTimeout(this.init.bind(this));
    }

    /**
     * Do not use this directly, use CategoryNode#setSelected instead.
     *
     * @param node
     */
    @action
    public setSelected(node?: CategoryNode) {
        if (this.selected) {
            this.selected.selected = false;
        }

        this.selected = node;
    }

    @action
    public setCreateRoot(data: TreeNodeProps) {
        this.createRoot = data;
    }

    @action
    private init() {
        this.staticTree.push(
            new CategoryNode(
                {
                    id: "ALL",
                    title: __("All posts"),
                    icon: "copy",
                    count: this.rootStore.optionStore.others.allPostCnt,
                },
                this,
            ),
        );

        // Initially fetch tree
        if (this.rootStore.optionStore.others.screenSettings.isActive) {
            this.fetchTree();
        }
    }

    public byId(id: CategoryNode["id"], excludeStatic = true) {
        const element = this.refs.get(id);
        if (excludeStatic && this.staticTree.indexOf(element) > -1) {
            return undefined;
        }
        return element;
    }

    public fetchTree: (
        params?: Partial<ParamsRouteTreeGet>,
        callback?: (result: ResponseRouteTreeGet) => void,
    ) => CancellablePromise<void> = flow(function* (this: TreeStore, params, callback) {
        this.busy = true;
        const { taxnow, typenow } = this.rootStore.optionStore.others;

        if (!taxnow || !typenow) {
            return;
        }

        const result: ResponseRouteTreeGet = yield request<
            RequestRouteTreeGet,
            ParamsRouteTreeGet,
            ResponseRouteTreeGet
        >({
            location: locationRestTreeGet,
            sendReferer: true,
            params: Object.assign<ParamsRouteTreeGet, Partial<ParamsRouteTreeGet>>(
                {
                    remember: false,
                    taxonomy: taxnow,
                    type: typenow,
                },
                params,
            ),
        });

        const { selectedId, tree } = result;

        this.tree = tree.map(CategoryNode.mapFromRestEndpoint.bind(this));
        this.busy = false;
        this.byId(selectedId, false).setSelected(true);

        callback?.(result);
    });

    public persist: (data: RequestRouteTermsPost) => CancellablePromise<CategoryNode> = flow(function* (
        this: TreeStore,
        data,
    ) {
        const result: ResponseRouteTermsPost = yield request<
            RequestRouteTermsPost,
            ParamsRouteTermsPost,
            ResponseRouteTermsPost
        >({
            location: locationRestTermsPost,
            request: data,
        });

        const newObj = CategoryNode.mapFromRestEndpoint.apply(this, [
            {
                category_name: result.category_name,
                childNodes: [],
                count: result.count,
                editableSlug: result.editableSlug,
                name: result.name,
                post_type: result.post_type,
                queryArgs: result.queryArgs,
                taxonomy: result.taxonomy,
                term_id: result.term_id,
            },
        ]);

        const { parent } = data;
        if (parent === 0) {
            this.tree.push(newObj);
        } else {
            this.byId(parent).addChildNode(newObj);
        }
        return newObj;
    });

    public sort: (data: {
        id: number;
        oldIndex: number;
        newIndex: number;
        parentFromId: number;
        parentToId: number;
        nextId: number;
        request?: boolean;
    }) => CancellablePromise<void | boolean> = flow(function* (
        this: TreeStore,
        { id, oldIndex, newIndex, parentFromId, parentToId, nextId, ...rest },
    ) {
        // Find node which gets moved and detach it from tree
        const fromList = parentFromId === 0 ? this.tree : this.byId(parentFromId).childNodes;
        const toList = parentToId === 0 ? this.tree : this.byId(parentToId).childNodes;
        const movement = fromList[oldIndex];
        fromList.splice(oldIndex, 1);
        toList.splice(newIndex, 0, movement);

        if (!rest.request) {
            return true;
        }

        const { typenow, taxnow } = this.rootStore.optionStore.others;

        // Request to backend
        try {
            yield request<RequestRouteHierarchyPut, ParamsRouteHierarchyPut, ResponseRouteHierarchyPut>({
                location: locationRestHierarchyPut,
                params: {
                    id,
                },
                request: {
                    nextId,
                    parent: parentToId,
                    type: typenow,
                    taxonomy: taxnow,
                },
            }) as Promise<boolean>;
            return true;
        } catch (e) {
            yield this.sort({
                id,
                oldIndex: newIndex,
                newIndex: oldIndex,
                parentFromId: parentToId,
                parentToId: parentFromId,
                nextId,
                request: false,
            });
            throw e;
        }
    });
}

export { TreeStore };
