import { CaminoApiError, Index } from '../types'
import type { Pool } from 'pg'

import express, { type Router } from 'express'
import { join } from 'path'
import { inspect } from 'node:util'

import { activites, demarches, entreprises, titre, titres, travaux } from '../api/rest/index'
import { NewDownload, avisDocumentDownload, etapeDocumentDownload, etapeTelecharger, streamLargeObjectInResponse } from '../api/rest/fichiers'
import { getTitreLiaisons, postTitreLiaisons, removeTitre, titresAdministrations, updateTitre, utilisateurTitreAbonner, getTitre, getUtilisateurTitreAbonner, titresSuper } from '../api/rest/titres'
import {
  creerEntreprise,
  fiscalite,
  getEntrepriseRest,
  modifierEntreprise,
  getEntrepriseDocuments,
  postEntrepriseDocument,
  deleteEntrepriseDocument,
  entrepriseDocumentDownload,
  getAllEntreprises,
} from '../api/rest/entreprises'
import { deleteUtilisateur, generateQgisToken, moi, updateUtilisateurPermission, utilisateurs, getUtilisateurs, getUtilisateur } from '../api/rest/utilisateurs'
import { logout, resetPassword } from '../api/rest/keycloak'
import { getDGTMStats, getGranulatsMarinsStats, getGuyaneStats, getMinerauxMetauxMetropolesStats } from '../api/rest/statistiques/index'
import {
  CaminoRestRoutes,
  DownloadFormat,
  contentTypes,
  GetRestRoutes,
  PostRestRoutes,
  PutRestRoutes,
  DeleteRestRoutes,
  isCaminoRestRoute,
  DownloadRestRoutes,
  CaminoRestRoute,
  NewDownloadRestRoutes,
  NewPostRestRoutes,
  NewGetRestRoutes,
  NewPutRestRoutes,
  NewDeleteRestRoutes,
} from 'camino-common/src/rest'
import { CaminoConfig } from 'camino-common/src/static/config'
import { CaminoRequest, CustomResponse } from '../api/rest/express-type'
import { User, UserNotNull } from 'camino-common/src/roles'
import { createEtape, deleteEtape, deposeEtape, getEtape, getEtapeAvis, getEtapeDocuments, getEtapeEntrepriseDocuments, getEtapesTypesEtapesStatusWithMainStep, updateEtape } from '../api/rest/etapes'
import { ZodType, z } from 'zod'
import { getCommunes } from '../api/rest/communes'
import { SendFileOptions } from 'express-serve-static-core'
import { activiteDocumentDownload, getActivite, updateActivite, deleteActivite } from '../api/rest/activites'
import { isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools'
import { getDemarcheByIdOrSlugApi, demarcheSupprimer, demarcheCreer, getDemarchesEnConcurrence, getResultatEnConcurrence } from '../api/rest/demarches'
import { geojsonImport, geojsonImportPoints, getPerimetreInfos, geojsonImportForages } from '../api/rest/perimetre'
import { getDataGouvStats } from '../api/rest/statistiques/datagouv'
import { addAdministrationActiviteTypeEmails, deleteAdministrationActiviteTypeEmails, getAdministrationActiviteTypeEmails, getAdministrationUtilisateurs } from '../api/rest/administrations'
import { titreDemandeCreer } from '../api/rest/titre-demande'
import { config } from '../config/index'
import { addLog } from '../api/rest/logs.queries'
import { HTTP_STATUS } from 'camino-common/src/http'
import { zodParseEffect } from '../tools/fp-tools'
import { Cause, Effect, Exit, Option, pipe } from 'effect'

interface IRestResolverResult {
  nom: string
  format: DownloadFormat
  contenu?: string
  filePath?: string
  buffer?: Buffer
}

type IRestResolver = (
  {
    params,
    query,
  }: {
    params: Index<unknown>
    query: Index<unknown>
  },
  user: User
) => Promise<IRestResolverResult | null>

type CaminoRestRoutesType = typeof CaminoRestRoutes
type RestGetCall<Route extends GetRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['get']['output']>>) => Promise<void>
export type RestNewPostCall<Route extends NewPostRestRoutes> = (
  data: Effect.Effect<
    {
      pool: Pool
      user: UserNotNull
      body: z.infer<CaminoRestRoutesType[Route]['newPost']['input']>
      params: z.infer<CaminoRestRoutesType[Route]['params']>
    },
    never,
    never
  >
) => Effect.Effect<z.infer<CaminoRestRoutesType[Route]['newPost']['output']>, CaminoApiError<string>>
export type RestNewPutCall<Route extends NewPutRestRoutes> = (
  data: Effect.Effect<
    {
      pool: Pool
      user: UserNotNull
      body: z.infer<CaminoRestRoutesType[Route]['newPut']['input']>
      params: z.infer<CaminoRestRoutesType[Route]['params']>
    },
    never,
    never
  >
) => Effect.Effect<z.infer<CaminoRestRoutesType[Route]['newPut']['output']>, CaminoApiError<string>>
type SearchParams<Route extends NewGetRestRoutes> = CaminoRestRoutesType[Route]['newGet'] extends { searchParams: ZodType }
  ? z.infer<CaminoRestRoutesType[Route]['newGet']['searchParams']>
  : Record<never, string>
