/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, {
	AxiosError,
	AxiosInstance,
	AxiosRequestConfig,
	InternalAxiosRequestConfig,
} from 'axios'
import Cookies from 'js-cookie'

export type TokenPair = { accessToken: string; refreshToken?: string }

export type RefreshFn = (refreshToken: string) => Promise<TokenPair>

export type CreateHttpClientOptions = {
	baseURL: string
	cookieKeys: any
	refreshFn: RefreshFn

	skipRefreshPaths?: string[]

	onAuthFailure?: (reason: unknown) => void
}

type RetryableRequestConfig = InternalAxiosRequestConfig & { _retry?: boolean }

function isAxiosError(e: unknown): e is AxiosError {
	return axios.isAxiosError(e)
}

export function createHttpClient(opts: CreateHttpClientOptions): AxiosInstance {
	const client = axios.create({ baseURL: opts.baseURL })

	client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
		const token = Cookies.get(opts.cookieKeys.access)
		if (token) {
			config.headers = config.headers ?? {}
			config.headers.Authorization = `Bearer ${token}`
		}
		return config
	})

	let refreshing: Promise<string> | null = null

	const shouldSkipRefresh = (url?: string) => {
		if (!url) return false
		const list = opts.skipRefreshPaths ?? []
		return list.some((p) => url.includes(p))
	}

	client.interceptors.response.use(
		(res) => res,
		async (err: unknown) => {
			if (!isAxiosError(err)) return Promise.reject(err)

			const status = err.response?.status
			const original = err.config as RetryableRequestConfig | undefined

			if (!original) return Promise.reject(err)

			if (shouldSkipRefresh(original.url)) return Promise.reject(err)

			if (status !== 401 || original._retry) return Promise.reject(err)

			original._retry = true

			const refreshToken = Cookies.get(opts.cookieKeys.refresh)
			if (!refreshToken) {
				opts.onAuthFailure?.(err)
				return Promise.reject(err)
			}

			if (!refreshing) {
				refreshing = (async () => {
					const pair = await opts.refreshFn(refreshToken)
					Cookies.set(opts.cookieKeys.access, pair.accessToken)
					if (pair.refreshToken)
						Cookies.set(opts.cookieKeys.refresh, pair.refreshToken)
					return pair.accessToken
				})().finally(() => {
					refreshing = null
				})
			}

			try {
				const newAccess = await refreshing
				original.headers = original.headers ?? {}
				original.headers.Authorization = `Bearer ${newAccess}`
				return client(original as AxiosRequestConfig)
			} catch (refreshErr) {
				Cookies.remove(opts.cookieKeys.access)
				Cookies.remove(opts.cookieKeys.refresh)
				if (opts.cookieKeys.user) Cookies.remove(opts.cookieKeys.user)

				opts.onAuthFailure?.(refreshErr)
				return Promise.reject(refreshErr)
			}
		},
	)

	return client
}
