import axiosRetry from 'axios-retry'
import {
	AxiosCacheInstance,
	setupCache,
	CacheOptions,
} from 'axios-cache-interceptor'
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'

import { RetryOptions } from '../index'
import {
	parseDates,
	stringifyDates,
	toCamelCase,
	toSnakeCase,
} from './converter'
import { ApiError } from './errors'

// Constants
export enum BaseUrl {
	Prod = 'https://api.bam.fan',
	Dev = 'https://develop.bam.fan',
	QA = 'https://qa.bam.fan',
	ThProd = 'https://api.tickethead.io',
	ThStaging = 'https://api.staging.tickethead.io',
}

export function createBaseUrl(
	baseUrl: string = BaseUrl.Prod,
	tenantName: string = undefined
) {
	const [protocol, base] = baseUrl.split('://')

	return `${protocol}://${tenantName?.concat('.') ?? ''}${base}`
}

/**
 * Sets the update request strategy for an axios instance
 */
export function setAxiosUpdateTokenFunction(
	instance: AxiosInstance,
	updateToken: () => Promise<string>
) {
	;(instance as any).updateToken = updateToken
}

/**
 * Does nothing. Will cause 401 requests to fail as usual.
 */
async function nopUpdate(): Promise<string> {
	return ''
}

export interface IClientBuildOptions {
	baseUrl?: string
	tenantName?: string
	retryOptions?: RetryOptions
	cacheOptions?: CacheOptions
	camelCaseResponse?: boolean
}
/**
 * Builds an API client object. By default, creates a production client with cluster-wide access.
 * If `tenantName` is specified, the client includes the tenant name in the URL.
 *
 * @param baseUrl Base URL of the REST API (default: PROD_BASE_URL)
 * @param tenantName Tenant name (defaults to undefined)
 * @param retryOptions Configuration for the retry policy
 * @param cacheOptions Configuration for caching, by default no caching is done
 * @returns axios.AxiosInstance
 */
export function buildClient({
	baseUrl = BaseUrl.Prod,
	tenantName,
	retryOptions = { retries: 3 },
	cacheOptions,
	camelCaseResponse,
}: IClientBuildOptions): AxiosInstance | AxiosCacheInstance {
	const axiosOptions: AxiosRequestConfig = {
		baseURL: createBaseUrl(baseUrl, tenantName),
	}

	let instance = axios.create(axiosOptions)
	if (cacheOptions) {
		instance = setupCache(instance, cacheOptions)
	}

	// Add a retry mechanism to all requests
	axiosRetry(instance, retryOptions)

	instance.defaults.headers.post['Content-Type'] = 'application/json'
	instance.defaults.headers.patch['Content-Type'] = 'application/json'
	instance.defaults.headers.put['Content-Type'] = 'application/json'

	// Sets the token update strategy
	setAxiosUpdateTokenFunction(instance, nopUpdate)

	// TODO: Add expiration check and update the token if needed
	instance.interceptors.request.use(
		function (config) {
			if (
				!config.headers['Content-Type'] ||
				config.headers['Content-Type'] === 'application/json'
			) {
				config.data = stringifyDates(toSnakeCase(config.data))
			}
			return config
		},
		function (error) {
			Promise.reject(error)
		}
	)

	instance.interceptors.response.use(
		function (response) {
			// If there is no need to convert case
			if (
				!camelCaseResponse ||
				['stream', 'blob', 'arraybuffer'].includes(response.config.responseType)
			) {
				return response
			}

			return {
				...response,
				data: parseDates(toCamelCase(response.data)),
			}
		},
		async function (error) {
			// Reject promise if usual error or it has been retried
			if (error?.response?.status !== 401 || error?.response?.config._retry) {
				// error.response.config._retry = false
				return Promise.reject(errorBuilder(error))
			}

			// When response code is 401, try to refresh the token.
			try {
				// BEWARE of hack
				// Added updateToken function to axios instance because removing an interceptor is very hard
				// It enables changing of the token updating strategy on the fly
				const token = await (instance as any).updateToken()
				// Sets the retry flag
				error.response.config._retry = true
				error.response.config.headers['Authorization'] = 'Bearer ' + token
				return await instance(error.response.config)
			} catch (err) {
				return Promise.reject(errorBuilder(error))
			}
		}
	)

	return instance
}

/**
 * Builds a error instance based on the response HTTP status code
 *
 * @param error Error response from axios instance
 * @returns Error implementation
 */
function errorBuilder(error: any): ApiError {
	// The error is sometimes an error object and sometimes an axios error
	const message = error.response?.data?.message || error.message
	const errorCode =
		error.response?.data?.errorCode ||
		error.response?.data?.error ||
		error.error
	const status = error.response?.status || error.statusCode
	let url = error?.url
	if (error.config?.baseURL) {
		url = `${error.config?.baseURL}/${error.config?.url}`
	}
	const method = error.config?.method ?? error?.method
	const data = error.config?.data

	if (status) {
		return new ApiError(message, errorCode, status, url, method, data)
	}
	return new ApiError(
		'Error in response from service',
		JSON.stringify(error),
		status,
		url,
		method,
		data
	)
}
