import Cookie from "js-cookie";

import type {
    IRouteLocationInterface,
    IRouteParamsInterface,
    IRouteRequestInterface,
    IRouteResponseInterface,
} from "@devowl-wp/api";
import { ERouteHttpVerb } from "@devowl-wp/api";

import { obfuscatePath } from "./obfuscatePath.js";
import { trailingslashit, untrailingslashit } from "../../helpers.js";
import { simpleHash } from "../../utils/simpleHash.js";

import type { WithOptional } from "../../helpers.js";
import type { BaseOptions } from "../../options.js";

type RouteLocationInterface = WithOptional<IRouteLocationInterface, "method"> & {
    /**
     * WordPress REST API namespace.
     */
    namespace?: string;
    /**
     * Determines, if the request should be obfuscated when sending the request. This could be helpful
     * to make requests work against ad-blockers.
     */
    obfuscatePath?: Parameters<typeof obfuscatePath>["2"];
};

type RouteRequestInterface = IRouteRequestInterface; // POST-Query in JSON-format
type RouteParamsInterface = IRouteParamsInterface; // Parameters in IRouteLocationInterface#path which gets resolved like "example/:id", items not found in the URL are appended as GET
type RouteResponseInterface = IRouteResponseInterface; // Result in JSON-format

interface UrlBuilderArgs {
    location: RouteLocationInterface;
    params?: RouteParamsInterface;
    /**
     * When your `GET` endpoint depends on a cookie value and you are using a page cache like NGINX together with
     * Redis, you could run into caching issues. Why? NGINX mostly uses the query parameters as cache key and also
     * hits the cache even the cookie values changed. Two possible solutions are either to switch to a non-`GET` verb
     * or using this property. This allows you to define a list of cookie keys which should be sent as `_httpCookieInvalidate`
     * parameter and the cookie value hashed in simple way.
     */
    cookieValueAsParam?: string[];
    options: {
        restRoot: BaseOptions["restRoot"];
        restNamespace: BaseOptions["restNamespace"];
        restNonce?: BaseOptions["restNonce"];
        restQuery?: BaseOptions["restQuery"];
        restRecreateNonceEndpoint?: BaseOptions["restRecreateNonceEndpoint"];
        restPathObfuscateOffset?: BaseOptions["restPathObfuscateOffset"];
    };
}

function applyQueryString(url: URL, query: Record<string, any> | Record<string, any>[], merge?: boolean) {
    const params = merge ? url.searchParams : new URLSearchParams();
    const arr = Array.isArray(query) ? query : [query];
    for (const q of arr) {
        for (const [key, value] of Object.entries(q || {})) {
            params.delete(key);
            if (Array.isArray(value)) {
                value.forEach((v) => {
                    if (v !== null && v !== undefined) {
                        params.append(`${key}[]`, String(v));
                    }
                });
            } else if (value !== null && value !== undefined) {
                params.set(key, String(value));
            }
        }
    }
    url.search = params.toString();
    return url;
}

/**
 * Build an URL for a specific scheme.
 *
 * @param param0
 */
function commonUrlBuilder({
    location,
    params = {},
    nonce = true,
    options,
    cookieValueAsParam,
}: {
    nonce?: boolean;
} & UrlBuilderArgs) {
    const { obfuscatePath: doObfuscatePath } = location;
    const { origin } = window.location;
    const { restPathObfuscateOffset } = options;
    const apiUrl = new URL(options.restRoot, origin);
    const query = Object.fromEntries(apiUrl.searchParams.entries());
    const permalinkPath = query.rest_route || apiUrl.pathname; // Determine path from permalink settings

    // Find parameters from passed location
    const { searchParams: pathSearchParams, pathname: fixedPathName } = new URL(location.path, origin);
    const getParams = Object.fromEntries(pathSearchParams.entries());

    // Find dynamic parameters from URL bindings (like /user/:id)
    const foundParams: string[] = [];
    const path = fixedPathName.replace(/:([A-Za-z0-9-_]+)/g, (match: string, group: string) => {
        foundParams.push(group);
        return (params as any)[group];
    });

    // Find undeclared body params (which are not bind above) and add it to GET query
    for (const checkParam of Object.keys(params)) {
        if (foundParams.indexOf(checkParam) === -1) {
            getParams[checkParam] = (params as any)[checkParam]; // We do not need `encodeURIComponent` as it is supported by `URLSearchParams` already
        }
    }

    // Calculate invalidator for cookie values
    if (cookieValueAsParam) {
        getParams._httpCookieInvalidate = `${simpleHash(JSON.stringify(cookieValueAsParam.map(Cookie.get)))}`;
    }

    // Force protocol from parent location
    apiUrl.protocol = window.location.protocol;

    // Set path depending on permalink settings
    const usePermalinkPath = trailingslashit(permalinkPath); // => `/wp-json`
    let useAfterPermalinkPath = untrailingslashit(location.namespace || options.restNamespace) + path; // => `/real-cookie-banner/v1/consent`

    if (restPathObfuscateOffset && doObfuscatePath) {
        useAfterPermalinkPath = obfuscatePath(restPathObfuscateOffset, useAfterPermalinkPath, doObfuscatePath);
    }

    const usePath = `${usePermalinkPath}${useAfterPermalinkPath}`;
    if (query.rest_route) {
        query.rest_route = usePath;
    } else {
        apiUrl.pathname = usePath; // Set path
    }

    // Append WordPress REST nonce
    if (nonce && options.restNonce) {
        query._wpnonce = options.restNonce;
    }

    // Determine if non-GET verbs should always use `POST`
    applyQueryString(apiUrl, query);
    if (
        ["wp-json/", "rest_route="].filter((s) => apiUrl.toString().indexOf(s) > -1).length > 0 &&
        location.method &&
        location.method !== ERouteHttpVerb.GET
    ) {
        applyQueryString(apiUrl, [{ _method: location.method }], true);
    }

    // Build final search
    applyQueryString(apiUrl, [options.restQuery, getParams], true);
    return apiUrl.toString();
}

export {
    type RouteLocationInterface,
    type RouteRequestInterface,
    type RouteParamsInterface,
    type RouteResponseInterface,
    type UrlBuilderArgs,
    commonUrlBuilder,
    applyQueryString,
};