type CookieParams = {
  clearConnectedCookie: () => void
  addConnectedCookie: () => void
}
export type RestNewGetCall<Route extends NewGetRestRoutes> = (
  data: Effect.Effect<
    {
      pool: Pool
      user: User
      params: z.infer<CaminoRestRoutesType[Route]['params']>
      searchParams: SearchParams<Route>
      cookie: CookieParams
    },
    never,
    never
  >
) => Effect.Effect<z.infer<CaminoRestRoutesType[Route]['newGet']['output']>, CaminoApiError<string>>

export type RestNewDeleteCall<Route extends NewDeleteRestRoutes> = (
  data: Effect.Effect<
    {
      pool: Pool
      user: User
      params: z.infer<CaminoRestRoutesType[Route]['params']>
      cookie: CookieParams
    },
    never,
    never
  >
) => Effect.Effect<Option.Option<never>, CaminoApiError<string>>

type RestPostCall<Route extends PostRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['post']['output']>>) => Promise<void>
type RestPutCall<Route extends PutRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['put']['output']>>) => Promise<void>
type RestDeleteCall = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<void | Error>) => Promise<void>
type RestDownloadCall = (pool: Pool) => IRestResolver

type Transform<Route> = (Route extends GetRestRoutes ? { getCall: RestGetCall<Route> } : {}) &
  (Route extends NewGetRestRoutes ? { newGetCall: RestNewGetCall<Route> } : {}) &
  (Route extends PostRestRoutes ? { postCall: RestPostCall<Route> } : {}) &
  (Route extends NewPostRestRoutes ? { newPostCall: RestNewPostCall<Route> } : {}) &
  (Route extends NewPutRestRoutes ? { newPutCall: RestNewPutCall<Route> } : {}) &
  (Route extends PutRestRoutes ? { putCall: RestPutCall<Route> } : {}) &
  (Route extends DeleteRestRoutes ? { deleteCall: RestDeleteCall } : {}) &
  (Route extends NewDeleteRestRoutes ? { newDeleteCall: RestNewDeleteCall<Route> } : {}) &
  (Route extends NewDownloadRestRoutes ? { newDownloadCall: NewDownload } : {}) &
  (Route extends DownloadRestRoutes ? { downloadCall: RestDownloadCall } : {})

const getConfig: RestNewGetCall<'/config'> = (): Effect.Effect<CaminoConfig, CaminoApiError<"Impossible d'accéder à la configuration">> => {
  return Effect.try({
    try: () => {
      const caminoConfig: CaminoConfig = {
        CAMINO_STAGE: config().CAMINO_STAGE,
        API_MATOMO_URL: config().API_MATOMO_URL,
        API_MATOMO_ID: config().API_MATOMO_ID,
      }

      return caminoConfig
    },
    catch: e => ({ message: "Impossible d'accéder à la configuration" as const, status: HTTP_STATUS.INTERNAL_SERVER_ERROR, extra: e }),
  })
}

