import { HTTP_STATUS, mergeHeaders } from "#lib/http";
import { customFetch } from "#lib/fetch";
import { logoutAccount } from "#entities/account";
import { APIV2Error } from "./error-v2";
import {
  IMethod,
  IRequestBody,
  IResponseBodySuccess,
  IResponseBodyError,
  requestBodyType,
  IResponseError,
} from "./types";

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

interface IOptions extends Omit<RequestInit, "headers" | "method"> {
  body?: any;
  headers?: Headers;
  searchParams?: URLSearchParams;
}

/**
 * Generic request for API V2.
 * @param pathSpec
 * @param method
 * @param path
 * A path to the endpoint, relative to the base API path.
 * @param options
 */
export async function apiV2Fetch<ReturnShape = undefined>(
  pathSpec: string,
  method: IMethod,
  path: string,
  options?: IOptions
): Promise<ReturnShape> {
  const searchParams = options?.searchParams;
  // `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,
        method,
        credentials: "same-origin",
      };
    } else {
      const requestBody = {
        type: requestBodyType,
        data: options.body,
      } satisfies IRequestBody;
      const jsonBody = JSON.stringify(requestBody);
      finalOptions = {
        ...options,
        method,
        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) {
    let error: IResponseError | undefined = 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 responseBody: IResponseBodyError = JSON.parse(text);
      error = responseBody.error;
    } catch (error) {}

    let message: string;

    switch (response.status) {
      case HTTP_STATUS.BAD_REQUEST:
      case HTTP_STATUS.UNPROCESSABLE_CONTENT: {
        message = "Failed to fetch from API due to client inputs.";
        break;
      }

      case HTTP_STATUS.UNAUTHORIZED: {
        await logoutAccount(true);
        message = "Failed to fetch from API due to lack of credentials.";
        break;
      }

      case HTTP_STATUS.NOT_FOUND: {
        message = `Failed to fetch from API because path "${response.url} doesn't exist.`;
        break;
      }

      case HTTP_STATUS.SERVICE_UNAVAILABLE: {
        message = "API is in maintenance or not available.";
        break;
      }

      default: {
        message =
          response.status >= 500
            ? "Failed to fetch from API due to server error."
            : "Failed to fetch from API for unknown reasons.";
        break;
      }
    }

    const errorOptions = {
      pathSpec,
      request,
      response,
      error,
    } satisfies ConstructorParameters<typeof APIV2Error>["1"];

    throw new APIV2Error(message, errorOptions);
  }

  const result: IResponseBodySuccess<ReturnShape> = await response.json();

  return result.data;
}
