import { logoutAccount } from "#entities/account";
import { HTTP_STATUS, mergeHeaders } from "#lib/http";
import { customFetch } from "#lib/fetch";
import { APIError } from "#lib/api";

const urlBase = `/api/v1`;
const jsonHeaders = new Headers();
jsonHeaders.append("Content-Type", "application/json");

/**
 * TODO: discriminated union with JSONable body signature
 */
interface IOptions extends Omit<RequestInit, "headers"> {
  method: "GET" | "POST" | "DELETE";
  body?: any;
  headers?: Headers;
}

/**
 * Generic request for Kemono API.
 * @param path
 * A path to the endpoint, realtive to the base API path.
 */
export async function apiFetch<ReturnShape>(
  path: string,
  options: IOptions,
  searchParams?: URLSearchParams
): Promise<ReturnShape> {
  // `URL` constructor requires a full origin
  // to be present in either of arguments
  // but the argument for `fetch()` accepts relative paths just fine
  // so we are doing some gymnastics in order not to depend
  // on browser context (does not exist on server)
  // or an env variable (not needed if the origin is the same).
  const url = new URL(`${urlBase}${path}`, "https://example.com");
  url.search = !searchParams ? "" : String(searchParams);

  url.searchParams.sort();

  const apiPath = `${url.pathname}${
    // `URL.search` param includes `?` even with no params
    // so we include it conditionally
    searchParams?.size !== 0 ? url.search : ""
  }`;

  let finalOptions: RequestInit;
  {
    if (!options.body) {
      finalOptions = {
        ...options,
        credentials: "same-origin",
      };
    } else {
      const jsonBody = JSON.stringify(options.body);
      finalOptions = {
        ...options,
        headers: options.headers
          ? mergeHeaders(options.headers, jsonHeaders)
          : jsonHeaders,
        body: jsonBody,
        credentials: "same-origin",
      };
    }
  }
  const request = new Request(apiPath, finalOptions);
  const response = await customFetch(request);

  if (!response.ok) {
    // server logged the account out
    if (response.status === 401) {
      await logoutAccount(true);

      throw new APIError(
        `Failed to fetch from API due to lack of credentials. Reason: ${response.status} - ${response.statusText}.`,
        { request, response }
      );
    }

    if (response.status === 400 || response.status === 422) {
      let body: string | undefined;
      // doing it this way because response doesn't allow
      // parsing body several times
      // and cloning response is a bit too much
      const text = (await response.text()).trim();

      try {
        const json = JSON.parse(text);
        body = JSON.stringify(json);
      } catch (error) {
        body = text;
      }

      throw new APIError(
        `Failed to fetch from API due to client inputs. Reason: ${
          response.status
        } - ${response.statusText}.${!body ? "" : ` ${body}`}`,
        { request, response }
      );
    }

    if (response.status === 404) {
      let body: string | undefined;
      // doing it this way because response doesn't allow
      // parsing body several times
      // and cloning response is a bit too much
      const text = (await response.text()).trim();

      try {
        const json = JSON.parse(text);
        body = JSON.stringify(json);
      } catch (error) {
        body = text;
      }

      throw new APIError(
        `Failed to fetch from API because path "${
          response.url
        }" doesn't exist. Reason: ${response.status} - ${response.statusText}.${
          !body ? "" : ` ${body}`
        }`,
        { request, response }
      );
    }

    if (response.status === HTTP_STATUS.SERVICE_UNAVAILABLE) {
      throw new APIError("API is in maintenance or not available.", {
        request,
        response,
      });
    }

    if (response.status >= 500) {
      throw new APIError("Failed to fetch from API due to server error.", {
        request,
        response,
      });
    }

    throw new APIError("Failed to fetch from API for unknown reasons.", {
      request,
      response,
    });
  }

  const resultBody: ReturnShape = await response.json();

  return resultBody;
}