const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<key> & CaminoRestRoutesType[key] }> = {
  // NE PAS TOUCHER A CES ROUTES, ELLES SONT UTILISÉES HORS UI
  '/download/fichiers/:documentId': { newDownloadCall: etapeDocumentDownload, ...CaminoRestRoutes['/download/fichiers/:documentId'] },
  '/download/entrepriseDocuments/:documentId': { newDownloadCall: entrepriseDocumentDownload, ...CaminoRestRoutes['/download/entrepriseDocuments/:documentId'] },
  '/download/activiteDocuments/:documentId': { newDownloadCall: activiteDocumentDownload, ...CaminoRestRoutes['/download/activiteDocuments/:documentId'] },
  '/download/avisDocument/:etapeAvisId': { newDownloadCall: avisDocumentDownload, ...CaminoRestRoutes['/download/avisDocument/:etapeAvisId'] },
  '/fichiers/:documentId': { newDownloadCall: etapeDocumentDownload, ...CaminoRestRoutes['/fichiers/:documentId'] },
  '/titres/:id': { downloadCall: titre, ...CaminoRestRoutes['/titres/:id'] },
  '/titres': { downloadCall: titres, ...CaminoRestRoutes['/titres'] },
  '/titres_qgis': { downloadCall: titres, ...CaminoRestRoutes['/titres_qgis'] },
  '/demarches': { downloadCall: demarches, ...CaminoRestRoutes['/demarches'] },
  '/travaux': { downloadCall: travaux, ...CaminoRestRoutes['/travaux'] },
  '/activites': { downloadCall: activites, ...CaminoRestRoutes['/activites'] },
  '/utilisateurs': { downloadCall: utilisateurs, ...CaminoRestRoutes['/utilisateurs'] },
  '/etape/zip/:etapeId': { downloadCall: etapeTelecharger, ...CaminoRestRoutes['/etape/zip/:etapeId'] },
  '/entreprises': { downloadCall: entreprises, ...CaminoRestRoutes['/entreprises'] },
  // NE PAS TOUCHER A CES ROUTES, ELLES SONT UTILISÉES HORS UI

  '/moi': { newGetCall: moi, ...CaminoRestRoutes['/moi'] },
  '/config': { newGetCall: getConfig, ...CaminoRestRoutes['/config'] },
  '/rest/titres/:id/titreLiaisons': { newGetCall: getTitreLiaisons, newPostCall: postTitreLiaisons, ...CaminoRestRoutes['/rest/titres/:id/titreLiaisons'] },
  '/rest/etapesTypes/:demarcheId/:date': { newGetCall: getEtapesTypesEtapesStatusWithMainStep, ...CaminoRestRoutes['/rest/etapesTypes/:demarcheId/:date'] },
  '/rest/titres': { newPostCall: titreDemandeCreer, ...CaminoRestRoutes['/rest/titres'] },
  '/rest/titres/:titreId': { deleteCall: removeTitre, postCall: updateTitre, getCall: getTitre, ...CaminoRestRoutes['/rest/titres/:titreId'] },
  '/rest/titres/:titreId/abonne': { postCall: utilisateurTitreAbonner, newGetCall: getUtilisateurTitreAbonner, ...CaminoRestRoutes['/rest/titres/:titreId/abonne'] },
  '/rest/titresAdministrations': { getCall: titresAdministrations, ...CaminoRestRoutes['/rest/titresAdministrations'] },
  '/rest/titresSuper': { newGetCall: titresSuper, ...CaminoRestRoutes['/rest/titresSuper'] },
  '/rest/statistiques/minerauxMetauxMetropole': { getCall: getMinerauxMetauxMetropolesStats, ...CaminoRestRoutes['/rest/statistiques/minerauxMetauxMetropole'] }, // UNTESTED YET
  '/rest/statistiques/guyane': { getCall: getGuyaneStats, ...CaminoRestRoutes['/rest/statistiques/guyane'] },
  '/rest/statistiques/guyane/:annee': { getCall: getGuyaneStats, ...CaminoRestRoutes['/rest/statistiques/guyane/:annee'] },
  '/rest/statistiques/granulatsMarins': { getCall: getGranulatsMarinsStats, ...CaminoRestRoutes['/rest/statistiques/granulatsMarins'] },
  '/rest/statistiques/granulatsMarins/:annee': { getCall: getGranulatsMarinsStats, ...CaminoRestRoutes['/rest/statistiques/granulatsMarins/:annee'] },
  '/rest/statistiques/dgtm': { getCall: getDGTMStats, ...CaminoRestRoutes['/rest/statistiques/dgtm'] },
  '/rest/statistiques/datagouv': { getCall: getDataGouvStats, ...CaminoRestRoutes['/rest/statistiques/datagouv'] },
  '/rest/demarches': { newPostCall: demarcheCreer, ...CaminoRestRoutes['/rest/demarches'] },
  '/rest/demarches/:demarcheIdOrSlug': { newGetCall: getDemarcheByIdOrSlugApi, deleteCall: demarcheSupprimer, ...CaminoRestRoutes['/rest/demarches/:demarcheIdOrSlug'] },
  '/rest/demarches/:demarcheId/miseEnConcurrence': { newGetCall: getDemarchesEnConcurrence, ...CaminoRestRoutes['/rest/demarches/:demarcheId/miseEnConcurrence'] },
  '/rest/demarches/:demarcheId/resultatMiseEnConcurrence': { newGetCall: getResultatEnConcurrence, ...CaminoRestRoutes['/rest/demarches/:demarcheId/resultatMiseEnConcurrence'] },
  '/rest/utilisateur/generateQgisToken': { newPostCall: generateQgisToken, ...CaminoRestRoutes['/rest/utilisateur/generateQgisToken'] },
  '/rest/utilisateurs/:id/permission': { postCall: updateUtilisateurPermission, ...CaminoRestRoutes['/rest/utilisateurs/:id/permission'] },
  '/rest/utilisateurs/:id/delete': { getCall: deleteUtilisateur, ...CaminoRestRoutes['/rest/utilisateurs/:id/delete'] },
  '/rest/utilisateurs/:id': { newGetCall: getUtilisateur, ...CaminoRestRoutes['/rest/utilisateurs/:id'] },
  '/rest/utilisateurs': { newGetCall: getUtilisateurs, ...CaminoRestRoutes['/rest/utilisateurs'] },
  '/rest/entreprises/:entrepriseId/fiscalite/:annee': { getCall: fiscalite, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/fiscalite/:annee'] }, // UNTESTED YET
  '/rest/entreprises/:entrepriseId': { newGetCall: getEntrepriseRest, newPutCall: modifierEntreprise, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId'] },
  '/rest/entreprises/:entrepriseId/documents': { newGetCall: getEntrepriseDocuments, newPostCall: postEntrepriseDocument, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/documents'] },
  '/rest/entreprises/:entrepriseId/documents/:entrepriseDocumentId': {
    newDeleteCall: deleteEntrepriseDocument,
    ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/documents/:entrepriseDocumentId'],
  },
  '/rest/entreprises': { postCall: creerEntreprise, getCall: getAllEntreprises, ...CaminoRestRoutes['/rest/entreprises'] },
  '/rest/administrations/:administrationId/utilisateurs': { getCall: getAdministrationUtilisateurs, ...CaminoRestRoutes['/rest/administrations/:administrationId/utilisateurs'] },
  '/rest/administrations/:administrationId/activiteTypeEmails': {
    getCall: getAdministrationActiviteTypeEmails,
    newPostCall: addAdministrationActiviteTypeEmails,
    ...CaminoRestRoutes['/rest/administrations/:administrationId/activiteTypeEmails'],
  },
  '/rest/administrations/:administrationId/activiteTypeEmails/delete': {
    newPostCall: deleteAdministrationActiviteTypeEmails,
    ...CaminoRestRoutes['/rest/administrations/:administrationId/activiteTypeEmails/delete'],
  },
  '/rest/demarches/:demarcheId/geojson': { getCall: getPerimetreInfos, ...CaminoRestRoutes['/rest/demarches/:demarcheId/geojson'] },
  '/rest/etapes/:etapeId/geojson': { getCall: getPerimetreInfos, ...CaminoRestRoutes['/rest/etapes/:etapeId/geojson'] },
  '/rest/etapes/:etapeIdOrSlug': { deleteCall: deleteEtape, getCall: getEtape, ...CaminoRestRoutes['/rest/etapes/:etapeIdOrSlug'] },
  '/rest/etapes': { newPostCall: createEtape, newPutCall: updateEtape, ...CaminoRestRoutes['/rest/etapes'] },
  '/rest/etapes/:etapeId/depot': { newPutCall: deposeEtape, ...CaminoRestRoutes['/rest/etapes/:etapeId/depot'] },
  '/rest/etapes/:etapeId/entrepriseDocuments': { getCall: getEtapeEntrepriseDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/entrepriseDocuments'] },
  '/rest/etapes/:etapeId/etapeDocuments': { getCall: getEtapeDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeDocuments'] },
  '/rest/etapes/:etapeId/etapeAvis': { getCall: getEtapeAvis, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeAvis'] },
  '/rest/activites/:activiteId': { getCall: getActivite, putCall: updateActivite, deleteCall: deleteActivite, ...CaminoRestRoutes['/rest/activites/:activiteId'] },
  '/rest/communes': { newGetCall: getCommunes, ...CaminoRestRoutes['/rest/communes'] },
  '/rest/geojson/import/:geoSystemeId': { newPostCall: geojsonImport, ...CaminoRestRoutes['/rest/geojson/import/:geoSystemeId'] },
  '/rest/geojson_points/import/:geoSystemeId': { newPostCall: geojsonImportPoints, ...CaminoRestRoutes['/rest/geojson_points/import/:geoSystemeId'] },
  '/rest/geojson_forages/import/:geoSystemeId': { newPostCall: geojsonImportForages, ...CaminoRestRoutes['/rest/geojson_forages/import/:geoSystemeId'] },
  '/deconnecter': { getCall: logout, ...CaminoRestRoutes['/deconnecter'] },
  '/changerMotDePasse': { getCall: resetPassword, ...CaminoRestRoutes['/changerMotDePasse'] },
} as const
export const restWithPool = (dbPool: Pool): Router => {
  const rest = express.Router()

  Object.keys(restRouteImplementations)
    .filter(isCaminoRestRoute)
    .forEach(route => {
      const maRoute = restRouteImplementations[route]
      if ('getCall' in maRoute) {
        console.info(`GET ${route}`)
        rest.get(route, restCatcher(maRoute.getCall(dbPool))) // eslint-disable-line @typescript-eslint/no-misused-promises
      }
      if ('newGetCall' in maRoute) {
        console.info(`GET ${route}`)
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        rest.get(route, async (req: CaminoRequest, res: express.Response, _next: express.NextFunction) => {
          try {
            const call = Effect.Do.pipe(
              Effect.bind('searchParams', () => {
                if ('searchParams' in maRoute.newGet) {
                  return zodParseEffect(maRoute.newGet.searchParams, req.query)
                }

                return Effect.succeed(undefined)
              }),
              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
              Effect.mapError(caminoError => {
                return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
              }),
              // TODO 2024-06-26 ici, si on ne met pas les params et les searchParams à any, on se retrouve avec une typescript union hell qui fait tout planter
              Effect.bind<'result', { searchParams: any; params: any }, z.infer<(typeof maRoute)['newGet']['output']>, CaminoApiError<string>, never>('result', ({ searchParams, params }) => {
                return maRoute.newGetCall(
                  Effect.Do.pipe(
                    Effect.let('user', () => req.auth),
                    Effect.let('pool', () => dbPool),
                    Effect.let('params', () => params),
                    Effect.let('searchParams', () => searchParams),
                    Effect.let('cookie', () => ({
                      clearConnectedCookie: () => res.clearCookie('shouldBeConnected'),
                      addConnectedCookie: () => res.cookie('shouldBeConnected', 'anyValueIsGood, We just check the presence of this cookie'),
                    }))
                  )
                )
              }),
              Effect.bind('parsedResult', ({ result }) =>
                pipe(
                  zodParseEffect(maRoute.newGet.output, result),
                  Effect.mapError(caminoError => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }))
                )
              ),
              Effect.mapBoth({
                onFailure: caminoError => {
                  console.warn(`problem with route ${route}: ${caminoError.message}`)
                  res.status(caminoError.status).json(caminoError)
                },
                onSuccess: ({ parsedResult }) => {
                  if (isNullOrUndefined(parsedResult)) {
                    res.sendStatus(HTTP_STATUS.NO_CONTENT)
                  } else {
                    res.json(parsedResult)
                  }
                },
              }),
              Effect.runPromiseExit
            )

            const pipeline = await call
            if (Exit.isFailure(pipeline)) {
              if (!Cause.isFailType(pipeline.cause)) {
                console.error('catching error on newGet route', route, pipeline.cause, req.body)
                res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: pipeline.cause })
              }
            }
          } catch (e) {
            console.error('catching error on newGet route', route, e, req.body)
            res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: e })
          }
        })
      }
      if ('postCall' in maRoute) {
        console.info(`POST ${route}`)
        rest.post(route, restCatcherWithMutation('post', maRoute.postCall(dbPool), dbPool)) // eslint-disable-line @typescript-eslint/no-misused-promises
      }

      if ('newPostCall' in maRoute) {
        console.info(`POST ${route}`)
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        rest.post(route, async (req: CaminoRequest, res: express.Response, _next: express.NextFunction) => {
          try {
            const call = Effect.Do.pipe(
              Effect.bind('user', () => {
                if (isNotNullNorUndefined(req.auth)) {
                  return Effect.succeed(req.auth as UserNotNull)
                } else {
                  return Effect.fail({ message: 'Accès interdit', status: HTTP_STATUS.FORBIDDEN })
                }
              }),
              Effect.bind('body', () => zodParseEffect(maRoute.newPost.input, req.body)),
              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
              Effect.mapError(caminoError => {
                if (!('status' in caminoError)) {
                  return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
                }

                return caminoError
              }),
              // TODO 2024-06-26 ici, si on ne met pas le body et params à any, on se retrouve avec une typescript union hell qui fait tout planter
              Effect.bind<'result', { body: any; user: UserNotNull; params: any }, z.infer<(typeof maRoute)['newPost']['output']>, CaminoApiError<string>, never>('result', ({ user, body, params }) =>
                maRoute.newPostCall(
                  Effect.Do.pipe(
                    Effect.let('user', () => user),
                    Effect.let('pool', () => dbPool),
                    Effect.let('params', () => params),
                    Effect.let('body', () => body)
                  )
                )
              ),
              Effect.bind('parsedResult', ({ result }) =>
                pipe(
                  zodParseEffect(maRoute.newPost.output, result),
                  Effect.mapError(caminoError => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }))
                )
              ),
              Effect.tap(({ user }) => addLog(dbPool, user.id, 'post', req.url, req.body)),
              Effect.mapBoth({
                onFailure: caminoError => {
                  console.warn(`problem with route ${route}: ${caminoError.message}`)

                  if (!('status' in caminoError)) {
                    res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json(caminoError)
                  } else {
                    res.status(caminoError.status).json(caminoError)
                  }
                },
                onSuccess: ({ parsedResult }) => {
                  res.json(parsedResult)
                },
              }),
              Effect.runPromiseExit
            )

            const pipeline = await call

            if (Exit.isFailure(pipeline)) {
              if (!Cause.isFailType(pipeline.cause)) {
                console.error('catching error on newPost route', route, pipeline.cause, req.body)
                res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: pipeline.cause })
              }
            }
          } catch (e) {
            console.error('catching error on newPost route', route, e, req.body)
            res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: e })
          }
        })
      }
      if ('newPutCall' in maRoute) {
        console.info(`PUT ${route}`)
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        rest.put(route, async (req: CaminoRequest, res: express.Response, _next: express.NextFunction) => {
          try {
            const call = Effect.Do.pipe(
              Effect.bind('user', () => {
                if (isNotNullNorUndefined(req.auth)) {
                  return Effect.succeed(req.auth as UserNotNull)
                } else {
                  return Effect.fail({ message: 'Accès interdit', status: HTTP_STATUS.FORBIDDEN })
                }
              }),
              Effect.bind('body', () => zodParseEffect(maRoute.newPut.input, req.body)),
              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
              Effect.mapError(caminoError => {
                if (!('status' in caminoError)) {
                  return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
                }

                return caminoError
              }),
              // TODO 2024-06-26 ici, si on ne met pas le body et params à any, on se retrouve avec une typescript union hell qui fait tout planter
              Effect.bind<'result', { body: any; user: UserNotNull; params: any }, z.infer<(typeof maRoute)['newPut']['output']>, CaminoApiError<string>, never>('result', ({ user, body, params }) =>
                maRoute.newPutCall(
                  Effect.Do.pipe(
                    Effect.let('user', () => user),
                    Effect.let('pool', () => dbPool),
                    Effect.let('params', () => params),
                    Effect.let('body', () => body)
                  )
                )
              ),
              Effect.bind('parsedResult', ({ result }) =>
                pipe(
                  zodParseEffect(maRoute.newPut.output, result),
                  Effect.mapError(caminoError => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }))
                )
              ),
              Effect.tap(({ user }) => addLog(dbPool, user.id, 'post', req.url, req.body)),
              Effect.mapBoth({
                onFailure: caminoError => {
                  console.warn(`problem with route ${route}: ${caminoError.message}`)

                  if (!('status' in caminoError)) {
                    res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json(caminoError)
                  } else {
                    res.status(caminoError.status).json(caminoError)
                  }
                },
                onSuccess: ({ parsedResult }) => {
                  res.json(parsedResult)
                },
              }),
              Effect.runPromiseExit
            )

            const pipeline = await call

            if (Exit.isFailure(pipeline)) {
              if (!Cause.isFailType(pipeline.cause)) {
                console.error('catching error on newPut route', route, pipeline.cause, req.body)
                res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: pipeline.cause })
              }
            }
          } catch (e) {
            console.error('catching error on newPut route', route, e, req.body)
            res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: e })
          }
        })
      }
      if ('putCall' in maRoute) {
        console.info(`PUT ${route}`)
        rest.put(route, restCatcherWithMutation('put', maRoute.putCall(dbPool), dbPool)) // eslint-disable-line @typescript-eslint/no-misused-promises
      }

      if ('deleteCall' in maRoute) {
        console.info(`delete ${route}`)
        rest.delete(route, restCatcherWithMutation('delete', maRoute.deleteCall(dbPool), dbPool)) // eslint-disable-line @typescript-eslint/no-misused-promises
      }
      if ('newDeleteCall' in maRoute) {
        console.info(`DELETE ${route}`)
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        rest.delete(route, async (req: CaminoRequest, res: express.Response, _next: express.NextFunction) => {
          try {
            const call = Effect.Do.pipe(
              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
              Effect.mapError(caminoError => {
                return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
              }),
              // TODO 2024-06-26 ici, si on ne met pas les params et les searchParams à any, on se retrouve avec une typescript union hell qui fait tout planter
              Effect.bind<'result', { params: any }, Option.Option<never>, CaminoApiError<string>, never>('result', ({ params }) => {
                return maRoute.newDeleteCall(
                  Effect.Do.pipe(
                    Effect.let('user', () => req.auth),
                    Effect.let('pool', () => dbPool),
                    Effect.let('params', () => params),
                    Effect.let('cookie', () => ({
                      clearConnectedCookie: () => res.clearCookie('shouldBeConnected'),
                      addConnectedCookie: () => res.cookie('shouldBeConnected', 'anyValueIsGood, We just check the presence of this cookie'),
                    }))
                  )
                )
              }),
              Effect.mapBoth({
                onFailure: caminoError => {
                  console.warn(`problem with route ${route}: ${caminoError.message}`)
                  res.status(caminoError.status).json(caminoError)
                },
                onSuccess: () => {
                  res.sendStatus(HTTP_STATUS.NO_CONTENT)
                },
              }),
              Effect.runPromiseExit
            )

            const pipeline = await call
            if (Exit.isFailure(pipeline)) {
              if (!Cause.isFailType(pipeline.cause)) {
                console.error('catching error on newDelete route', route, pipeline.cause, req.body)
                res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: pipeline.cause })
              }
            }
          } catch (e) {
            console.error('catching error on newDelete route', route, e, req.body)
            res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: e })
          }
        })
      }

      if ('downloadCall' in maRoute) {
        console.info(`download ${route}`)
        rest.get(route, restDownload(maRoute.downloadCall(dbPool))) // eslint-disable-line @typescript-eslint/no-misused-promises
      }

      if ('newDownloadCall' in maRoute) {
        console.info(`newDownload ${route}`)
        rest.get(route, restNewDownload(dbPool, maRoute.newDownloadCall)) // eslint-disable-line @typescript-eslint/no-misused-promises
      }
    })

  rest.use((err: Error | null, _req: CaminoRequest, res: express.Response, next: express.NextFunction) => {
    if (isNotNullNorUndefined(err)) {
      res.status(500)
      res.send({ error: err.message })

      return
    }

    next()
  })

  return rest
}

