import { titreArchive, titresGet, titreGet, titreUpdate } from '../../database/queries/titres'
import { HTTP_STATUS } from 'camino-common/src/http'
import { CommonTitreAdministration, TitreLink, TitreLinks, TitreGet, titreGetValidator, SuperTitre } from 'camino-common/src/titres'
import { CaminoRequest, CustomResponse } from './express-type'
import { userSuper } from '../../database/user-super'
import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, onlyUnique } from 'camino-common/src/typescript-tools'
import { CaminoApiError } from '../../types'
import { isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
import TitresTitres from '../../database/models/titres--titres'
import { titreAdministrationsGet } from '../_format/titres'
import { canDeleteTitre, canEditTitre, canLinkTitres } from 'camino-common/src/permissions/titres'
import { linkTitres } from '../../database/queries/titres-titres.queries'
import { checkTitreLinks, CheckTitreLinksError } from '../../business/validations/titre-links-validate'
import { titreEtapeForMachineValidator, toMachineEtapes } from '../../business/rules-demarches/machine-common'
import { DemarchesStatutsIds } from 'camino-common/src/static/demarchesStatuts'
import { ETAPES_TYPES, EtapeTypeId } from 'camino-common/src/static/etapesTypes'
import { CaminoDate, getCurrent } from 'camino-common/src/date'
import { isAdministration, isSuper, User, UserNotNull } from 'camino-common/src/roles'
import { canEditDemarche, canCreateTravaux } from 'camino-common/src/permissions/titres-demarches'
import { titreUpdateTask } from '../../business/titre-update'
import { getDoublonsByTitreId, getTitre as getTitreDb, updateUtilisateurTitre } from './titres.queries'
import type { Pool } from 'pg'
import { TitresStatutIds } from 'camino-common/src/static/titresStatuts'
import { accesSuperSeulementError, getTitresWithBrouillons, GetTitresWithBrouillonsErrors, getTitreUtilisateur } from '../../database/queries/titres-utilisateurs.queries'
import { titreIdValidator, titreIdOrSlugValidator, TitreId } from 'camino-common/src/validators/titres'
import { RestNewGetCall, RestNewPostCall } from '../../server/rest'
import { Effect, Match, pipe } from 'effect'
import { CaminoError } from 'camino-common/src/zod-tools'
import { EffectDbQueryAndValidateErrors } from '../../pg-database'
import { DBTitre } from '../../database/models/titres'
import { AdministrationId } from 'camino-common/src/static/administrations'
import { filterOrFailFromValid } from '../../tools/fp-tools'
import { ApiMachineInfo } from '../../business/rules-demarches/machines'
import { MachineInfo } from 'camino-common/src/machines'

const etapesAMasquer = [ETAPES_TYPES.classementSansSuite, ETAPES_TYPES.desistementDuDemandeur, ETAPES_TYPES.demandeDeComplements_RecevabiliteDeLaDemande_]

type TitresSuperErrors = GetTitresWithBrouillonsErrors
export const titresSuper: RestNewGetCall<'/rest/titresSuper'> = (rootPipe): Effect.Effect<SuperTitre[], CaminoApiError<TitresSuperErrors>> =>
  rootPipe.pipe(
    Effect.filterOrFail(
      ({ user }) => isSuper(user),
      () => ({ message: accesSuperSeulementError })
    ),
    Effect.flatMap(({ pool, user }) => getTitresWithBrouillons(pool, user)),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.whenOr('Seuls les super peuvent accéder à ces données', () => ({
          ...caminoError,
          status: HTTP_STATUS.FORBIDDEN,
        })),
        Match.exhaustive
      )
    )
  )

