/* eslint-disable class-methods-use-this */
import type { EmptyTypes, ExtendRequest } from "types";
import { getAdapterOnError, getAdapterBindings } from "./adapter.bindings";
import type {
  EndpointMapper,
  AdapterFetcherType,
  AdapterPayloadMappingType,
  HeaderMappingType,
  QueryParamsMapper,
  QueryParamsType,
  RequestResponseType,
  ResponseType,
} from "./adapter.types";
import type { RequestInstance, RequestOptionsType } from "request";
import type { Client, ClientInstance } from "client";
import { mocker } from "mocker";
import type { LoggerMethods } from "managers";
import type { getErrorMessage } from "./adapter.utils";
import { getAdapterHeaders, getAdapterPayload, RequestProcessingError } from "./adapter.utils";

export type DefaultMapperType = <V, C>(value: V, config: C) => V;
export const defaultMapper: DefaultMapperType = (value) => value;

/**
 * Base adapter class responsible for executing HTTP requests. Adapters handle endpoint resolution,
 * header/payload/query-params mapping, and the actual fetch call. Extend or configure this class
 * to integrate custom transport layers (e.g., axios, graphql, firebase).
 */
export class Adapter<
  AdapterOptions,
  MethodType extends string,
  StatusType extends number | string,
  Extra extends Record<string, any>,
  QueryParams = QueryParamsType | string | EmptyTypes,
  DefaultQueryParams = undefined,
  EndpointType = string,
  EndpointMapperType extends EndpointMapper<EndpointType> | DefaultMapperType = DefaultMapperType,
  QueryParamsMapperType extends QueryParamsMapper<QueryParams> | DefaultMapperType = DefaultMapperType,
  HeaderMapperType extends HeaderMappingType | DefaultMapperType = DefaultMapperType,
  PayloadMapperType extends AdapterPayloadMappingType | DefaultMapperType = DefaultMapperType,
