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

import { ScannerResultExternalUrl } from "../models/scannerResultExternalUrl.js";
import { ScannerResultExternalUrlSingle } from "../models/scannerResultExternalUrlSingle.js";
import { ScannerResultTemplate } from "../models/scannerResultTemplate.js";
import { request } from "../utils/request.js";
import { locationRestScannerQueuePost } from "../wp-api/scannerQueue.post.js";
import { locationRestScannerResultAllExternalUrlsByHostGet } from "../wp-api/scannerResultsAllExternalUrls.get.js";
import { locationRestScannerResultExternalsGet } from "../wp-api/scannerResultsExternal.get.js";
import { locationRestScannerResultMarkupGet } from "../wp-api/scannerResultsMarkup.get.js";
import { locationRestScannerResultTemplatesGet } from "../wp-api/scannerResultsTemplate.get.js";

import type { RootStore } from "./stores.js";
import type {
    ParamsRouteScannerQueuePost,
    RequestRouteScannerQueuePost,
    ResponseRouteScannerQueuePost,
} from "../wp-api/scannerQueue.post.js";
import type {
    ParamsRouteScannerResultAllExternalUrls,
    RequestRouteScannerResultAllExternalUrls,
    ResponseRouteScannerResultAllExternalUrls,
} from "../wp-api/scannerResultsAllExternalUrls.get.js";
import type {
    ParamsRouteScannerResultExternalsGet,
    RequestRouteScannerResultExternalsGet,
    ResponseRouteScannerResultExternalsGet,
} from "../wp-api/scannerResultsExternal.get.js";
import type {
    ParamsRouteScannerResultMarkupGet,
    RequestRouteScannerResultMarkupGet,
    ResponseRouteScannerResultMarkupGet,
} from "../wp-api/scannerResultsMarkup.get.js";
import type {
    ParamsRouteScannerResultTemplatesGet,
    RequestRouteScannerResultTemplatesGet,
    ResponseRouteScannerResultTemplatesGet,
} from "../wp-api/scannerResultsTemplate.get.js";

class ScannerStore {
    @observable
    public resultTemplates = new Map<string, ScannerResultTemplate>();

    @observable
    public busyResultTemplates = false;

    @observable
    public fetchedAllResultTemplates = false;

    @observable
    public resultExternalUrls = new Map<string, ScannerResultExternalUrl>();

    @observable
    public resultAllExternalUrls = new Map<
        /**
         * Can be the host or `template`.
         */
        string,
        Map<number, ScannerResultExternalUrlSingle>
    >();

    @observable
    public busyExternalUrls = false;

    @observable
    public fetchedAllResultExternalUrls = false;

    @observable
    public busyMarkup = false;

    @observable
    public resultMarkup = new Map<number, ResponseRouteScannerResultMarkupGet>();

    @computed
    public get sortedTemplates() {
        const result = Array.from(this.resultTemplates.values());

        // Move ignored items to the bottom list
        result.sort((a, b) => {
            // MobX observability: `values()` does not check if `isIgnored` got updated, so we
            // need to trigger a read statement
            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
            a.data.consumerData.isIgnored;
            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
            b.data.consumerData.isIgnored;

            return a.data.headline.localeCompare(b.data.headline);
        });
        return result;
    }

    @computed
    public get sortedExternalUrls() {
        const result = Array.from(this.resultExternalUrls.values());

        // Move inactive items to the bottom list
        result.sort((a, b) => (a.inactive === b.inactive ? 0 : a.inactive ? 1 : -1));
        return result;
    }

    @computed
    public get templatesCount() {
        return this.fetchedAllResultTemplates
            ? this.resultTemplates.size
            : this.rootStore.optionStore.allScannerResultTemplatesCount;
    }

    @computed
    public get externalUrlsCount() {
        return this.fetchedAllResultExternalUrls
            ? this.resultExternalUrls.size
            : this.rootStore.optionStore.allScannerResultExternalUrlsCount;
    }

    @computed
    public get canShowResults() {
        return (
            this.templatesCount + this.externalUrlsCount > 0 &&
            this.rootStore.checklistStore.checklist?.items["scanner"].checked &&
            this.rootStore.optionStore.others.isLicensed
        );
    }

    @computed
    public get foundScanResultsCount() {
        return this.resultTemplates.size + this.resultExternalUrls.size;
    }

    /**
     * Count of templates and external URL hosts which still needs attention.
     */
    @computed
    public get needsAttentionCount() {
        return [...this.resultTemplates.values(), ...this.resultExternalUrls.values()].filter(
            ({ inactive }) => !inactive,
        ).length;
    }

    public readonly rootStore: RootStore;

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

    public addUrlsToQueue: (data: RequestRouteScannerQueuePost) => Promise<ResponseRouteScannerQueuePost> = flow(
        function* (this: ScannerStore, data) {
            return yield request<
                RequestRouteScannerQueuePost,
                ParamsRouteScannerQueuePost,
                ResponseRouteScannerQueuePost
            >({
                location: locationRestScannerQueuePost,
                request: data,
            });
        },
    );