export const titresAdministrations =
  (_pool: Pool) =>
  async (req: CaminoRequest, res: CustomResponse<CommonTitreAdministration[]>): Promise<void> => {
    const user = req.auth

    if (!user) {
      res.sendStatus(HTTP_STATUS.FORBIDDEN)
    } else {
      if (isAdministration(user)) {
        const filters = {
          statutsIds: [TitresStatutIds.DemandeInitiale, TitresStatutIds.ModificationEnInstance, TitresStatutIds.SurvieProvisoire],
        }

        const titresAutorises = await titresGet(
          filters,
          {
            fields: { pointsEtape: { id: {} }, demarches: { id: {} } },
          },
          user
        )
        const titresAutorisesIds = titresAutorises
          .filter(titre => {
            if (titre.administrationsLocales === undefined) {
              throw new Error('les administrations locales doivent être chargées')
            }

            if (titre.demarches === undefined) {
              throw new Error('les démarches doivent être chargées')
            }

            return (
              canEditTitre(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales ?? []) ||
              canEditDemarche(user, titre.typeId, titre.titreStatutId, titre.administrationsLocales ?? []) ||
              canCreateTravaux(
                user,
                titre.typeId,
                titre.administrationsLocales ?? [],
                titre.demarches.map(({ demarcheDateDebut }) => ({ demarche_date_debut: demarcheDateDebut ?? null }))
              )
            )
          })
          .map(({ id }) => id)
        const titres = await titresGet(
          { ...filters, ids: titresAutorisesIds, colonne: 'nom' },
          {
            fields: {
              titulairesEtape: { id: {} },
              activites: { id: {} },
              demarches: { etapes: { id: {} } },
            },
          },
          userSuper
        )

        const titresFormated: CommonTitreAdministration[] = []
        for (const titre of titres) {
          if (titre.slug !== undefined) {
            if (!titre.titulaireIds) {
              throw new Error('les titulaires ne sont pas chargés')
            }

            if (!titre.references) {
              throw new Error('les références ne sont pas chargées')
            }

            if (!titre.activites) {
              throw new Error('les activités ne sont pas chargées')
            }

            const references = titre.references

            if (!titre.demarches) {
              throw new Error('les démarches ne sont pas chargées')
            }

            const demarcheLaPlusRecente =
              titre.demarches.length > 0 ? titre.demarches.toSorted(({ ordre: ordreA }, { ordre: ordreB }) => (ordreA ?? 0) - (ordreB ?? 0))[titre.demarches.length - 1] : null
            let enAttenteDeAdministration = false
            const prochainesEtapes: EtapeTypeId[] = []
            let derniereEtape: {
              etapeTypeId: EtapeTypeId
              date: CaminoDate
            } | null = null
            if (isNotNullNorUndefined(demarcheLaPlusRecente)) {
              if (isNullOrUndefinedOrEmpty(demarcheLaPlusRecente.etapes)) {
                throw new Error('les étapes ne sont pas chargées')
              }
              if (demarcheLaPlusRecente.statutId !== DemarchesStatutsIds.EnConstruction) {
                const etapes = demarcheLaPlusRecente.etapes.map(etape => titreEtapeForMachineValidator.parse(etape))
                const etapesDerniereDemarche = toMachineEtapes(etapes)
                const machineInfo = MachineInfo.withMachineId(titre.typeId, demarcheLaPlusRecente.typeId, demarcheLaPlusRecente.id, demarcheLaPlusRecente.machineId)
                if (!machineInfo.valid) {
                  throw new Error(`La machine associée à la démarche est incohérente : ${machineInfo.error}`)
                }

                const machine = new ApiMachineInfo(machineInfo.value)
                derniereEtape = etapesDerniereDemarche[etapesDerniereDemarche.length - 1]
                if (isNotNullNorUndefined(machine.machine)) {
                  try {
                    const whoIsBlocking = machine.machine.whoIsBlocking(etapesDerniereDemarche)
                    if (!whoIsBlocking.valid) {
                      throw new Error(whoIsBlocking.error)
                    }
                    enAttenteDeAdministration = whoIsBlocking.value.includes(user.administrationId)
                    const nextEtapes = machine.machine.possibleNextEtapes(etapesDerniereDemarche, getCurrent())
                    if (!nextEtapes.valid) {
                      throw new Error(nextEtapes.error)
                    }
                    prochainesEtapes.push(
                      ...nextEtapes.value
                        .map(etape => etape.etapeTypeId)
                        .filter(onlyUnique)
                        .filter(etape => !etapesAMasquer.includes(etape))
                    )
                  } catch (e) {
                    console.error(`Impossible de traiter le titre ${titre.id} car la démarche ${demarcheLaPlusRecente.typeId} n'est pas valide`, e)
                  }
                }
                titresFormated.push({
                  id: titre.id,
                  slug: titre.slug,
                  nom: titre.nom,
                  titre_statut_id: titre.titreStatutId,
                  type_id: titre.typeId,
                  references,
                  titulaireIds: titre.titulaireIds,
                  enAttenteDeAdministration,
                  derniereEtape,
                  prochainesEtapes,
                })
              }
            } else {
              titresFormated.push({
                id: titre.id,
                slug: titre.slug,
                nom: titre.nom,
                titre_statut_id: titre.titreStatutId,
                type_id: titre.typeId,
                references,
                titulaireIds: titre.titulaireIds,
                enAttenteDeAdministration,
                derniereEtape,
                prochainesEtapes,
              })
            }
          }
        }

        res.json(titresFormated)
      } else {
        res.sendStatus(HTTP_STATUS.FORBIDDEN)
      }
    }
  }

