import { HTTP_STATUS, HttpStatus } from 'camino-common/src/http'
import {
  CaminoRestParams,
  CaminoRestRoute,
  CaminoRestRoutes,
  DeleteRestRoutes,
  DownloadRestRoutes,
  NewDownloadRestRoutes,
  getRestRoute,
  GetRestRoutes,
  NewPostRestRoutes,
  NewGetRestRoutes,
  NewPutRestRoutes,
  NewDeleteRestRoutes,
} from 'camino-common/src/rest'
import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty } from 'camino-common/src/typescript-tools'
import { CaminoError } from 'camino-common/src/zod-tools'
import { ZodType, z } from 'zod'

type Loading = { status: 'LOADING' }
type CaminoApiError = { status: 'ERROR'; message: string }
type NewCaminoApiError = { status: 'NEW_ERROR'; error: CaminoError<string> }
export class CaminoHttpError extends Error {
  public readonly statusCode: HttpStatus
  constructor(errorMessage: string, statusCode: HttpStatus) {
    super(errorMessage)

    this.statusCode = statusCode
  }
}

export type AsyncData<T> = Loading | { status: 'LOADED'; value: T } | CaminoApiError | NewCaminoApiError

export const asyncDataAutomaticLoad = async <T extends object>(value: () => Promise<T | CaminoError<string>>, setState: (newState: AsyncData<T>) => void): Promise<void> => {
  setState({ status: 'LOADING' })
  const result = await value()
  if ('message' in result) {
    setState({ status: 'NEW_ERROR', error: result })
  } else {
    setState({ status: 'LOADED', value: result })
  }
}

type UiRestRoute = string & { __camino: 'RestRoute' }

const baseRoute = '/apiUrl'

const getUiRestRoute = <T extends CaminoRestRoute>(route: T, params: CaminoRestParams<T>, searchParams: Record<string, string | string[]> = {}): UiRestRoute => {
  return `${baseRoute}${getRestRoute(route, params, searchParams)}` as UiRestRoute
}

// TODO 2023-05-25: move into Download component and make download component display href
export const getDownloadRestRoute = <T extends DownloadRestRoutes | NewDownloadRestRoutes>(route: T, params: CaminoRestParams<T>, searchParams: Record<string, string> = {}): UiRestRoute => {
  return getUiRestRoute(route, params, searchParams)
}

const callFetch = async <T extends CaminoRestRoute>(
  path: T,
  params: CaminoRestParams<T>,
  method: 'post' | 'put' | 'get' | 'delete',
  searchParams: Record<string, string | string[]> = {},
  body?: any
): Promise<any> => {
  const url = getUiRestRoute(path, params, searchParams)

  const defaultOptions = {
    method,
    headers: { 'Content-Type': 'application/json' },
  }

  const fetched = await fetch(url, isNotNullNorUndefined(body) ? { ...defaultOptions, body: JSON.stringify(body) } : defaultOptions)
  if (fetched.ok) {
    if (fetched.status === 200) {
      // TODO 2024-08-21 parser le body via les validator de rest.ts
      const body = await fetched.json()

      return body
    }

    return
  }
  if (fetched.status === HTTP_STATUS.UNAUTHORIZED) {
    window.location.replace('/oauth2/sign_in?rd=' + encodeURIComponent(window.location.href))
  }
  console.error(`Une erreur s'est produite lors de la récupération des données ${await fetched.text()}`)
  // TODO 2024-05-02 améliorer la gestion du status http
  throw new CaminoHttpError(`Une erreur s'est produite lors de la récupération des données`, fetched.status as HttpStatus)
}
type GetWithJsonArgs<T extends GetRestRoutes | NewGetRestRoutes, Method extends keyof (typeof CaminoRestRoutes)[T]> = (typeof CaminoRestRoutes)[T][Method] extends { searchParams: ZodType }
  ? [path: T, params: CaminoRestParams<T>, searchParams: (typeof CaminoRestRoutes)[T][Method]['searchParams']['_input']]
  : [path: T, params: CaminoRestParams<T>]

/**
 * @deprecated use newGetWithJson
 **/
export const getWithJson = async <T extends GetRestRoutes>(...args: GetWithJsonArgs<T, 'get'>): Promise<z.infer<(typeof CaminoRestRoutes)[T]['get']['output']>> =>
  callFetch(args[0], args[1], 'get', args.length === 3 ? args[2] : {})

