import { type BatchOptions, createFetcher } from '../fetcher';
import { type LinkedType } from '../types';
import { generateGraphqlOperation, type GraphqlOperation, ParseRequestOptions } from './generateGraphqlOperation';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import {
  type Client as WSClient,
  type ClientOptions as WSClientOptions,
  createClient as createWSClient,
} from 'graphql-ws';
import { Observable } from 'zen-observable-ts';

type HeaderValue = string | string[] | number | boolean | null;
type RawHeaders = Record<string, HeaderValue>;

export type Headers = RawHeaders | (() => RawHeaders) | (() => Promise<RawHeaders>);

export interface ClientRequestConfig<D = any> extends AxiosRequestConfig<D> {
  headers?: RawHeaders;
}

export type BaseFetcher = {
  fetcherMethod: (operation: GraphqlOperation | GraphqlOperation[], config?: ClientRequestConfig) => Promise<any>;
  fetcherInstance: AxiosInstance | unknown | undefined;
};

export type ClientOptions = Omit<ClientRequestConfig, 'body' | 'headers'> & {
  url?: string;
  timeout?: number;
  batch?: BatchOptions | boolean;
  fetcherMethod?: BaseFetcher['fetcherMethod'];
  fetcherInstance?: BaseFetcher['fetcherInstance'];
  headers?: Headers;
  subscription?: { url?: string; headers?: Headers } & Partial<WSClientOptions>;
  webSocketImpl?: unknown;
  generateGraphqlOperationOptions?: ParseRequestOptions;
};

export interface GraphQLClient {
  wsClient?: WSClient;
  query?: <T = any>(request: any, config?: ClientRequestConfig) => Promise<T>;
  mutation?: <T = any>(request: any, config?: ClientRequestConfig) => Promise<T>;
  subscription?: <T = any>(request: any, config?: ClientOptions) => Observable<T>;
  fetcherInstance: BaseFetcher['fetcherInstance'];
  fetcherMethod: BaseFetcher['fetcherMethod'];
}

export function createClient({
  queryRoot,
  mutationRoot,
  subscriptionRoot,
  generateGraphqlOperationOptions,
  ...options
}: ClientOptions & {
  queryRoot?: LinkedType;
  mutationRoot?: LinkedType;
  subscriptionRoot?: LinkedType;
}): GraphQLClient {
  const { fetcherMethod, fetcherInstance } = createFetcher(options);
  const client: GraphQLClient = {
    fetcherInstance,
    fetcherMethod,
  };

  if (queryRoot) {
    client.query = (request, config) => {
      if (!queryRoot) throw new Error('queryRoot argument is missing');
      return client.fetcherMethod(
        generateGraphqlOperation('query', queryRoot, request, generateGraphqlOperationOptions),
        config,
      );
    };
  }

  if (mutationRoot) {
    client.mutation = (request, config) => {
      if (!mutationRoot) throw new Error('mutationRoot argument is missing');
      return client.fetcherMethod(
        generateGraphqlOperation('mutation', mutationRoot, request, generateGraphqlOperationOptions),
        config,
      );
    };
  }

  if (subscriptionRoot) {
    client.subscription = (request, config) => {
      if (!subscriptionRoot) {
        throw new Error('subscriptionRoot argument is missing');
      }
      const op = generateGraphqlOperation('subscription', subscriptionRoot, request, generateGraphqlOperationOptions);
      if (!client.wsClient) {
        client.wsClient = getSubscriptionClient(options, config);
      }
      return new Observable<any>((observer) => {
        const unsubscribe = client.wsClient?.subscribe(op, {
          next: (data) => observer.next(data),
          error: (err) => observer.error(err),
          complete: () => observer.complete(),
        });

        return () => {
          unsubscribe?.();
        };
      });
    };
  }

  return client;
}

function getSubscriptionClient(opts: ClientOptions = {}, config?: ClientOptions): WSClient {
  const { url: httpClientUrl, subscription, webSocketImpl } = opts || {};
  let { url, headers = {}, ...restOpts } = subscription || {};

  // by default use the top level url
  if (!url && httpClientUrl) {
    url = httpClientUrl?.replace(/^http/, 'ws');
  }

  if (!url) {
    throw new Error('Subscription client error: missing url parameter');
  }

  const explicitWebSocketImpl = getExplicitWebSocketImpl(config?.webSocketImpl ?? restOpts.webSocketImpl ?? webSocketImpl);

  const wsOpts: WSClientOptions = {
    url,
    lazy: true,
    shouldRetry: () => true,
    retryAttempts: 3,
    connectionParams: async () => {
      let headersObject = typeof headers == 'function' ? await headers() : headers;
      headersObject = headersObject || {};
      return {
        headers: headersObject,
      };
    },
    ...restOpts,
    ...config,
  };

  if (explicitWebSocketImpl) {
    wsOpts.webSocketImpl = explicitWebSocketImpl;
  }

  return createWSClient(wsOpts);
}

function getExplicitWebSocketImpl(webSocketImpl: unknown): unknown {
  if (!webSocketImpl) {
    return undefined;
  }

  if (
    typeof webSocketImpl === 'function' &&
    'constructor' in webSocketImpl &&
    'CLOSED' in webSocketImpl &&
    'CLOSING' in webSocketImpl &&
    'CONNECTING' in webSocketImpl &&
    'OPEN' in webSocketImpl
  ) {
    return webSocketImpl;
  }

  return undefined;
}