const linkTitreError = 'Droit insuffisant pour lier des titres entre eux' as const
const demarcheNonChargeesError = 'Les démarches ne sont pas chargées' as const
type PostTitreLiaisonErrors = EffectDbQueryAndValidateErrors | typeof droitInsuffisant | typeof linkTitreError | typeof demarcheNonChargeesError | CheckTitreLinksError

export const postTitreLiaisons: RestNewPostCall<'/rest/titres/:id/titreLiaisons'> = (rootPipe): Effect.Effect<TitreLinks, CaminoApiError<PostTitreLiaisonErrors>> => {
  return rootPipe.pipe(
    Effect.bind('titre', ({ user, params }) =>
      Effect.tryPromise({
        try: async () =>
          titreGet(
            params.id,
            {
              fields: {
                pointsEtape: { id: {} },
                demarches: { id: {} },
              },
            },
            user
          ),
        catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
      }).pipe(
        Effect.filterOrFail(
          titre => isNotNullNorUndefined(titre),
          () => ({ message: droitInsuffisant })
        )
      )
    ),
    Effect.bind('administrations', ({ titre }) =>
      Effect.try({
        try: () => titreAdministrationsGet(titre),
        catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
      })
    ),
    Effect.filterOrFail(
      ({ administrations, user }) => canLinkTitres(user, administrations),
      () => ({ message: linkTitreError })
    ),
    Effect.filterOrFail(
      ({ titre }) => isNotNullNorUndefined(titre.demarches),
      () => ({ message: demarcheNonChargeesError })
    ),
    Effect.bind('titresFrom', ({ body, user }) => {
      if (body.length > 0) {
        return Effect.tryPromise({
          try: () => titresGet({ ids: [...body] }, { fields: { id: {} } }, user),
          catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
        })
      }
      return Effect.succeed([])
    }),
    Effect.tap(({ titre, titresFrom, body }) => filterOrFailFromValid(checkTitreLinks(titre.typeId, body, titresFrom, titre.demarches ?? []))),
    Effect.tap(({ pool, params, body }) => linkTitres(pool, { linkTo: params.id, linkFrom: body })),
    Effect.bind('amont', ({ params, user }) => titreLinksGet(params.id, 'titreFromId', user)),
    Effect.bind('aval', ({ params, user }) => titreLinksGet(params.id, 'titreToId', user)),
    Effect.map(({ amont, aval }) => {
      const result: TitreLinks = { amont, aval }

      return result
    }),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", demarcheNonChargeesError, 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.whenOr('Droit insuffisant pour accéder au titre ou titre inexistant', linkTitreError, 'droits insuffisants ou titre inexistant', () => ({
          ...caminoError,
          status: HTTP_STATUS.FORBIDDEN,
        })),
        Match.whenOr('ce titre peut avoir un seul titre lié', 'ce titre ne peut pas être lié à d’autres titres', 'lien incompatible entre ces types de titre', () => ({
          ...caminoError,
          status: HTTP_STATUS.BAD_REQUEST,
        })),
        Match.exhaustive
      )
    )
  )
}

const titreLinksGet = (titreId: string, link: 'titreToId' | 'titreFromId', user: User): Effect.Effect<TitreLink[], CaminoError<EffectDbQueryAndValidateErrors>> => {
  return pipe(
    Effect.tryPromise({
      try: async () => TitresTitres.query().where(link === 'titreToId' ? 'titreFromId' : 'titreToId', titreId),
      catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
    }),
    Effect.map(titresTitres => titresTitres.map(t => t[link])),
    Effect.flatMap(titreIds =>
      Effect.tryPromise({
        try: async () => {
          if (isNullOrUndefinedOrEmpty(titreIds)) {
            return []
          } else {
            return titresGet({ ids: titreIds }, { fields: { id: {} } }, user)
          }
        },
        catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
      })
    ),
    Effect.map(titres => titres.map(({ id, nom }) => ({ id, nom })))
  )
}