export const downloadBlob = async <T extends DownloadRestRoutes>(
  path: T,
  params: CaminoRestParams<T>,
  searchParams: Record<string, string> = {}
): Promise<{ data: Blob; filename: string } | undefined> => {
  const url = getUiRestRoute(path, params, searchParams)

  const fetched = await fetch(url)
  if (fetched.ok) {
    if (fetched.status === 200) {
      let filename: string = 'telechargement_camino'

      const contentDisposition = fetched.headers.get('Content-Disposition')
      if (isNotNullNorUndefinedNorEmpty(contentDisposition)) {
        const filenameMatch = contentDisposition.match(/filename=(.+)/)
        if (isNotNullNorUndefinedNorEmpty(filenameMatch) && filenameMatch.length > 1) {
          filename = filenameMatch[1]
        }
      }

      return { data: await fetched.blob(), filename }
    }

    return
  }

  if (fetched.status === HTTP_STATUS.UNAUTHORIZED) {
    window.location.replace('/oauth2/sign_in?rd=' + encodeURIComponent(window.location.href))
  }

  console.error(`Une erreur s'est produite lors de la récupération des données ${await fetched.text()}`)
  // TODO 2024-05-02 améliorer la gestion du status http
  throw new CaminoHttpError(`Une erreur s'est produite lors de la récupération des données`, fetched.status as HttpStatus)
}

const errorMessage: CaminoError<string> = { message: "L'api est inaccessible" }
export const newGetWithJson = async <T extends NewGetRestRoutes>(...args: GetWithJsonArgs<T, 'newGet'>): Promise<CaminoError<string> | z.infer<(typeof CaminoRestRoutes)[T]['newGet']['output']>> => {
  try {
    const url = getUiRestRoute(args[0], args[1], args.length === 3 ? args[2] : {})

    const defaultOptions = {
      method: 'get',
      headers: { 'Content-Type': 'application/json' },
    }

    const fetched = await fetch(url, defaultOptions)
    if (fetched.ok) {
      if (fetched.status === 200) {
        // TODO 2024-08-21 parser le body via les validator de rest.ts
        const body = await fetched.json()

        return body
      }

      return
    }

    if (fetched.status === HTTP_STATUS.UNAUTHORIZED) {
      window.location.replace('/oauth2/sign_in?rd=' + encodeURIComponent(window.location.href))
    }

    const bodyErrorResponse = await fetched.json()
    return bodyErrorResponse
  } catch (e: any) {
    return { ...errorMessage, extra: e }
  }
}

export const newDeleteWithJson = async <T extends NewDeleteRestRoutes>(path: T, params: CaminoRestParams<T>): Promise<CaminoError<string> | void> => {
  try {
    const url = getUiRestRoute(path, params, {})

    const defaultOptions = {
      method: 'delete',
      headers: { 'Content-Type': 'application/json' },
    }

    const fetched = await fetch(url, defaultOptions)
    if (fetched.ok) {
      return undefined
    }
    if (fetched.status === HTTP_STATUS.UNAUTHORIZED) {
      window.location.replace('/oauth2/sign_in?rd=' + encodeURIComponent(window.location.href))
    }
    const bodyErrorResponse = await fetched.json()
    return bodyErrorResponse
  } catch (e: any) {
    return { ...errorMessage, extra: e }
  }
}

/**
 * @deprecated use newDeleteWithJson
 **/
export const deleteWithJson = async <T extends DeleteRestRoutes>(path: T, params: CaminoRestParams<T>, searchParams: Record<string, string | string[]> = {}): Promise<void> =>
  callFetch(path, params, 'delete', searchParams)

export const newPostWithJson = async <T extends NewPostRestRoutes>(
  path: T,
  params: CaminoRestParams<T>,
  body: z.infer<(typeof CaminoRestRoutes)[T]['newPost']['input']>
): Promise<CaminoError<string> | z.infer<(typeof CaminoRestRoutes)[T]['newPost']['output']>> => {
  try {
    const url = getUiRestRoute(path, params, {})

    const defaultOptions = {
      method: 'post',
      headers: { 'Content-Type': 'application/json' },
    }

    const fetched = await fetch(url, { ...defaultOptions, body: JSON.stringify(body) })
    if (fetched.ok) {
      const bodyResponse = await fetched.json()
      return bodyResponse
    }
    if (fetched.status === HTTP_STATUS.UNAUTHORIZED) {
      window.location.replace('/oauth2/sign_in?rd=' + encodeURIComponent(window.location.href))
    }
    const bodyErrorResponse = await fetched.json()
    return bodyErrorResponse
  } catch (e: any) {
    return { ...errorMessage, extra: e }
  }
}

export const newPutWithJson = async <T extends NewPutRestRoutes>(
  path: T,
  params: CaminoRestParams<T>,
  body: z.infer<(typeof CaminoRestRoutes)[T]['newPut']['input']>
): Promise<CaminoError<string> | z.infer<(typeof CaminoRestRoutes)[T]['newPut']['output']>> => {
  try {
    const url = getUiRestRoute(path, params, {})

    const defaultOptions = {
      method: 'put',
      headers: { 'Content-Type': 'application/json' },
    }

    const fetched = await fetch(url, { ...defaultOptions, body: JSON.stringify(body) })
    if (fetched.ok) {
      const bodyResponse = await fetched.json()
      return bodyResponse
    }
    if (fetched.status === HTTP_STATUS.UNAUTHORIZED) {
      window.location.replace('/oauth2/sign_in?rd=' + encodeURIComponent(window.location.href))
    }
    const bodyErrorResponse = await fetched.json()
    return bodyErrorResponse
  } catch (e: any) {
    return { ...errorMessage, extra: e }
  }
}
