import { config } from "./config";

export class ApiError extends Error {
    constructor(
        message: string,
        public readonly status: number,
        public readonly code?: string,
        /** Field name to message, when the server names the fields that are wrong. */
        public readonly fields: Record<string, string> = {},
    ) {
        super(message);
        this.name = "ApiError";
    }
}

export interface Paged<T> {
    items: T[];
    total: number;
}

/**
 * With plain permalinks the REST root is ".../?rest_route=/fxtr/v1/", so the
 * path's own query string has to join it with "&" instead of a second "?".
 */
export function buildUrl(restUrl: string, path: string): string {
    let url = restUrl.replace(/\/$/, "") + "/" + path.replace(/^\//, "");
    const query = url.indexOf("?");
    if (query !== -1) {
        url = url.slice(0, query + 1) + url.slice(query + 1).replace(/\?/g, "&");
    }

    return url;
}

/** Drops empty values, so a cleared filter leaves the URL instead of sending "". */
export function queryString(params: Record<string, string | number | undefined>): string {
    const search = new URLSearchParams();
    for (const [key, value] of Object.entries(params)) {
        if (value !== undefined && value !== "" && value !== 0) {
            search.set(key, String(value));
        }
    }
    const out = search.toString();

    return out ? `?${out}` : "";
}

async function send(restUrl: string, method: string, path: string, body?: unknown): Promise<Response> {
    return fetch(buildUrl(restUrl, path), {
        method,
        credentials: "same-origin",
        headers: { "Content-Type": "application/json", "X-WP-Nonce": config().restNonce },
        body: body === undefined ? undefined : JSON.stringify(body),
    });
}

async function unwrap<T>(response: Response): Promise<T> {
    const data = (await response.json().catch(() => null)) as unknown;
    if (!response.ok) {
        const err = (data ?? {}) as { message?: unknown; code?: unknown; data?: { fields?: unknown } };
        const fields = err.data?.fields;
        // WordPress's fatal-error handler puts HTML in the message.
        throw new ApiError(
            typeof err.message === "string"
                ? err.message
                      .replace(/<[^>]*>/g, " ")
                      .replace(/\s+/g, " ")
                      .trim()
                : response.statusText,
            response.status,
            typeof err.code === "string" ? err.code : undefined,
            fields && typeof fields === "object" && !Array.isArray(fields)
                ? Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, String(value)]))
                : {},
        );
    }

    return data as T;
}

/**
 * Calls under one REST root with the page's nonce. An add-on makes its own
 * for its namespace: client(() => "https://…/wp-json/fxtrp/v1/").
 */
export function client(root: () => string) {
    return {
        get: async <T>(path: string): Promise<T> => unwrap<T>(await send(root(), "GET", path)),
        post: async <T>(path: string, body?: unknown): Promise<T> => unwrap<T>(await send(root(), "POST", path, body)),
        patch: async <T>(path: string, body?: unknown): Promise<T> => unwrap<T>(await send(root(), "PATCH", path, body)),
        put: async <T>(path: string, body?: unknown): Promise<T> => unwrap<T>(await send(root(), "PUT", path, body)),
        del: async <T>(path: string): Promise<T> => unwrap<T>(await send(root(), "DELETE", path)),
        async paged<T>(path: string): Promise<Paged<T>> {
            const response = await send(root(), "GET", path);
            const items = await unwrap<T[]>(response);

            return { items: items ?? [], total: parseInt(response.headers.get("X-WP-Total") ?? "0", 10) };
        },
    };
}

export const api = client(() => config().restUrl);