const droitInsuffisant = 'Droit insuffisant pour accéder au titre ou titre inexistant' as const
type GetTitreLiaisonErrors = EffectDbQueryAndValidateErrors | typeof droitInsuffisant
export const getTitreLiaisons: RestNewGetCall<'/rest/titres/:id/titreLiaisons'> = (rootPipe): Effect.Effect<TitreLinks, CaminoApiError<GetTitreLiaisonErrors>> => {
  return rootPipe.pipe(
    Effect.bind('titre', ({ params, user }) =>
      Effect.tryPromise({
        try: async () => titreGet(params.id, { fields: { id: {} } }, user),
        catch: e => ({ message: "Impossible d'exécuter la requête dans la base de données" as const, extra: e }),
      })
    ),
    Effect.filterOrFail(
      titre => isNotNullNorUndefined(titre),
      () => ({ message: droitInsuffisant })
    ),
    Effect.bind('amont', ({ params, user }) => titreLinksGet(params.id, 'titreFromId', user)),
    Effect.bind('aval', ({ params, user }) => titreLinksGet(params.id, 'titreToId', user)),
    Effect.map(({ amont, aval }) => {
      const value: TitreLinks = { amont, aval }

      return value
    }),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.when('Droit insuffisant pour accéder au titre ou titre inexistant', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
        Match.exhaustive
      )
    )
  )
}

export const removeTitre =
  (pool: Pool) =>
  async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
    const user = req.auth

    const titreId = titreIdValidator.safeParse(req.params.titreId)
    if (!titreId.success) {
      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
    } else {
      const titreOld = await titreGet(
        titreId.data,
        {
          fields: {
            demarches: { etapes: { id: {} } },
            activites: { id: {} },
          },
        },
        user
      )

      if (!titreOld) {
        res.sendStatus(HTTP_STATUS.NOT_FOUND)
      } else if (!canDeleteTitre(user)) {
        res.sendStatus(HTTP_STATUS.FORBIDDEN)
      } else {
        await titreArchive(titreId.data)
        await titreUpdateTask(pool, titreId.data)
        if (isNotNullNorUndefined(titreOld.doublonTitreId)) {
          await titreUpdateTask(pool, titreOld.doublonTitreId)
        }

        const doublonIds: TitreId[] = await getDoublonsByTitreId(pool, titreId.data)
        if (isNotNullNorUndefinedNorEmpty(doublonIds)) {
          for (const id of doublonIds) {
            await titreUpdateTask(pool, id)
          }
        }
        res.sendStatus(HTTP_STATUS.NO_CONTENT)
      }
    }
  }

const erreurLorsDeLaRecuperationDuTitre = 'Erreur lors de la récupération du titre' as const
type UtilisateurTitreAbonnerErrors = typeof erreurLorsDeLaRecuperationDuTitre | EffectDbQueryAndValidateErrors
export const utilisateurTitreAbonner: RestNewPostCall<'/rest/titres/:titreId/abonne'> = (rootPipe): Effect.Effect<{ id: TitreId; abonne: boolean }, CaminoApiError<UtilisateurTitreAbonnerErrors>> =>
  rootPipe.pipe(
    Effect.bind('titre', ({ user, params }) =>
      Effect.tryPromise({
        try: () => titreGet(params.titreId, { fields: { id: {} } }, user),
        catch: e => ({ message: erreurLorsDeLaRecuperationDuTitre, extra: e }),
      })
    ),
    Effect.tap(({ pool, params, body, user }) => updateUtilisateurTitre(pool, body.abonne, params.titreId, user.id)),
    Effect.map(({ body, params }) => ({ id: params.titreId, abonne: body.abonne })),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr('Erreur lors de la récupération du titre', "Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.exhaustive
      )
    )
  )

