import { flow, observable } from "mobx";

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

import type { ClientModel } from "./clientModel.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 = {
    path: RouteLocationInterface["path"];
    singlePath?: RouteLocationInterface["path"];
    namespace?: RouteLocationInterface["namespace"];
    methods: ERouteHttpVerb[];
    request: ReturnType<typeof createRequestFactory>["request"];
};

interface CollectionDefinition<
    SingleValue extends ClientModel<any>,
    Response extends RouteResponseInterface = Array<SingleValue["data"]>,
> {
    model: SingleValue;
    getParameters: RouteParamsInterface;
    getSingleParameters?: RouteParamsInterface;
    response?: Response;
}

abstract class ClientCollection<T extends CollectionDefinition<ClientModel<any>>> {
    @observable
    public entries = new Map<T["model"]["key"], T["model"]>();

    @observable
    public busy = false;

    public readonly annotated?: AnnotateDefinition;

    public constructor() {
        setTimeout(() => {
            if (!this.annotated) {
                console.error("You have not used the @ClientCollection.annotate annoation together with this class!");
            }
        }, 0);
    }

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

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

    public get: (data?: {
        request?: T["getParameters"];
        params?: T["getParameters"];
        clear?: boolean;
    }) => CancellablePromise<void> = flow(function* (this: ClientCollection<any>, data) {
        const { request, params, clear = false } = data || {};
        this.busy = true;
        try {
            const { path, namespace } = this.annotated;
            const response: T["response"] = yield this.annotated.request<
                T["getParameters"],
                T["getParameters"],
                T["response"]
            >({
                location: {
                    path,
                    method: ERouteHttpVerb.GET,
                    namespace,
                },
                request,
                params,
            });

            // Save cookies as models
            clear && this.entries.clear();
            for (const row of response) {
                const instance = this.instance(row) as T["model"];
                const existing = this.entries.get(instance.key);

                if (!existing) {
                    this.entries.set(instance.key, instance);
                } else {
                    // Update stale data
                    existing.data = instance.data;
                }
            }
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busy = false;
        }
    });

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

        const { request, params } = data || {};
        this.busy = true;
        try {
            const { singlePath, namespace } = this.annotated;
            const row: T["response"] = yield this.annotated.request<
                T["getSingleParameters"],
                T["getSingleParameters"],
                T["response"]
            >({
                location: {
                    path: singlePath,
                    method: ERouteHttpVerb.GET,
                    namespace,
                },
                request,
                params,
            });

            // Save cookie as model
            const instance = this.instance(row) as T["model"];
            this.entries.set(instance.key, instance);
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busy = false;
        }
    });

    public abstract instance(response: T["response"]): T["model"];
}

export { type AnnotateDefinition, type CollectionDefinition, ClientCollection };
