import type { PublicRuntimeConfig } from 'nuxt/schema';
import type { UnwrapRef } from 'vue';
import type { Enabled, InfiniteData, Query, UseInfiniteQueryOptions, UseQueryOptions } from '@tanstack/vue-query';
import { QueryObserver } from '@tanstack/vue-query';
import { FetchError } from 'ofetch';
import { addKnownErrorConstructor } from 'serialize-error';
import _ from 'lodash';

export class ApiResponseError extends Error {
    public constructor(
        public readonly errorCode?: number,
        public readonly errorInfo?: Record<string, unknown[]> | string,
        public readonly fetchError?: FetchError
    ) {
        super(JSON.stringify({ fetchError, errorCode, errorInfo }));
        this.name = 'ApiResponseError';
    }
}

[FetchError, ApiResponseError].forEach(error => {
    try {
        addKnownErrorConstructor(error, () => (error instanceof FetchError
            ? new FetchError('')
            : new ApiResponseError()));

    // https://github.com/sindresorhus/serialize-error/blob/4d0f3b27d618739c3a3868fd5b4d619b4ea03ad8/error-constructors.js#L31C19-L31C70
    // eslint-disable-next-line no-empty
    } catch {}
});

// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export const isApiError = (response: ApiError | unknown): response is ApiError => _.isObject(response)
    && 'errorCode' in response && _.isNumber(response.errorCode)
    && 'errorInfo' in response && (_.isObject(response.errorInfo) || _.isString(response.errorInfo));
export const queryFunction = async <TResponse>
(
    config: PublicRuntimeConfig,
    requestHeaders: Record<string, string>,
    endpoint: string,
    queryParam?: ObjUnknown,
    signal?: AbortSignal
): Promise<TResponse> => {
    const globalLoadingStore = useGlobalLoadingStore();
    if (import.meta.client) {
        globalLoadingStore.start();
        document.body.style.cursor = 'progress';
    }
    try {
        return await $fetch<TResponse>(
            `${config.beUrl}/api/${endpoint}`,
            {
                query: queryParam,
                headers: {
                    Accept: 'application/json',
                    ...requestHeaders
                },
                signal,
                retry: 0
            }
        ) as TResponse;
    } catch (e: unknown) {
        if (e instanceof FetchError && isApiError(e.data))
            throw new ApiResponseError(e.data.errorCode, e.data.errorInfo, e);
        throw e;
    } finally {
        if (import.meta.client) {
            globalLoadingStore.stop();
            document.body.style.cursor = '';
        }
    }
};

export type ApiErrorClass = ApiResponseError | FetchError;
const useApi = <
    TApi extends Api<TResponse, TQueryParam>,
    TResponse = TApi['response'],
    TQueryParam extends ObjUnknown = TApi['queryParam']>
(endpoint: string) =>
    (
        queryParam?: Ref<TQueryParam | undefined>,
        options?: MaybeRef<Partial<Exclude<UseQueryOptions<TResponse, ApiErrorClass>, Ref>>>
    ) => {
        const config = useRuntimeConfig().public;
        const clientRequestHeaders = useRequestHeaders(['Authorization']);
        const ret = useQuery<TResponse, ApiErrorClass>({
            queryKey: [endpoint, queryParam],
            queryFn: async () => queryFunction<TResponse>(
                config,
                clientRequestHeaders,
                endpoint,
                queryParam?.value
            ),
            ...options
        });
        onServerPrefetch(async () => { // https://github.com/TanStack/query/issues/7609
            const enabled = unref(options)?.enabled;
            if (_.isFunction(enabled)
                ? (enabled as Exclude<Enabled<TResponse, ApiErrorClass>, boolean>)(
                    new QueryObserver<TResponse, ApiErrorClass>(
                        useQueryClient(),
                        { queryKey: [endpoint, queryParam?.value] }
                    ).getCurrentQuery() as unknown as Query<TResponse, ApiErrorClass>
                )
                : unref(enabled) ?? true)
                await ret.suspense();
        });

        return ret;
    };
const useApiWithCursor = <
    TApi extends Api<TResponse, TQueryParam>,
    TResponse extends CursorPagination = TApi['response'],
    TQueryParam extends ObjUnknown = TApi['queryParam']>
(endpoint: string) => {
    type Data = InfiniteData<TResponse, Cursor>;
    type QueryKey = [string, TQueryParam | undefined];
    type QueryOptions = UseInfiniteQueryOptions<TResponse, ApiErrorClass, Data, QueryKey, Cursor>;
    type PartialMaybeRef<TMaybeRef> = TMaybeRef extends ComputedRef
        ? ComputedRef<Partial<UnwrapRef<TMaybeRef>>>
        : TMaybeRef extends Ref
            ? Ref<Partial<UnwrapRef<TMaybeRef>>>
            : Partial<TMaybeRef>;

    return (
        queryParam?: Ref<TQueryParam | undefined>,
        options?: PartialMaybeRef<QueryOptions>
    ) => {
        const config = useRuntimeConfig().public;
        const clientRequestHeaders = useRequestHeaders(['Authorization']);
        type TQueryParamWithCursor = TQueryParam & { cursor?: Cursor };
        type UseInfiniteQueryOptions = Parameters<typeof useInfiniteQuery<TResponse, ApiErrorClass, Data, QueryKey, Cursor>>[0];
        const ret = useInfiniteQuery<TResponse, ApiErrorClass, Data, QueryKey, Cursor>({
            queryKey: [endpoint, queryParam],
            queryFn: async ({ pageParam }) =>
                queryFunction<TResponse>(
                    config,
                    clientRequestHeaders,
                    endpoint,
                    { ...queryParam?.value, cursor: undefinedWhenEmpty(pageParam) } as TQueryParamWithCursor
                ),
            getNextPageParam: lastPage => lastPage.pages.nextCursor,
            initialPageParam: '',
            ...options
        } as UseInfiniteQueryOptions);
        onServerPrefetch(async () => { // https://github.com/TanStack/query/issues/7609
            const enabled = unref(options)?.enabled;
            if ((_.isFunction(enabled)
                ? (enabled as Exclude<Enabled<TResponse, ApiErrorClass, Data, QueryKey>, boolean>)(
                    new QueryObserver<TResponse, ApiErrorClass, Data, TResponse, QueryKey>(
                        useQueryClient(),
                        { queryKey: [endpoint, queryParam?.value] }
                    ).getCurrentQuery() as unknown as Query<TResponse, ApiErrorClass, Data, QueryKey>
                )
                : unref(enabled) ?? true
            ) === true)
                await ret.suspense();
        });

        return ret;
    };
};
export const useApiForums = useApi<ApiForums>('forums');
export const useApiUsers = useApi<ApiUsers>('users');
export const apiPostsEndpoint = 'posts';
export const useApiPosts = useApiWithCursor<ApiPosts>(apiPostsEndpoint);