type GetUtilisateurTitreAbonnerError = EffectDbQueryAndValidateErrors | typeof droitInsuffisant
export const getUtilisateurTitreAbonner: RestNewGetCall<'/rest/titres/:titreId/abonne'> = (rootPipe): Effect.Effect<boolean, CaminoApiError<GetUtilisateurTitreAbonnerError>> =>
  rootPipe.pipe(
    Effect.filterOrFail(
      (value): value is typeof value & { user: UserNotNull } => isNotNullNorUndefined(value.user),
      () => ({ message: droitInsuffisant })
    ),
    Effect.flatMap(({ user, pool, params }) => getTitreUtilisateur(pool, params.titreId, user.id)),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.when('Droit insuffisant pour accéder au titre ou titre inexistant', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
        Match.exhaustive
      )
    )
  )

const incoherenceTitreId = "l'id du titre doit être le même que sa route" as const
const recuperationTitreImpossible = 'Impossible de récupérer le titre' as const
const titreNonTrouve = 'Titre non trouvé' as const
const administrationsNonChargees = 'Administrations non chargées' as const
const droitsInsuffisantsPourModifierLeTitre = 'Droits insuffisants pour modifier le titre' as const
const erreurPendantLaMiseAJourDuTitre = 'Erreur durant la mise à jour du titre' as const
type UpdateTitreErrors =
  | typeof incoherenceTitreId
  | typeof recuperationTitreImpossible
  | typeof titreNonTrouve
  | typeof administrationsNonChargees
  | typeof droitsInsuffisantsPourModifierLeTitre
  | typeof erreurPendantLaMiseAJourDuTitre
export const updateTitre: RestNewPostCall<'/rest/titres/:titreId'> = (rootPipe): Effect.Effect<{ id: TitreId }, CaminoApiError<UpdateTitreErrors>> =>
  rootPipe.pipe(
    Effect.filterOrFail(
      ({ body, params }) => params.titreId === body.id,
      () => ({ message: incoherenceTitreId })
    ),
    Effect.bind('titreOld', ({ user, params }) =>
      Effect.tryPromise({
        try: () => titreGet(params.titreId, { fields: { pointsEtape: { id: {} } } }, user),
        catch: e => ({ message: recuperationTitreImpossible, extra: e }),
      }).pipe(
        Effect.filterOrFail(
          titreOld => isNotNullNorUndefined(titreOld),
          () => ({ message: titreNonTrouve })
        ),
        Effect.filterOrFail(
          (titreOld): titreOld is DBTitre & { administrationsLocales: AdministrationId[] } => isNotNullNorUndefined(titreOld.administrationsLocales),
          () => ({ message: administrationsNonChargees })
        )
      )
    ),
    Effect.filterOrFail(
      ({ user, titreOld }) => canEditTitre(user, titreOld.typeId, titreOld.titreStatutId, titreOld.administrationsLocales),
      () => ({ message: droitsInsuffisantsPourModifierLeTitre })
    ),
    Effect.tap(({ body, pool }) =>
      Effect.tryPromise({
        try: async () => {
          await titreUpdate(body.id, body)

          await titreUpdateTask(pool, body.id)
        },
        catch: e => ({ message: erreurPendantLaMiseAJourDuTitre, extra: e }),
      })
    ),
    Effect.map(({ body }) => ({ id: body.id })),
    Effect.mapError(caminoError =>
      Match.value(caminoError.message).pipe(
        Match.when('Titre non trouvé', () => ({ ...caminoError, status: HTTP_STATUS.NOT_FOUND })),
        Match.whenOr('Impossible de récupérer le titre', 'Erreur durant la mise à jour du titre', () => ({
          ...caminoError,
          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
        })),
        Match.whenOr("l'id du titre doit être le même que sa route", 'Administrations non chargées', 'Droits insuffisants pour modifier le titre', () => ({
          ...caminoError,
          status: HTTP_STATUS.BAD_REQUEST,
        })),
        Match.exhaustive
      )
    )
  )

export const getTitre =
  (pool: Pool) =>
  async (req: CaminoRequest, res: CustomResponse<TitreGet>): Promise<void> => {
    const titreId: string | undefined = req.params.titreId
    const user = req.auth
    if (!titreId) {
      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
    } else {
      try {
        const titre = await getTitreDb(pool, user, titreIdOrSlugValidator.parse(titreId))

        if (titre === null) {
          res.sendStatus(HTTP_STATUS.NOT_FOUND)
        } else {
          const titreStripped = titreGetValidator.parse(titre)
          res.json(titreStripped)
        }
      } catch (e) {
        console.error(e)

        res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
      }
    }
  }
