import { AptosConfig } from "../api/aptosConfig.js";
import { aptosRequest } from "./core.js";
import { AptosResponse, AnyNumber, ClientConfig, MimeType } from "../types/index.js";
import { AptosApiType } from "../utils/const.js";

/**
 * Options for making a GET request, including configuration for the API client.
 * @group Implementation
 * @category Client
 */
export type GetRequestOptions = {
  /**
   * The config for the API client
   * @group Implementation
   * @category Client
   */
  aptosConfig: AptosConfig;
  /**
   * The type of API endpoint to call e.g. fullnode, indexer, etc
   * @group Implementation
   * @category Client
   */
  type: AptosApiType;
  /**
   * The name of the API method
   * @group Implementation
   * @category Client
   */
  originMethod: string;
  /**
   * The URL path to the API method
   * @group Implementation
   * @category Client
   */
  path: string;
  /**
   * The content type of the request body
   * @group Implementation
   * @category Client
   */
  contentType?: MimeType;
  /**
   * The accepted content type of the response of the API
   * @group Implementation
   * @category Client
   */
  acceptType?: MimeType;
  /**
   * The query parameters for the request
   * @group Implementation
   * @category Client
   */
  params?: Record<string, string | AnyNumber | boolean | undefined>;
  /**
   * Specific client overrides for this request to override aptosConfig
   * @group Implementation
   * @category Client
   */
  overrides?: ClientConfig;
};

/**
 * Options for making a request to the Aptos API, excluding the "type" field.
 * @group Implementation
 * @category Client
 */
export type GetAptosRequestOptions = Omit<GetRequestOptions, "type">;

/**
 * Executes a GET request to retrieve data based on the provided options.
 *
 * @param options - The options for the GET request.
 * @param options.aptosConfig - The configuration object for Aptos requests.
 * @param options.overrides - Optional overrides for the request configuration.
 * @param options.params - Query parameters to include in the request.
 * @param options.contentType - The content type of the request.
 * @param options.acceptType - The accepted response type.
 * @param options.path - The specific path for the request.
 * @param options.originMethod - The original method of the request.
 * @param options.type - The type of request being made.
 * @returns The response from the GET request.
 * @group Implementation
 * @category Client
 */
export async function get<Req extends {}, Res extends {}>(
  options: GetRequestOptions,
): Promise<AptosResponse<Req, Res>> {
  const { aptosConfig, overrides, params, contentType, acceptType, path, originMethod, type } = options;
  const url = aptosConfig.getRequestUrl(type);

  return aptosRequest<Req, Res>(
    {
      url,
      method: "GET",
      originMethod,
      path,
      contentType,
      acceptType,
      params,
      overrides: {
        ...aptosConfig.clientConfig,
        ...overrides,
      },
    },
    aptosConfig,
    options.type,
  );
}

/**
 * Retrieves data from the Aptos full node using the provided options.
 *
 * @param options - The options for the request to the Aptos full node.
 * @param options.aptosConfig - Configuration settings specific to the Aptos client and full node.
 * @param options.aptosConfig.clientConfig - The client configuration settings.
 * @param options.aptosConfig.fullnodeConfig - The full node configuration settings.
 * @param options.overrides - Additional overrides for the request.
 * @param options.type - The type of API request being made.
 *
 * @returns A promise that resolves with the response from the Aptos full node.
 * @group Implementation
 * @category Client
 */
export async function getAptosFullNode<Req extends {}, Res extends {}>(
  options: GetAptosRequestOptions,
): Promise<AptosResponse<Req, Res>> {
  const { aptosConfig } = options;

  return get<Req, Res>({
    ...options,
    type: AptosApiType.FULLNODE,
    overrides: {
      ...aptosConfig.clientConfig,
      ...aptosConfig.fullnodeConfig,
      ...options.overrides,
      HEADERS: { ...aptosConfig.clientConfig?.HEADERS, ...aptosConfig.fullnodeConfig?.HEADERS },
    },
  });
}

