import { useUserStore } from "../store/UserStore";
import { useApplicationStore } from "../store/ApplicationStore";
import { ImportMetaExtras } from "../../../import-meta";

interface ApiProgressData {
    loaded: number;
    total: number;
}

type ApiProgressCallback = (progress: ApiProgressData) => void;

interface ApiOptions {
    url: string;
    params?: object;
    method?: string;
    headers?: HeadersInit;
    body?: string | object | FormData | ArrayBuffer | Blob;
    signal?: AbortSignal | null;
    progress?: ApiProgressCallback;
}

export interface ApiResponse {
    status: number;
    message: string;
    data: unknown;
    json?: Record<string, unknown>;
}

const apiDefaultHeaders = {
    Accept: "application/json",
    "Content-Type": "application/json;charset=UTF-8",
};

export const api = {
    timeout: 8000,
    baseUrl: (import.meta as ImportMetaExtras).env.APP_URL_API,

    send: function (options: ApiOptions) {
        return new Promise((resolve, reject) => {
            let url = this.baseUrl + options.url;

            if (options.params) {
                let params = "";

                for (const [key, value] of Object.entries(options.params)) {
                    const placeholder = `{${key}}`;
                    if (url.includes(placeholder)) {
                        url = url.replace(
                            placeholder,
                            encodeURIComponent(value)
                        );
                    } else {
                        params += `&${encodeURIComponent(
                            key
                        )}=${encodeURIComponent(value)}`;
                    }
                }

                url = url.replace(/{(.*?)}/g, "$1");
                if (params.length > 0) {
                    url += (url.includes("?") ? "" : "?") + params.substring(1);
                }
            }

            options.headers = {
                ...apiDefaultHeaders,
                ...(options.headers || {}),
            };

            const userStore = useUserStore();
            if (userStore.id) {
                options.headers["Authorization"] = `Bearer ${userStore.token}`;
            }

            options.method = options.method.toUpperCase() || "GET";

            if (options.body && typeof options.body === "object") {
                if (options.body instanceof FormData) {
                    if (
                        Object.prototype.hasOwnProperty.call(
                            options.headers,
                            "Content-Type"
                        )
                    ) {
                        // remove the "Content-Type" key from the headers object
                        delete options.headers["Content-Type"];
                    }

                    if (options.method != "POST") {
                        options.body.append("_method", options.method);
                        options.method = "POST";
                    }
                } else if (
                    options.body instanceof Blob ||
                    options.body instanceof ArrayBuffer
                ) {
                    // do nothing, let XHR handle these types of bodies without a Content-Type header
                } else {
                    options.body = JSON.stringify(options.body);
                    options.headers["Content-Type"] = "application/json";
                }
            }

            if (
                (options.method == "POST" ||
                    options.method == "PUT" ||
                    options.method == "PATCH") &&
                options.progress
            ) {
                const xhr = new XMLHttpRequest();

                xhr.upload.onprogress = function (event) {
                    if (event.lengthComputable) {
                        options.progress({
                            loaded: event.loaded,
                            total: event.total,
                        });
                    }
                };

                xhr.open(options.method, url);
                for (const header in options.headers) {
                    xhr.setRequestHeader(header, options.headers[header]);
                }
                xhr.send(options.body as XMLHttpRequestBodyInit);

                xhr.onload = function () {
                    const result = {
                        status: xhr.status,
                        statusText: xhr.statusText,
                        url: url,
                        headers: {},
                        data: "",
                    };

                    const headersString = xhr.getAllResponseHeaders();
                    const headersArray = headersString.trim().split("\n");
                    headersArray.forEach((header) => {
                        const [name, value] = header.trim().split(":");
                        result.headers[name] = value.trim();
                    });

                    if (
                        xhr.response &&
                        result.headers["content-type"] == "application/json"
                    ) {
                        try {
                            result.data = JSON.parse(xhr.response);
                        } catch (error) {
                            result.data = xhr.response;
                        }
                    } else {
                        result.data = xhr.response;
                    }

                    if (xhr.status < 300) {
                        resolve(result);
                    } else {
                        useApplicationStore().unavailable = true;
                        reject(result);
                    }
                };
            } else {
                const fetchOptions: RequestInit = {
                    method: options.method.toUpperCase() || "GET",
                    headers: options.headers,
                    signal: options.signal || null,
                };

                if (
                    (typeof options.body == "string" &&
                        options.body.length > 0) ||
                    options.body instanceof FormData
                ) {
                    fetchOptions.body = options.body;
                }

                fetch(url, fetchOptions)
                    .then(async (response) => {
                        let data: string | object = "";
                        if (response.headers.get("content-length") !== "0") {
                            if (
                                response &&
                                response.headers.get("content-type") == null
                            ) {
                                try {
                                    data = response.json
                                        ? await response.json()
                                        : {};
                                } catch (error) {
                                    try {
                                        data = response.text
                                            ? await response.text()
                                            : "";
                                    } catch (error) {
                                        data = "";
                                    }
                                }
                            } else {
                                data =
                                    response && response.json
                                        ? await response.json()
                                        : {};
                            }
                        }

                        const result = {
                            status: response.status,
                            statusText: response.statusText,
                            url: response.url,
                            headers: response.headers,
                            data: data,
                        };

                        if (response.status >= 300) {
                            useApplicationStore().unavailable = true;
                            reject(result);
                        }

                        resolve(result);
                    })
                    .catch((error) => {
                        // Handle any errors thrown during the fetch process
                        const { response, ...rest } = error;
                        reject({
                            ...rest,
                            response: response && response.json(),
                        });
                    });
            }
        });
    },

    get: async function (options: ApiOptions | string): Promise<ApiResponse> {
        let apiOptions = {} as ApiOptions;

        if (typeof options == "string") {
            apiOptions.url = options;
        } else {
            apiOptions = options;
        }

        apiOptions.method = "GET";
        return await this.send(apiOptions);
    },

    post: async function (options: ApiOptions | string): Promise<ApiResponse> {
        let apiOptions = {} as ApiOptions;

        if (typeof options == "string") {
            apiOptions.url = options;
        } else {
            apiOptions = options;
        }

        apiOptions.method = "POST";
        return await this.send(options);
    },

    put: async function (options: ApiOptions | string): Promise<ApiResponse> {
        let apiOptions = {} as ApiOptions;

        if (typeof options == "string") {
            apiOptions.url = options;
        } else {
            apiOptions = options;
        }

        apiOptions.method = "PUT";
        return await this.send(options);
    },

    delete: async function (
        options: ApiOptions | string
    ): Promise<ApiResponse> {
        let apiOptions = {} as ApiOptions;

        if (typeof options == "string") {
            apiOptions.url = options;
        } else {
            apiOptions = options;
        }

        apiOptions.method = "DELETE";
        return await this.send(options);
    },
};

/**
 * Get an api result data as type.
 *
 * @param result The api result object.
 * @param defaultValue The default data to return if no result exists.
 * @returns Data object.
 */
export function getApiResultData<T>(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    result: any,
    defaultValue: T | null = null
): T | null {
    if (!result || !Object.prototype.hasOwnProperty.call(result, "data")) {
        return defaultValue;
    }

    const data = result.data as T;
    return data instanceof Object ? data : defaultValue;
}
