import type { FetchOptions } from 'ofetch'
import { $fetch, FetchError } from 'ofetch'
import { useAuthStore } from '~/shared/stores/auth'

export type AnyObject = Record<string, unknown>

export type BlobResponse = {
  data: Blob
  headers: Headers
}

export type ApiClient = {
  get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
  getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<BlobResponse>
  post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
  put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
  patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
  delete<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
}

export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
  FetchOptions<ResponseType> & {
    toast?: boolean
    toastOn401?: boolean
    toastTitle?: string
    toastErrorMessage?: string
    toastSuccessMessage?: string
    toastErrorKey?: string
    toastSuccessKey?: string
  }

let isHandlingUnauthorized = false

export function useApi(): ApiClient {
  const config = useRuntimeConfig()
  const baseURL = config.public.apiBase || '/api'
  const toast = useToast()
  const auth = useAuthStore()
  const nuxtApp = useNuxtApp()
  const i18n = nuxtApp.$i18n as
    | {
        t: (key: string) => string
        te?: (key: string) => boolean
      }
    | undefined
  const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
  const te = (key: string) => (i18n?.te ? i18n.te(key) : false)

  function extractErrorMessage(error: unknown, responseData?: unknown): string {
    const data = responseData ?? (error as FetchError)?.data

    if (typeof data === 'string') {
      return data
    }

    if (data && typeof data === 'object') {
      const record = data as Record<string, unknown>
      return (
        (record['hydra:description'] as string) ||
        (record.detail as string) ||
        (record.message as string) ||
        (record.error as string) ||
        (record.title as string) ||
        (record['hydra:title'] as string) ||
        ''
      )
    }

    return (error as FetchError)?.message ?? 'Erreur inconnue.'
  }

  const methodErrorKeys: Record<string, string> = {
    GET: 'errors.http.get',
    POST: 'errors.http.post',
    PUT: 'errors.http.put',
    PATCH: 'errors.http.patch',
    DELETE: 'errors.http.delete'
  }

  const client = $fetch.create({
    baseURL,
    retry: 0,
    credentials: 'include',
    onResponse({ options, response }) {
      const apiOptions = options as ApiFetchOptions<'json'>
      if (apiOptions?.toast === false) {
        return
      }

      if (response?.status && response.status >= 400) {
        return
      }

      const successKey = apiOptions?.toastSuccessKey
      const successMessage =
        apiOptions?.toastSuccessMessage ||
        (successKey ? (te(successKey) ? t(successKey) : successKey) : '')

      if (successMessage) {
        toast.success({
          title: 'Succès',
          message: successMessage
        })
      }
    },
    async onResponseError({ response, error, options }) {
      const apiOptions = options as ApiFetchOptions<'json'>
      if (response?.status === 401) {
        const requestUrl = typeof options?.url === 'string' ? options.url : ''
        const isLoginCheck = requestUrl.includes('/login_check')
        const isLogout = requestUrl.includes('/logout')
        const shouldToast401 = apiOptions?.toastOn401 === true && apiOptions?.toast !== false

        if (shouldToast401) {
          const errorKey = apiOptions?.toastErrorKey
          const errorMessage =
            errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
          const extractedMessage = extractErrorMessage(error, response?._data)
          const message =
            apiOptions?.toastErrorMessage ||
            errorMessage ||
            extractedMessage ||
            'Une erreur est survenue.'

          toast.error({
            title: apiOptions?.toastTitle ?? 'Erreur',
            message
          })
        }

        if (!isLoginCheck && !isLogout) {
          if (!isHandlingUnauthorized) {
            isHandlingUnauthorized = true
            auth.clearSession()
            const route = useRoute()
            if (route.path !== '/login') {
              await navigateTo('/login')
            }
            isHandlingUnauthorized = false
          }
        }

        return
      }

      if (apiOptions?.toast === false) {
        return
      }

      const method =
        typeof options?.method === 'string' ? options.method.toUpperCase() : 'GET'
      const defaultKey = methodErrorKeys[method]
      const defaultMessage =
        defaultKey && te(defaultKey) ? t(defaultKey) : ''
      const errorKey = apiOptions?.toastErrorKey
      const errorMessage =
        errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
      const extractedMessage = extractErrorMessage(error, response?._data)
      const message =
        apiOptions?.toastErrorMessage ||
        errorMessage ||
        defaultMessage ||
        extractedMessage ||
        'Une erreur est survenue.'

      toast.error({
        title: apiOptions?.toastTitle ?? 'Erreur',
        message
      })
    }
  })

  function request<T>(
    method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
    url: string,
    options: ApiFetchOptions<'json'> = {}
  ) {
    const needsJsonBody = method === 'POST' || method === 'PUT'
    const needsMergePatch = method === 'PATCH'
    const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData

    const headers = new Headers(options.headers as HeadersInit | undefined)

    if (!isFormData) {
      if (needsMergePatch && !headers.has('Content-Type')) {
        headers.set('Content-Type', 'application/merge-patch+json')
      } else if (needsJsonBody && !headers.has('Content-Type')) {
        headers.set('Content-Type', 'application/json')
      }
    }

    return client<T>(url, { ...options, method, headers })
  }

  return {
    get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
      return request<T>('GET', url, { ...options, query })
    },
    getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
      return client
        .raw(url, { ...options, method: 'GET', query, responseType: 'blob' })
        .then((res) => ({ data: res._data as Blob, headers: res.headers }))
    },
    post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
      return request<T>('POST', url, { ...options, body })
    },
    put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
      return request<T>('PUT', url, { ...options, body })
    },
    patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
      return request<T>('PATCH', url, { ...options, body })
    },
    delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
      return request<T>('DELETE', url, { ...options, query })
    }
  }
}
