import { PaginationShape } from "./types/PaginationShape";

export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

export interface BackendApiEndpoint<
    M extends Method,
    PP extends string[],
    T,
    R,
    PG extends boolean
> {
    method: M;
    endpoint: string;
    pathParams: PP;
    request?: T;
    response?: R & (PG extends true ? any[] : any);
    paginated: PG;
}

function nestedUrlSearchParams(data: any, topLevel: boolean = true): string {
    switch (typeof data) {
        case "string":
        case "bigint":
        case "boolean":
        case "number":
        case "symbol":
            if (topLevel) return encodeURIComponent(data.toString());

            return data.toString();
        case "object":
            if (data === null) return "null";

            if (Array.isArray(data))
                return data
                    .map((d) => nestedUrlSearchParams(d, true))
                    .map(encodeURIComponent)
                    .join("&");

            const inner = Object.entries(data)
                .filter(([_, v]) => typeof v !== "undefined")
                .map(([k, v]) => [k, nestedUrlSearchParams(v, false)]);

            return new URLSearchParams(inner).toString();

        default:
            return "";
    }
}

export type MagApiErrorCode = "Client:GenericApiError" | string;

export interface MagApiError {
    status: number;
    code: MagApiErrorCode;
    message: string;
}

export interface PaginatedResult<T> {
    prev?: URL;
    data: T & any[];
    next?: URL;
}

function extractHeaderRel(
    headers: Headers,
    rel: "prev" | "next"
): URL | undefined {
    for (const [k, v] of headers) {
        if (k.toLowerCase() !== "link") {
            continue;
        }

        for (const links of v.split(",").map((s) => s.trim())) {
            const [url, relPar] = links.split(";").map((s) => s.trim());
            if (!url || !relPar) {
                continue;
            }

            const urlMatch = url.match(/^<(.+?)>$/)?.[1];
            const relMatch = relPar.match(/rel="(.+?)"/)?.[1];

            if (relMatch == rel && urlMatch) {
                try {
                    return new URL(urlMatch);
                } catch (e) {
                    console.error(e);
                    return undefined;
                }
            }
        }
    }

    return undefined;
}

export class MagApiClient {
    private readonly baseUrl: string;

    constructor(baseUrl: string) {
        this.baseUrl = baseUrl;
    }

    async call<
        T extends BackendApiEndpoint<
            T["method"] & Method,
            T["pathParams"] & string[],
            T["request"],
            T["response"],
            T["paginated"] & boolean
        >
    >(
        { endpoint, method, paginated }: T,
        data: T["request"] &
            (T["paginated"] extends true ? Partial<PaginationShape> : any),
        pathParams: {
            [K in keyof T["pathParams"] as T["pathParams"][K] & string]:
                | string
                | number;
        },
        token?: string | null | undefined
    ): Promise<
        T["paginated"] extends true
            ? PaginatedResult<T["response"]>
            : T["response"]
    > {
        type Response = T["response"];

        const authorizationToken = token ?? undefined;
        const authorization = authorizationToken
            ? `Bearer ${authorizationToken}`
            : undefined;

        for (const name in pathParams) {
            endpoint = endpoint.replace(`:${name}`, `${pathParams[name]}`);
        }

        const baseUrl = this.baseUrl.replace(/\/+$/, "");
        let url = `${baseUrl}${endpoint}`;

        if (method === "GET") {
            const query = nestedUrlSearchParams(data as any);
            if (query) {
                url += `?${query}`;
            }
        }

        return (await fetch(url, {
            method,
            body: method !== "GET" ? JSON.stringify(data) : undefined,
            credentials: "omit",
            cache: "no-cache",
            headers: authorization ? { authorization } : {},
        })
            .then(async (res) => {
                const body = res.status === 204 ? null : await res.json();

                if (res.status === 200) {
                    if (paginated) {
                        return {
                            prev: extractHeaderRel(res.headers, "prev"),
                            data: body as Response,
                            next: extractHeaderRel(res.headers, "next"),
                        };
                    } else {
                        return body as Response;
                    }
                } else if (res.status === 204) {
                    if (paginated) {
                        return {
                            prev: extractHeaderRel(res.headers, "prev"),
                            data: null as any as Response,
                            next: extractHeaderRel(res.headers, "next"),
                        };
                    } else {
                        return null as any as Response;
                    }
                } else {
                    throw body as MagApiError;
                }
            })
            .catch((e) => {
                console.error(
                    "Generic API error. This should not usually happen!",
                    e
                );
                throw {
                    status: -1,
                    code: "Client:GenericApiError",
                    message: e,
                } as MagApiError;
            })) as T["paginated"] extends true
            ? PaginatedResult<Response>
            : Response;
    }
}