type ExpressRoute = (req: CaminoRequest, res: express.Response, next: express.NextFunction) => Promise<void>

const restCatcher = (expressCall: ExpressRoute) => async (req: CaminoRequest, res: express.Response, next: express.NextFunction) => {
  try {
    await expressCall(req, res, next)
  } catch (e) {
    console.error('catching error', e)
    next(e)
  }
}
const restCatcherWithMutation = (method: string, expressCall: ExpressRoute, pool: Pool) => async (req: CaminoRequest, res: express.Response, next: express.NextFunction) => {
  const user = req.auth
  try {
    if (!user) {
      res.sendStatus(HTTP_STATUS.FORBIDDEN)
    } else {
      await pipe(addLog(pool, user.id, method, req.url, req.body), Effect.runPromise)
      await expressCall(req, res, next)
    }
  } catch (e) {
    console.error('catching error', e)
    next(e)
  }
}

const restNewDownload = (pool: Pool, resolver: NewDownload) => async (req: CaminoRequest, res: express.Response, next: express.NextFunction) => {
  try {
    const user = req.auth

    const result = await resolver(req.params, user, pool)

    await streamLargeObjectInResponse(pool, res, result.loid, result.fileName)
  } catch (e) {
    console.error(inspect(e, { depth: null }))

    next(e)
  }
}

