import type { FetchOptions as OfetchOptions, ResponseType as OfetchResponseType } from 'ofetch';
import type { ofetch } from 'ofetch';

import type { Auth } from '../core/auth';
import type {
  ServerSentEventsOptions,
  ServerSentEventsResult,
} from '../core/serverSentEvents';
import type { Client as CoreClient, Config as CoreConfig } from '../core/types';
import type { Middleware } from './utils';

export type ResponseStyle = 'data' | 'fields';

export interface Config<T extends ClientOptions = ClientOptions>
  extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
  /**
   * HTTP(S) agent configuration (Node.js only). Passed through to ofetch.
   */
  agent?: OfetchOptions['agent'];
  /**
   * Base URL for all requests made by this client.
   */
  baseUrl?: T['baseUrl'];
  /**
   * Node-only proxy/agent options.
   */
  dispatcher?: OfetchOptions['dispatcher'];
  /**
   * Fetch API implementation. Used for SSE streaming. You can use this option
   * to provide a custom fetch instance.
   *
   * @default globalThis.fetch
   */
  fetch?: typeof fetch;
  /**
   * Controls the native ofetch behaviour that throws `FetchError` when
   * `response.ok === false`. We default to suppressing it to match the fetch
   * client semantics and let `throwOnError` drive the outcome.
   */
  ignoreResponseError?: OfetchOptions['ignoreResponseError'];
  // No custom fetch option: provide custom instance via `ofetch` instead
  /**
   * Please don't use the Fetch client for Next.js applications. The `next`
   * options won't have any effect.
   *
   * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
   */
  next?: never;
  /**
   * Custom ofetch instance created via `ofetch.create()`. If provided, it will
   * be used for requests instead of the default `ofetch` export.
   */
  ofetch?: typeof ofetch;
  /**
   * ofetch hook called before a request is sent.
   */
  onRequest?: OfetchOptions['onRequest'];
  /**
   * ofetch hook called when a request fails before receiving a response
   * (e.g., network errors or aborted requests).
   */
  onRequestError?: OfetchOptions['onRequestError'];
  /**
   * ofetch hook called after a successful response is received and parsed.
   */
  onResponse?: OfetchOptions['onResponse'];
  /**
   * ofetch hook called when the response indicates an error (non-ok status)
   * or when response parsing fails.
   */
  onResponseError?: OfetchOptions['onResponseError'];
  /**
   * Return the response data parsed in a specified format. By default, `auto`
   * will infer the appropriate method from the `Content-Type` response header.
   * You can override this behavior with any of the {@link Body} methods.
   * Select `stream` if you don't want to parse response data at all.
   *
   * @default 'auto'
   */
  parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text';
  /** Custom response parser (ofetch). */
  parseResponse?: OfetchOptions['parseResponse'];
  /**
   * Should we return only data or multiple fields (data, error, response, etc.)?
   *
   * @default 'fields'
   */
  responseStyle?: ResponseStyle;
  /**
   * ofetch responseType override. If provided, it will be passed directly to
   * ofetch and take precedence over `parseAs`.
   */
  responseType?: OfetchResponseType;
  /**
   * Automatically retry failed requests.
   */
  retry?: OfetchOptions['retry'];
  /**
   * Delay (in ms) between retry attempts.
   */
  retryDelay?: OfetchOptions['retryDelay'];
  /**
   * HTTP status codes that should trigger a retry.
   */
  retryStatusCodes?: OfetchOptions['retryStatusCodes'];
  /**
   * Throw an error instead of returning it in the response?
   *
   * @default false
   */
  throwOnError?: T['throwOnError'];
  /**
   * Abort the request after the given milliseconds.
   */
  timeout?: number;
}

export interface RequestOptions<
  TData = unknown,
  TResponseStyle extends ResponseStyle = 'fields',
  ThrowOnError extends boolean = boolean,
  Url extends string = string,
>
  extends
    Config<{
      responseStyle: TResponseStyle;
      throwOnError: ThrowOnError;
    }>,
    Pick<
      ServerSentEventsOptions<TData>,
      | 'onSseError'
      | 'onSseEvent'
      | 'sseDefaultRetryDelay'
      | 'sseMaxRetryAttempts'
      | 'sseMaxRetryDelay'
    > {
  /**
   * Any body that you want to add to your request.
   *
   * {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
   */
  body?: unknown;
  path?: Record<string, unknown>;
  query?: Record<string, unknown>;
  /**
   * Security mechanism(s) to use for the request.
   */
  security?: ReadonlyArray<Auth>;
  url: Url;
}

export interface ResolvedRequestOptions<
  TResponseStyle extends ResponseStyle = 'fields',
  ThrowOnError extends boolean = boolean,
  Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
  headers: Headers;
  serializedBody?: string;
}

export type RequestResult<
  TData = unknown,
  TError = unknown,
  ThrowOnError extends boolean = boolean,
  TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true
  ? Promise<
      TResponseStyle extends 'data'
        ? TData extends Record<string, unknown>
          ? TData[keyof TData]
          : TData
        : {
            data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
            request: Request;
            response: Response;
          }
    >
  : Promise<
      TResponseStyle extends 'data'
        ? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
        : (
            | {
                data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
                error: undefined;
              }
            | {
                data: undefined;
                error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
              }
          ) & {
            /** request may be undefined, because error may be from building the request object itself */
            request?: Request;
            /** response may be undefined due to a network error where no response object is produced */
            response?: Response;
          }
    >;

export interface ClientOptions {
  baseUrl?: string;
  responseStyle?: ResponseStyle;
  throwOnError?: boolean;
}

type MethodFn = <
  TData = unknown,
  TError = unknown,
  ThrowOnError extends boolean = false,
  TResponseStyle extends ResponseStyle = 'fields',
>(
  options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;

type SseFn = <
  TData = unknown,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  _TError = unknown,
  ThrowOnError extends boolean = false,
  TResponseStyle extends ResponseStyle = 'fields',
>(
  options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData>>;

type RequestFn = <
  TData = unknown,
  TError = unknown,
  ThrowOnError extends boolean = false,
  TResponseStyle extends ResponseStyle = 'fields',
>(
  options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
    Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;

type BuildUrlFn = <
  TData extends {
    body?: unknown;
    path?: Record<string, unknown>;
    query?: Record<string, unknown>;
    url: string;
  },
>(
  options: TData & Options<TData>,
) => string;

export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
  interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
};

/**
 * The `createClientConfig()` function will be called on client initialization
 * and the returned object will become the client's initial configuration.
 *
 * You may want to initialize your client this way instead of calling
 * `setConfig()`. This is useful for example if you're using Next.js
 * to ensure your client always has the correct values.
 */
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
  override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;

export interface TDataShape {
  body?: unknown;
  headers?: unknown;
  path?: unknown;
  query?: unknown;
  url: string;
}

type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;

export type Options<
  TData extends TDataShape = TDataShape,
  ThrowOnError extends boolean = boolean,
  TResponse = unknown,
  TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
  RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
  'body' | 'path' | 'query' | 'url'
> &
  ([TData] extends [never] ? unknown : Omit<TData, 'url'>);