/**
 * Makes a GET request to the Aptos Pepper service to retrieve data.
 *
 * @param options - The options for the request.
 * @param options.param1 - Description of param1.
 * @param options.param2 - Description of param2.
 * @returns AptosResponse - The response from the Aptos Pepper service.
 * @group Implementation
 * @category Client
 */
export async function getAptosPepperService<Req extends {}, Res extends {}>(
  options: GetAptosRequestOptions,
): Promise<AptosResponse<Req, Res>> {
  return get<Req, Res>({ ...options, type: AptosApiType.PEPPER });
}

/**
 * This function is a helper for paginating using a function wrapping an API
 * @group Implementation
 * @category Client
 */
export async function paginateWithCursor<Req extends Record<string, any>, Res extends Array<{}>>(
  options: GetAptosRequestOptions,
): Promise<Res> {
  let out: Res = [] as unknown as Res;
  let cursor: string | undefined;
  const requestParams = options.params as { start?: string; limit?: number };
  do {
    const response = await getAptosFullNode<Req, Res>({
      aptosConfig: options.aptosConfig,
      originMethod: options.originMethod,
      path: options.path,
      params: requestParams,
      overrides: options.overrides,
    });
    /**
     * the cursor is a "state key" from the API perspective. Client
     * should not need to "care" what it represents but just use it
     * to query the next chunk of data.
     * @group Implementation
     * @category Client
     */
    cursor = response.headers["x-aptos-cursor"];
    // Now that we have the cursor (if any), we remove the headers before
    // adding these to the output of this function.
    delete response.headers;
    // Use concat instead of push(...) for better performance with large arrays
    out = out.concat(response.data) as Res;
    requestParams.start = cursor;
  } while (cursor !== null && cursor !== undefined);
  return out;
}

/// This function is a helper for paginating using a function wrapping an API using offset instead of start
export async function paginateWithObfuscatedCursor<Req extends Record<string, any>, Res extends Array<{}>>(
  options: GetAptosRequestOptions,
): Promise<Res> {
  let out: Res = [] as unknown as Res;
  let cursor: string | undefined;
  const requestParams = options.params as { start?: string; limit?: number };
  const totalLimit = requestParams.limit;
  do {
    const { response, cursor: newCursor } = await getPageWithObfuscatedCursor<Req, Res>({ ...options });

    /**
     * the cursor is a "state key" from the API perspective. Client
     * should not need to "care" what it represents but just use it
     * to query the next chunk of data.
     */
    cursor = newCursor;
    // Use concat instead of push(...) for better performance with large arrays
    out = out.concat(response.data) as Res;
    if (options?.params) {
      options.params.start = cursor;
    }

    // Re-evaluate length
    if (totalLimit !== undefined) {
      const newLimit = totalLimit - out.length;
      if (newLimit <= 0) {
        break;
      }
      requestParams.limit = newLimit;
    }
  } while (cursor !== null && cursor !== undefined);
  return out;
}

export async function getPageWithObfuscatedCursor<Req extends Record<string, any>, Res extends Array<{}>>(
  options: GetAptosRequestOptions,
): Promise<{ response: AptosResponse<Req, Res>; cursor: string | undefined }> {
  const requestParams: { start?: string; limit?: number } = {};

  // Drop any other values
  // TODO: Throw error if cursor is not a string
  if (typeof options.params?.cursor === "string") {
    requestParams.start = options.params.cursor;
  }
  if (typeof options.params?.limit === "number") {
    requestParams.limit = options.params.limit;
  }

  const response = await getAptosFullNode<Req, Res>({
    aptosConfig: options.aptosConfig,
    originMethod: options.originMethod,
    path: options.path,
    params: requestParams,
    overrides: options.overrides,
  });

  /**
   * the cursor is a "state key" from the API perspective. Client
   * should not need to "care" what it represents but just use it
   * to query the next chunk of data.
   */
  const cursor = response.headers["x-aptos-cursor"] ?? undefined;
  return { response, cursor };
}