const restDownload = (resolver: IRestResolver) => async (req: CaminoRequest, res: express.Response, next: express.NextFunction) => {
  try {
    const user = req.auth

    const result = await resolver({ query: req.query, params: req.params }, user)

    if (!result) {
      throw new Error('erreur: aucun résultat')
    }

    const { nom, format, contenu, filePath, buffer } = result

    res.header('Content-disposition', `inline; filename=${encodeURIComponent(nom)}`)
    res.header('Content-Type', contentTypes[format])

    if (isNotNullNorUndefined(filePath) || isNotNullNorUndefined(buffer)) {
      res.header('x-sent', 'true')
      res.header('x-timestamp', Date.now().toString())
      const options: SendFileOptions = {
        dotfiles: 'deny',
        root: join(process.cwd(), 'files'),
      }

      if (isNotNullNorUndefined(filePath)) {
        res.sendFile(filePath, options, err => {
          if (isNotNullNorUndefined(err)) {
            console.error(`erreur de téléchargement ${err}`)
          }
          res.status(404).end()
        })
      }
      if (buffer) {
        res.header('Content-Length', `${buffer.length}`)
        res.send(buffer)
      }
    } else {
      res.send(contenu)
    }
  } catch (e) {
    console.error(inspect(e, { depth: null }))

    next(e)
  }
}
