import { computed, flow, observable, runInAction, set } from "mobx";

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

import type { ClientCollection } from "./clientCollection.js";
import type { commonRequest } from "../../factory/ajax/commonRequest.js";
import type {
    RouteLocationInterface,
    RouteParamsInterface,
    RouteResponseInterface,
} from "../../factory/ajax/commonUrlBuilder.js";
import type { createRequestFactory } from "../../factory/ajax/createRequestFactory.js";

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

type AnnotateDefinition = {
    keyId: string;
    request: ReturnType<typeof createRequestFactory>["request"];
    namespace?: RouteLocationInterface["namespace"];
    create?: {
        path: RouteLocationInterface["path"];
        method?: ERouteHttpVerb;
    };
    patch?: {
        path: RouteLocationInterface["path"];
        method?: ERouteHttpVerb;
    };
    delete?: {
        path: RouteLocationInterface["path"];
        method?: ERouteHttpVerb;
    };
};

interface ModelDefinition<Key, Properties extends RouteResponseInterface, Collection extends ClientCollection<any>> {
    key: Key;
    properties: Properties;
    collection: Collection;
    create?: {
        request?: Properties;
        parameters?: RouteParamsInterface;
        response?: Properties;
    };
    patch?: {
        request?: Partial<Properties>;
        parameters: RouteParamsInterface;
        response?: Properties;
    };
    delete?: {
        parameters: RouteParamsInterface;
        response: Properties;
    };
}

abstract class ClientModel<T extends ModelDefinition<any, any, any>> {
    @observable
    public data: T["properties"] = {};

    @observable
    public collection: T["collection"];

    @observable
    public busy = false;

    public readonly annotated?: AnnotateDefinition;

    @computed
    public get key(): T["key"] {
        return this.data?.[this.annotated.keyId];
    }

    public constructor(collection: T["collection"], data: Partial<T["properties"]> = {}) {
        setTimeout(() => {
            if (!this.annotated) {
                console.error("You have not used the @ClientModel.annotate annoation together with this class!");
            }
        }, 0);

        runInAction(() => {
            this.collection = collection;
            this.data = data;
        });
    }

    public static annotate? =
        <T extends { new (...args: any[]): any }>(annotate: AnnotateDefinition) =>
        (ctor: T) => {
            return class extends ctor {
                // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
                annotated = annotate;

                public constructor(...args: any[]) {
                    super(...args);
                }
            };
        };

    public fromResponse(response: T["properties"]) {
        set(this.data, response);
        return this;
    }

    public persist: (
        params?: T["create"]["parameters"],
        /**
         * When you pass `allowBatchRequest`, the `afterPersist` is not called due to performance reasons. Please
         * call `afterPersist` and `this.collection.entries.set` manually!
         */
        settings?: Pick<Parameters<typeof commonRequest>[0], "allowBatchRequest" | "settings">,
    ) => CancellablePromise<void> = flow(function* (this: ClientModel<any>, params, settings) {
        if (!this.annotated.create) {
            throw new Error("There is no persist method allowed");
        }

        this.busy = true;
        try {
            const {
                create: { path, method },
                namespace,
            } = this.annotated;
            const response: T["create"]["response"] = yield this.annotated.request<
                T["create"]["request"],
                T["create"]["parameters"],
                T["create"]["response"]
            >({
                location: {
                    path,
                    method: method || ERouteHttpVerb.POST,
                    namespace,
                },
                request: this.transformDataForPersist(),
                params: params || {},
                ...(settings || {}),
            });

            this.fromResponse(response);
            if (!settings?.allowBatchRequest) {
                this.collection.entries.set(this.key, this);
                this.afterPersist();
            }
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busy = false;
        }
    });

    public patch: (params?: T["patch"]["parameters"]) => CancellablePromise<void> = flow(function* (
        this: ClientModel<any>,
        params,
    ) {
        if (!this.annotated.patch) {
            throw new Error("There is no patch method allowed");
        }

        this.busy = true;
        try {
            const {
                patch: { path, method },
                namespace,
            } = this.annotated;
            const response: T["patch"]["response"] = yield this.annotated.request<
                T["patch"]["request"],
                T["patch"]["parameters"],
                T["patch"]["response"]
            >({
                location: {
                    path,
                    method: method || ERouteHttpVerb.PATCH,
                    namespace,
                },
                request: this.transformDataForPatch(),
                params: {
                    ...{ [this.annotated.keyId]: this.key },
                    ...(params || {}),
                },
            });

            this.fromResponse(response);
            this.afterPatch();
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busy = false;
        }
    });

    public delete: (
        params?: T["delete"]["parameters"],
        /**
         * When you pass `allowBatchRequest`, the `afterDelete` is not called due to performance reasons. Please
         * call `afterDelete` and `this.collection.entries.delete` manually!
         */
        settings?: Pick<Parameters<typeof commonRequest>[0], "allowBatchRequest" | "settings">,
    ) => CancellablePromise<T["delete"]["response"]> = flow(function* (this: ClientModel<any>, params, settings) {
        if (!this.annotated.delete) {
            throw new Error("There is no delete method allowed");
        }

        this.busy = true;
        try {
            const {
                delete: { path, method },
                namespace,
            } = this.annotated;
            const response: T["delete"]["response"] = yield this.annotated.request<
                T["delete"]["parameters"],
                T["delete"]["parameters"],
                T["delete"]["response"]
            >({
                location: {
                    path,
                    method: method || ERouteHttpVerb.DELETE,
                    namespace,
                },
                params: {
                    ...{ [this.annotated.keyId]: this.key },
                    ...(params || {}),
                },
                ...(settings || {}),
            });

            if (!settings?.allowBatchRequest) {
                this.collection.entries.delete(this.key);
                this.afterDelete();
            }
            return response;
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busy = false;
        }
    });

    /**
     * Transform the class-hold data to POSTable data. This can be useful if e. g.
     * one property differs from the response property schema.
     */
    public transformDataForPersist(): any {
        return this.data;
    }

    /**
     * Create your conditionals here and return only changed values.
     */
    public transformDataForPatch(): any {
        throw new Error("If you want to use patch method, you need to implement transformDataForPatch!");
    }

    public afterPersist() {
        // Silence is golden.
    }

    public afterPatch() {
        // Silence is golden.
    }

    public afterDelete() {
        // Silence is golden.
    }
}

export { ClientModel };