> {
  /** Fetching function */
  public unstable_fetcher: AdapterFetcherType<
    Adapter<
      AdapterOptions,
      MethodType,
      StatusType,
      Extra,
      QueryParams,
      DefaultQueryParams,
      EndpointType,
      EndpointMapperType,
      QueryParamsMapperType,
      HeaderMapperType,
      PayloadMapperType
    >
  >;

  /**
   * ********************
   * Defaults
   * ********************
   */

  public name: string;
  public defaultMethod: MethodType;
  public defaultExtra: Extra;
  public systemErrorStatus: StatusType;
  public systemErrorExtra: Extra;
  public defaultRequestOptions?: RequestOptionsType<EndpointType, AdapterOptions, MethodType>;
  public logger: LoggerMethods;
  public initialized = false;
  public client: ClientInstance;

  public unstable_onInitializeCallback?: (options: { client: ClientInstance }) => void;

  public unstable_queryParamsMapperConfig: Parameters<QueryParamsMapperType>[1];
  public unstable_headerMapperConfig: Parameters<HeaderMapperType>[1];
  public unstable_payloadMapperConfig: Parameters<PayloadMapperType>[1];
  public unstable_endpointMapperConfig: Parameters<EndpointMapperType>[1];

  constructor(
    public options: {
      name: string;
      defaultMethod: MethodType;
      defaultExtra: Extra;
      systemErrorStatus: StatusType;
      systemErrorExtra: Extra;
      defaultRequestOptions?: RequestOptionsType<EndpointType, AdapterOptions, MethodType>;
    },
  ) {
    this.name = options.name;
    this.defaultMethod = options.defaultMethod;
    this.defaultExtra = options.defaultExtra;
    this.systemErrorStatus = options.systemErrorStatus;
    this.systemErrorExtra = options.systemErrorExtra;
    this.defaultRequestOptions = options.defaultRequestOptions;
  }

  /** Initialize the adapter with a client reference and set up logging. Called automatically when the client is created. */
  initialize = (client: ClientInstance) => {
    this.logger = client.loggerManager.initialize(client, "Adapter");
    this.initialized = true;
    this.client = client;

    this.unstable_onInitializeCallback?.({ client });
    return this;
  };

  /** Register a callback invoked once the adapter is initialized with a client. */
  onInitialize = (callback: (options: { client: ClientInstance }) => void) => {
    this.unstable_onInitializeCallback = callback;
    return this;
  };

  /**
   * ********************
   * Options Setters
   * ********************
   */

  public unstable_internalErrorMapping: (error: ReturnType<typeof getErrorMessage>) => any = (error) => error;
  /** Method to get default headers and to map them based on the data format exchange, by default it handles FormData / JSON formats. */
  public unstable_headerMapper: HeaderMapperType = getAdapterHeaders as HeaderMapperType;
  /** Method to get request data and transform them to the required format. It handles FormData and JSON by default. */
  public unstable_payloadMapper: PayloadMapperType = getAdapterPayload as PayloadMapperType;
  /** Method to get the endpoint for the adapter request. */
  public unstable_endpointMapper: EndpointMapperType = defaultMapper as EndpointMapperType;
  /** Method to get request data and transform them to the required format.  */
  public unstable_queryParamsMapper: QueryParamsMapperType = defaultMapper as QueryParamsMapperType;
  /** Get default adapter options for the request. */
  public unstable_getAdapterDefaults?: (
    request: ExtendRequest<
      RequestInstance,
      {
        client: Client<
          any,
          Adapter<
            AdapterOptions,
            MethodType,
            StatusType,
            Extra,
            QueryParams,
            DefaultQueryParams,
            EndpointType,
            EndpointMapperType,
            QueryParamsMapperType,
            HeaderMapperType,
            PayloadMapperType
          >
        >;
      }
    >,
  ) => AdapterOptions;
  /** Get default request options for the request. */
  public unstable_getRequestDefaults?: (
    options: RequestOptionsType<EndpointType, AdapterOptions, MethodType>,
  ) => Partial<RequestOptionsType<EndpointType, AdapterOptions, MethodType>>;
  /**
   * Get formatted endpoint name of the request.
   * Helpful in displaying long endpoints like in case of graphql schemas etc.
   */
  public unstable_devtoolsEndpointGetter: (endpoint: string) => string = (endpoint) => endpoint;

  /**
   * ********************
   * Methods
   * ********************
   */

  /** Set the default HTTP method used when a request does not specify one. */
  setDefaultMethod = (method: MethodType) => {
    this.defaultMethod = method;
    return this;
  };

  /** Set the default extra metadata attached to every request response (e.g., headers, status). */
  setDefaultExtra = (extra: Extra) => {
    this.defaultExtra = extra;
    return this;
  };

  /** Set a function that formats the endpoint string for display in devtools. */
  setDevtoolsEndpointGetter = (callback: (endpoint: string) => string) => {
    this.unstable_devtoolsEndpointGetter = callback;
    return this;
  };

  /**
   * This method allows to configure global defaults for the request configuration like method, auth, deduplication etc.
   */
  setRequestDefaults = (callback: typeof this.unstable_getRequestDefaults): this => {
    this.unstable_getRequestDefaults = callback;
    return this;
  };

  /**
   * Set the adapter default options added to every sent request
   */
  setAdapterDefaults = (
    callback: (
      request: ExtendRequest<
        RequestInstance,
        {
          // No need for global error type, because we haven't sent the request yet
          client: Client<
            any,
            Adapter<
              AdapterOptions,
              MethodType,
              StatusType,
              Extra,
              QueryParams,
              DefaultQueryParams,
              EndpointType,
              EndpointMapperType,
              QueryParamsMapperType,
              HeaderMapperType,
              PayloadMapperType
            >
          >;
        }
      >,
    ) => AdapterOptions,
  ): this => {
    this.unstable_getAdapterDefaults = callback;
    return this;
  };

  /** Set a custom mapping for internal adapter errors before they are returned in the response. */
  setInternalErrorMapping = (callback: (error: ReturnType<typeof getErrorMessage>) => any) => {
    this.unstable_internalErrorMapping = callback;
    return this;
  };

  /**
   * Set the custom header mapping function
   */
  setHeaderMapper = <NewMapper extends HeaderMappingType>(headerMapper: NewMapper) => {
    this.unstable_headerMapper = ((req: Parameters<NewMapper>[0]) =>
      headerMapper(req, this.unstable_headerMapperConfig as Parameters<NewMapper>[1])) as unknown as HeaderMapperType;
    return this as unknown as Adapter<
      AdapterOptions,
      MethodType,
      StatusType,
      Extra,
      QueryParams,
      DefaultQueryParams,
      EndpointType,
      EndpointMapperType,
      QueryParamsMapperType,
      NewMapper,
      PayloadMapperType
    >;
  };

  /**
   * Set the request payload mapping function which gets triggered before request is send
   */
  setPayloadMapper = <NewMapper extends AdapterPayloadMappingType>(payloadMapper: NewMapper) => {
    this.unstable_payloadMapper = ((req: Parameters<NewMapper>[0]) =>
      payloadMapper(
        req,
        this.unstable_payloadMapperConfig as Parameters<NewMapper>[1],
      )) as unknown as PayloadMapperType;
    return this as unknown as Adapter<
      AdapterOptions,
      MethodType,
      StatusType,
      Extra,
      QueryParams,
      DefaultQueryParams,
      EndpointType,
      EndpointMapperType,
      QueryParamsMapperType,
      HeaderMapperType,
      NewMapper
    >;
  };

  /**
   * Set the endpoint mapping function which transforms the endpoint string before the request is sent
   */
  setEndpointMapper = <NewEndpointMapper extends EndpointMapper<EndpointType>>(endpointMapper: NewEndpointMapper) => {
    this.unstable_endpointMapper = ((endpoint: Parameters<NewEndpointMapper>[0]) =>
      endpointMapper(
        endpoint,
        this.unstable_endpointMapperConfig as Parameters<NewEndpointMapper>[1],
      )) as unknown as EndpointMapperType;
    return this as unknown as Adapter<
      AdapterOptions,
      MethodType,
      StatusType,
      Extra,
      QueryParams,
      DefaultQueryParams,
      EndpointType,
      NewEndpointMapper,
      QueryParamsMapperType,
      HeaderMapperType,
      PayloadMapperType
    >;
  };

  /**
   * Set the query params mapping function which get triggered before request get sent
   */
  setQueryParamsMapper = <NewQueryParamsMapper extends QueryParamsMapper<QueryParams>>(
    queryParamsMapper: NewQueryParamsMapper,
  ) => {
    this.unstable_queryParamsMapper = ((queryParams: Parameters<NewQueryParamsMapper>[0]) =>
      queryParamsMapper(
        queryParams,
        this.unstable_queryParamsMapperConfig as Parameters<NewQueryParamsMapper>[1],
      )) as unknown as QueryParamsMapperType;
    return this as unknown as Adapter<
      AdapterOptions,
      MethodType,
      StatusType,
      Extra,
      QueryParams,
      DefaultQueryParams,
      EndpointType,
      EndpointMapperType,
      NewQueryParamsMapper,
      HeaderMapperType,
      PayloadMapperType
    >;
  };

  /** Set the configuration object passed as the second argument to the query params mapper. */
  setQueryParamsMapperConfig = <NewQueryParamsMapperConfig extends Parameters<QueryParamsMapperType>[1]>(
    config: NewQueryParamsMapperConfig,
  ) => {
    this.unstable_queryParamsMapperConfig = config;
    return this;
  };

  /** Set the configuration object passed as the second argument to the header mapper. */
  setHeaderMapperConfig = <NewHeaderMapperConfig extends Parameters<HeaderMapperType>[1]>(
    config: NewHeaderMapperConfig,
  ) => {
    this.unstable_headerMapperConfig = config;
    return this;
  };

  /** Set the configuration object passed as the second argument to the endpoint mapper. */
  setEndpointMapperConfig = <NewEndpointMapperConfig extends Parameters<EndpointMapperType>[1]>(
    config: NewEndpointMapperConfig,
  ) => {
    this.unstable_endpointMapperConfig = config;
    return this;
  };

  /** Set the configuration object passed as the second argument to the payload mapper. */
  setPayloadMapperConfig = <NewPayloadMapperConfig extends Parameters<PayloadMapperType>[1]>(
    config: NewPayloadMapperConfig,
  ) => {
    this.unstable_payloadMapperConfig = config;
    return this;
  };

  /**
   * ********************
   * Fetching
   * ********************
   */

  /** Set the fetcher function that performs the actual network request. Must be called before any request can be executed. */
  public setFetcher(
    fetcher: AdapterFetcherType<
      Adapter<
        AdapterOptions,
        MethodType,
        StatusType,
        Extra,
        QueryParams,
        DefaultQueryParams,
        EndpointType,
        EndpointMapperType,
        QueryParamsMapperType,
        HeaderMapperType,
        PayloadMapperType
      >
    >,
  ) {
    this.unstable_fetcher = fetcher.bind(this);
    return this;
  }

  /** Execute a request through the adapter pipeline: maps endpoint/headers/payload, runs plugins, then calls the fetcher. */
  public async fetch(
    request: ExtendRequest<
      RequestInstance,
      {
        // No need for global error type, it doesn't matter to the fetcher itself
        client: Client<
          any,
          Adapter<
            AdapterOptions,
            MethodType,
            StatusType,
            Extra,
            QueryParams,
            DefaultQueryParams,
            EndpointType,
            EndpointMapperType,
            QueryParamsMapperType,
            HeaderMapperType,
            PayloadMapperType
          >
        >;
      }
    >,
    requestId: string,
  ): Promise<RequestResponseType<RequestInstance>> {
    let startTime: number | undefined;

    const execute = async (resolve: (value: ResponseType<any, any, any>) => void) => {
      try {
        if (!this.initialized) {
          throw new RequestProcessingError(`Adapter ${this.options.name} is not initialized`);
        }

        if (!this.unstable_fetcher) {
          throw new RequestProcessingError(`Fetcher for ${this.options.name} adapter is not set`);
        }

        this.client.triggerPlugins("onAdapterFetch", {
          adapter: this,
          request,
          requestId,
        });

        const bindings = await getAdapterBindings<
          Adapter<
            AdapterOptions,
            MethodType,
            StatusType,
            Extra,
            QueryParams,
            DefaultQueryParams,
            EndpointType,
            EndpointMapperType,
            QueryParamsMapperType,
            HeaderMapperType,
            PayloadMapperType
          >
        >({
          request,
          requestId,
          resolve,
          internalErrorMapping: this.unstable_internalErrorMapping,
          onStartTime: (time) => {
            startTime = time;
          },
        });

        if (request.unstable_mock && request.isMockerEnabled && request.client.isMockerEnabled) {
          return await mocker<
            Adapter<
              AdapterOptions,
              MethodType,
              StatusType,
              Extra,
              QueryParams,
              DefaultQueryParams,
              EndpointType,
              EndpointMapperType,
              QueryParamsMapperType,
              HeaderMapperType,
              PayloadMapperType
            >
          >(bindings);
        }

        return this.unstable_fetcher(bindings);
      } catch (error) {
        const onError = getAdapterOnError({
          request,
          requestId,
          startTime: startTime || Date.now(),
          logger: this.logger,
          resolve,
        });

        return onError({
          error: this.unstable_internalErrorMapping(error as ReturnType<typeof getErrorMessage>),
          status: this.options.systemErrorStatus,
          extra: this.options.systemErrorExtra,
        });
      }
    };

    const promise = new Promise((resolve) => {
      execute(resolve);
    });

    return promise as Promise<RequestResponseType<RequestInstance>>;
  }
}