    public fetchResultTemplates: () => Promise<void> = flow(function* (this: ScannerStore) {
        this.busyResultTemplates = true;
        try {
            this.resultTemplatesFromResponse(
                yield request<
                    RequestRouteScannerResultTemplatesGet,
                    ParamsRouteScannerResultTemplatesGet,
                    ResponseRouteScannerResultTemplatesGet
                >({
                    location: locationRestScannerResultTemplatesGet,
                }),
            );

            this.fetchedAllResultTemplates = true;
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busyResultTemplates = false;
        }
    });

    @action
    public resultTemplatesFromResponse({ items }: ResponseRouteScannerResultTemplatesGet) {
        // As we load all data within one request, we can safely remove stale objects
        const existing = Object.keys(items);
        for (const probablyStaleKey of this.resultTemplates.keys()) {
            if (existing.indexOf(probablyStaleKey) === -1) {
                this.resultTemplates.delete(probablyStaleKey);
            }
        }

        // Save template as object
        for (const pid of existing) {
            this.resultTemplates.set(pid, new ScannerResultTemplate(items[pid], this));
        }
    }

    public fetchResultExternals: () => Promise<void> = flow(function* (this: ScannerStore) {
        this.busyExternalUrls = true;
        try {
            this.resultExternalUrlsFromResponse(
                yield request<
                    RequestRouteScannerResultExternalsGet,
                    ParamsRouteScannerResultExternalsGet,
                    ResponseRouteScannerResultExternalsGet
                >({
                    location: locationRestScannerResultExternalsGet,
                }),
            );

            this.fetchedAllResultExternalUrls = true;
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busyExternalUrls = false;
        }
    });

    @action
    public resultExternalUrlsFromResponse({ items }: ResponseRouteScannerResultExternalsGet) {
        // As we load all data within one request, we can safely remove stale objects
        const existing = Object.keys(items);
        for (const probablyStaleKey of this.resultExternalUrls.keys()) {
            if (existing.indexOf(probablyStaleKey) === -1) {
                this.resultExternalUrls.delete(probablyStaleKey);
            }
        }

        // Save templates as objects
        for (const externalHost of existing) {
            const obj = this.resultExternalUrls.get(externalHost);

            if (obj) {
                set(obj, { data: items[externalHost] });
            } else {
                this.resultExternalUrls.set(externalHost, new ScannerResultExternalUrl(items[externalHost], this));
            }
        }
    }

    public fetchResultAllExternals: (instance: ScannerResultExternalUrl | ScannerResultTemplate) => Promise<void> =
        flow(function* (this: ScannerStore, instance) {
            const type = instance instanceof ScannerResultExternalUrl ? "host" : "template";
            const { identifier } = instance;
            instance.busy = true;
            try {
                const { items }: ResponseRouteScannerResultAllExternalUrls = yield request<
                    RequestRouteScannerResultAllExternalUrls,
                    ParamsRouteScannerResultAllExternalUrls,
                    ResponseRouteScannerResultAllExternalUrls
                >({
                    location: locationRestScannerResultAllExternalUrlsByHostGet,
                    params: {
                        type,
                        identifier: type === "host" ? identifier.replace(/\./g, "_") : identifier,
                    },
                });

                // Check if map for this host already exists
                let map = this.resultAllExternalUrls.get(identifier);
                if (!map) {
                    map = new Map();
                } else {
                    // As we load all data within one request, we can safely remove stale objects
                    const existing = items.map(({ id }) => id);
                    for (const probablyStaleKey of map.keys()) {
                        if (existing.indexOf(probablyStaleKey) === -1) {
                            map.delete(probablyStaleKey);
                        }
                    }
                }

                // Save templates as objects
                for (const item of Object.values(items)) {
                    map.set(item.id, new ScannerResultExternalUrlSingle(item, this));

                    this.resultAllExternalUrls.set(identifier, map);
                }
            } catch (e) {
                console.log(e);
                throw e;
            } finally {
                instance.busy = false;
            }
        });

    public fetchMarkup: (id: number) => Promise<void> = flow(function* (this: ScannerStore, id) {
        this.busyMarkup = true;
        try {
            const result: ResponseRouteScannerResultMarkupGet = yield request<
                RequestRouteScannerResultMarkupGet,
                ParamsRouteScannerResultMarkupGet,
                ResponseRouteScannerResultMarkupGet
            >({
                location: locationRestScannerResultMarkupGet,
                params: {
                    id,
                },
            });

            this.resultMarkup.set(id, result);
        } catch (e) {
            console.log(e);
            throw e;
        } finally {
            this.busyMarkup = false;
        }
    });
}

export { ScannerStore };
