import { getAuthToken } from '../core/auth';
import type { QuerySerializerOptions } from '../core/bodySerializer';
import {
  serializeArrayParam,
  serializeObjectParam,
  serializePrimitiveParam,
} from '../core/pathSerializer';
import { getUrl } from '../core/utils';
import type { Client, ClientOptions, Config, RequestOptions } from './types';

export const createQuerySerializer = <T = unknown>({
  parameters = {},
  ...args
}: QuerySerializerOptions = {}) => {
  const querySerializer = (queryParams: T) => {
    const search: string[] = [];
    if (queryParams && typeof queryParams === 'object') {
      for (const name in queryParams) {
        const value = queryParams[name];

        if (value === undefined || value === null) {
          continue;
        }

        const options = parameters[name] || args;

        if (Array.isArray(value)) {
          const serializedArray = serializeArrayParam({
            allowReserved: options.allowReserved,
            explode: true,
            name,
            style: 'form',
            value,
            ...options.array,
          });
          if (serializedArray) search.push(serializedArray);
        } else if (typeof value === 'object') {
          const serializedObject = serializeObjectParam({
            allowReserved: options.allowReserved,
            explode: true,
            name,
            style: 'deepObject',
            value: value as Record<string, unknown>,
            ...options.object,
          });
          if (serializedObject) search.push(serializedObject);
        } else {
          const serializedPrimitive = serializePrimitiveParam({
            allowReserved: options.allowReserved,
            name,
            value: value as string,
          });
          if (serializedPrimitive) search.push(serializedPrimitive);
        }
      }
    }
    return search.join('&');
  };
  return querySerializer;
};

const checkForExistence = (
  options: Pick<RequestOptions, 'auth' | 'query'> & {
    headers: Record<any, unknown>;
  },
  name?: string,
): boolean => {
  if (!name) {
    return false;
  }
  if (name in options.headers || options.query?.[name]) {
    return true;
  }
  if (
    'Cookie' in options.headers &&
    options.headers['Cookie'] &&
    typeof options.headers['Cookie'] === 'string'
  ) {
    return options.headers['Cookie'].includes(`${name}=`);
  }
  return false;
};

export async function setAuthParams(
  options: Pick<RequestOptions, 'auth' | 'query' | 'security'> & {
    headers: Record<any, unknown>;
  },
): Promise<void> {
  for (const auth of options.security ?? []) {
    if (checkForExistence(options, auth.name)) {
      continue;
    }
    const token = await getAuthToken(auth, options.auth);

    if (!token) {
      continue;
    }

    const name = auth.name ?? 'Authorization';

    switch (auth.in) {
      case 'query':
        if (!options.query) {
          options.query = {};
        }
        options.query[name] = token;
        break;
      case 'cookie': {
        const value = `${name}=${token}`;
        if ('Cookie' in options.headers && options.headers['Cookie']) {
          options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`;
        } else {
          options.headers['Cookie'] = value;
        }
        break;
      }
      case 'header':
      default:
        options.headers[name] = token;
        break;
    }
  }
}

export const buildUrl: Client['buildUrl'] = (options) => {
  const instanceBaseUrl = options.axios?.defaults?.baseURL;

  const baseUrl =
    options.baseURL && typeof options.baseURL === 'string' ? options.baseURL : instanceBaseUrl;

  return getUrl({
    baseUrl: baseUrl as string,
    path: options.path,
    // let `paramsSerializer()` handle query params if it exists
    query: !options.paramsSerializer ? options.query : undefined,
    querySerializer:
      typeof options.querySerializer === 'function'
        ? options.querySerializer
        : createQuerySerializer(options.querySerializer),
    url: options.url,
  });
};

export const mergeConfigs = (a: Config, b: Config): Config => {
  const config = { ...a, ...b };
  config.headers = mergeHeaders(a.headers, b.headers);
  return config;
};

/**
 * Special Axios headers keywords allowing to set headers by request method.
 */
export const axiosHeadersKeywords = [
  'common',
  'delete',
  'get',
  'head',
  'patch',
  'post',
  'put',
] as const;

export const mergeHeaders = (
  ...headers: Array<Required<Config>['headers'] | undefined>
): Record<any, unknown> => {
  const mergedHeaders: Record<any, unknown> = {};
  for (const header of headers) {
    if (!header || typeof header !== 'object') {
      continue;
    }

    const iterator = Object.entries(header);

    for (const [key, value] of iterator) {
      if (
        axiosHeadersKeywords.includes(key as (typeof axiosHeadersKeywords)[number]) &&
        typeof value === 'object'
      ) {
        mergedHeaders[key] = {
          ...(mergedHeaders[key] as Record<any, unknown>),
          ...value,
        };
      } else if (value === null) {
        delete mergedHeaders[key];
      } else if (Array.isArray(value)) {
        for (const v of value) {
          // @ts-expect-error
          mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string];
        }
      } else if (value !== undefined) {
        // assume object headers are meant to be JSON stringified, i.e., their
        // content value in OpenAPI specification is 'application/json'
        mergedHeaders[key] = typeof value === 'object' ? JSON.stringify(value) : (value as string);
      }
    }
  }
  return mergedHeaders;
};

export const createConfig = <T extends ClientOptions = ClientOptions>(
  override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
  ...override,
});
