import { Pool } from 'pg'
import { getDemarcheByIdOrSlug, GetDemarcheByIdOrSlugErrors, getDemarches, getEtapesByDemarcheId, getFirstEtapeDateByDemarcheIdOrSlug } from '../api/rest/demarches.queries'
import { Effect, pipe } from 'effect'
import { CaminoError } from 'camino-common/src/zod-tools'
import { DemarcheId } from 'camino-common/src/demarche'
import { isEtapeComplete } from 'camino-common/src/permissions/titres-etapes'
import { userSuper } from '../database/user-super'
import { titreEtapeGet, titreEtapeUpsert } from '../database/queries/titres-etapes'
import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, isNullOrUndefinedOrEmpty, memoize, toSorted } from 'camino-common/src/typescript-tools'
import { iTitreEtapeToFlattenEtape, TitreEtapeToFlattenEtapeErrors } from '../api/_format/titres-etapes'
import { ITitreEtape } from '../types'
import {
  getDocumentsByEtapeId,
  getEntrepriseDocumentIdsByEtapeId,
  getEtapeAvisLargeObjectIdsByEtapeId,
  GetEtapeAvisLargeObjectIdsByEtapeIdErrors,
  GetEtapeDocumentLargeObjectIdsByEtapeIdErrors,
} from '../database/queries/titres-etapes.queries'
import { getAdministrationsLocales } from 'camino-common/src/administrations'
import { ZodUnparseable } from '../tools/fp-tools'
import { TitreId, TitreSlug } from 'camino-common/src/validators/titres'
import { EtapeId } from 'camino-common/src/etape'
import { isDemarcheTypeProlongations } from 'camino-common/src/static/demarchesTypes'
import { getMostRecentValuePropFromEtapeFondamentaleValide, TitreGetDemarche } from 'camino-common/src/titres'
import { isDemarcheStatutNonStatue, isDemarcheStatutNonValide } from 'camino-common/src/static/demarchesStatuts'
import { getTitre } from '../api/rest/titres.queries'
import { MachineInfo } from 'camino-common/src/machines'

type CheckedProlongation = {
  titre_slug: TitreSlug | null
  demarche_id: DemarcheId
  etape_id: EtapeId | null
  titre_id: TitreId | null
  errors: string[]
}

const demarchePasProlongationError = 'pas une prolongation' as const
const demarcheSansEtapesError = "La démarche ne contient pas d'étapes" as const
const titreEtapeGetError = 'Le fetch de la firstEtape a échoué' as const
type CheckOnePrologationErrors =
  | GetDemarcheByIdOrSlugErrors
  | TitreEtapeToFlattenEtapeErrors
  | GetEtapeDocumentLargeObjectIdsByEtapeIdErrors
  | ZodUnparseable
  | GetEtapeAvisLargeObjectIdsByEtapeIdErrors
  | typeof titreEtapeGetError
  | typeof demarcheSansEtapesError
  | typeof demarchePasProlongationError
const checkOneProlongation = (pool: Pool, demarcheId: DemarcheId): Effect.Effect<CheckedProlongation, CaminoError<CheckOnePrologationErrors>> => {
  return Effect.Do.pipe(
    Effect.bind('demarche', () => getDemarcheByIdOrSlug(pool, demarcheId)),
    Effect.filterOrFail(
      ({ demarche }) => isDemarcheTypeProlongations(demarche.demarche_type_id),
      () => ({ message: demarchePasProlongationError })
    ),
    Effect.bind('etapes', () => getEtapesByDemarcheId(pool, demarcheId)),
    Effect.filterOrFail(
      ({ etapes }) => isNotNullNorUndefinedNorEmpty(etapes),
      () => ({ message: demarcheSansEtapesError, extra: {} })
    ),
    Effect.bind('firstEtape', ({ etapes }) =>
      pipe(
        Effect.tryPromise({
          try: () => titreEtapeGet(toSorted(etapes, (a, b) => a.ordre - b.ordre)[0].id, { fields: { id: {} }, fetchHeritage: true }, userSuper),
          catch: error => ({ message: titreEtapeGetError, extra: error }),
        }),
        Effect.filterOrFail(
          (firstEtape): firstEtape is ITitreEtape => isNotNullNorUndefined(firstEtape),
          () => ({ message: titreEtapeGetError, extra: 'La first étape est null ou undefined' })
        )
      )
    ),
    Effect.let('titreProps', ({ demarche, firstEtape }) => ({
      titreTypeId: memoize(() => Promise.resolve(demarche.titre_type_id)),
      administrationsLocales: memoize(() =>
        Promise.resolve(getAdministrationsLocales(isNotNullNorUndefined(firstEtape.communes) ? firstEtape.communes.map(({ id }) => id) : [], firstEtape.secteursMaritime))
      ),
      entreprisesTitulairesOuAmodiataires: memoize(() => Promise.resolve([...(firstEtape.titulaireIds ?? []), ...(firstEtape.amodiataireIds ?? [])])),
    })),
    Effect.bind('etapeDocuments', ({ firstEtape, demarche, titreProps }) => {
      return getDocumentsByEtapeId(firstEtape.id, pool, userSuper, titreProps.titreTypeId, titreProps.administrationsLocales, titreProps.entreprisesTitulairesOuAmodiataires, firstEtape.typeId, {
        demarche_type_id: demarche.demarche_type_id,
        entreprises_lecture: demarche.entreprises_lecture,
        public_lecture: demarche.public_lecture,
        titre_public_lecture: false,
      })
    }),
    Effect.bind('entrepriseDocuments', ({ firstEtape }) => getEntrepriseDocumentIdsByEtapeId({ titre_etape_id: firstEtape.id }, pool, userSuper)),
    Effect.bind('etapeAvis', ({ firstEtape, demarche, titreProps }) =>
      getEtapeAvisLargeObjectIdsByEtapeId(
        firstEtape.id,
        pool,
        userSuper,
        titreProps.titreTypeId,
        titreProps.administrationsLocales,
        titreProps.entreprisesTitulairesOuAmodiataires,
        firstEtape.typeId,
        {
          demarche_type_id: demarche.demarche_type_id,
          entreprises_lecture: demarche.entreprises_lecture,
          public_lecture: demarche.public_lecture,
          titre_public_lecture: false,
        }
      )
    ),
    Effect.bind('firstEtapeDate', ({ demarche }) =>
      getFirstEtapeDateByDemarcheIdOrSlug(demarche.demarche_id, pool).pipe(
        Effect.filterOrFail(
          date => isNotNullNorUndefined(date),
          () => ({ message: 'FirstEtapeDate null' })
        )
      )
    ),
    Effect.let('machineInfo', ({ demarche, firstEtapeDate }) => MachineInfo.withDate(demarche.titre_type_id, demarche.demarche_type_id, demarcheId, firstEtapeDate)),
    Effect.bind('flattenFirstEtape', ({ firstEtape, machineInfo }) => iTitreEtapeToFlattenEtape(machineInfo, firstEtape)),
    Effect.map(({ demarche, firstEtape, flattenFirstEtape, etapeDocuments, entrepriseDocuments, etapeAvis, machineInfo }) => {
      const isFirstEtapeComplete = isEtapeComplete(
        flattenFirstEtape,
        machineInfo,
        etapeDocuments,
        entrepriseDocuments,
        firstEtape.sdomZones,
        isNotNullNorUndefined(firstEtape.communes) ? firstEtape.communes.map(({ id }) => id) : [],
        etapeAvis,
        userSuper
      )

      return {
        titre_slug: demarche.titre_slug,
        demarche_id: demarche.demarche_id,
        etape_id: firstEtape.id,
        titre_id: demarche.titre_id,
        errors: isFirstEtapeComplete.valid ? [] : isFirstEtapeComplete.errors,
      }
    }),
    Effect.catchAll(caminoError => {
      return Effect.succeed({
        titre_slug: null,
        demarche_id: demarcheId,
        etape_id: null,
        titre_id: null,
        errors: [caminoError.message],
      })
    })
  )
}

const titreIdVideError = "Pas d'id de titre" as const
const upsertFailError = "Échec de mise à jour de l'étape" as const
const titreGetError = 'Impossible de fetch le titre' as const
const titreGetIsNullError = 'Titre introuvable' as const
const etapeIntrouvableError = 'Étape introuvable dans le titre' as const
const demarcheListIsEmptyError = 'Liste des démarches héritables vide' as const
type FixProlongationErrors = typeof titreIdVideError | typeof upsertFailError | typeof titreGetError | typeof titreGetIsNullError | typeof demarcheListIsEmptyError | typeof etapeIntrouvableError
const fixProlongation = (pool: Pool, checkedProlongation: CheckedProlongation): Effect.Effect<null, CaminoError<FixProlongationErrors>> => {
  return Effect.Do.pipe(
    Effect.bind('titre', () =>
      pipe(
        Effect.succeed(checkedProlongation.titre_id),
        Effect.filterOrFail(
          titreId => isNotNullNorUndefined(titreId),
          () => ({ message: titreIdVideError })
        ),
        Effect.flatMap(titreId =>
          Effect.tryPromise({
            try: () => getTitre(pool, userSuper, titreId),
            catch: error => ({ message: titreGetError, extra: error }),
          })
        ),
        Effect.filterOrFail(
          (titre): titre is NonNullable<typeof titre> => isNotNullNorUndefined(titre),
          error => ({ message: titreGetIsNullError, extra: error })
        )
      )
    ),
    Effect.bind('etape', ({ titre }) =>
      pipe(
        Effect.succeed(titre.demarches.find(({ id }) => id === checkedProlongation.demarche_id)),
        Effect.filterOrFail(
          (demarche): demarche is NonNullable<typeof demarche> => isNotNullNorUndefined(checkedProlongation.demarche_id) && isNotNullNorUndefined(demarche),
          () => ({ message: etapeIntrouvableError, extra: 'Démarche introuvable' })
        ),
        Effect.map(demarche => demarche.etapes.find(({ id }) => id === checkedProlongation.etape_id)),
        Effect.filterOrFail(
          (etape): etape is NonNullable<typeof etape> => isNotNullNorUndefined(checkedProlongation.etape_id) && isNotNullNorUndefined(etape),
          () => ({ message: etapeIntrouvableError, extra: 'Étape introuvable' })
        )
      )
    ),
    Effect.bind('propsHeritees', ({ titre }) =>
      Effect.Do.pipe(
        Effect.flatMap(() => {
          const demarches: TitreGetDemarche[] = []
          for (const demarche of toSorted([...titre.demarches], (a, b) => a.ordre - b.ordre)) {
            if (demarche.id === checkedProlongation.demarche_id) {
              return Effect.succeed([...demarches, demarche])
            } else if (!isDemarcheStatutNonStatue(demarche.demarche_statut_id) && !isDemarcheStatutNonValide(demarche.demarche_statut_id)) {
              demarches.push(demarche)
            }
          }

          return Effect.fail({ message: demarcheListIsEmptyError })
        }),
        Effect.map(demarches => {
          return {
            substances: getMostRecentValuePropFromEtapeFondamentaleValide('substances', demarches),
            titulaireIds: getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', demarches),
            perimetre: getMostRecentValuePropFromEtapeFondamentaleValide('perimetre', demarches),
            duree: getMostRecentValuePropFromEtapeFondamentaleValide('duree', [...demarches.filter(demarche => demarche.id === checkedProlongation.demarche_id)]),
          }
        })
      )
    ),
    Effect.flatMap(({ titre, etape, propsHeritees }) =>
      Effect.tryPromise({
        try: () => {
          const changes = []
          let duree = 'fondamentale' in etape ? etape.fondamentale.duree : null
          if ((isNullOrUndefined(duree) || duree === 0) && isNotNullNorUndefined(propsHeritees.duree) && propsHeritees.duree !== 0) {
            duree = propsHeritees.duree
            changes.push('duree')
          }

          let substances = 'fondamentale' in etape ? (etape.fondamentale.substances ?? []) : []
          if (isNullOrUndefinedOrEmpty(substances) && isNotNullNorUndefinedNorEmpty(propsHeritees.substances)) {
            substances = propsHeritees.substances
            changes.push('substances')
          }

          let titulaireIds = 'fondamentale' in etape ? (etape.fondamentale.titulaireIds ?? []) : []
          if (isNullOrUndefinedOrEmpty(titulaireIds) && isNotNullNorUndefinedNorEmpty(propsHeritees.titulaireIds)) {
            titulaireIds = propsHeritees.titulaireIds
            changes.push('titulaireIds')
          }
          let perimetre =
            'fondamentale' in etape
              ? {
                  surface: etape.fondamentale.perimetre?.surface,
                  geojson4326Perimetre: etape.fondamentale.perimetre?.geojson4326_perimetre,
                  geojsonOriginePerimetre: etape.fondamentale.perimetre?.geojson_origine_perimetre,
                  geojsonOrigineGeoSystemeId: etape.fondamentale.perimetre?.geojson_origine_geo_systeme_id,
                  geojson4326Points: etape.fondamentale.perimetre?.geojson4326_points,
                  geojsonOriginePoints: etape.fondamentale.perimetre?.geojson_origine_points,
                  geojson4326Forages: etape.fondamentale.perimetre?.geojson4326_forages,
                  geojsonOrigineForages: etape.fondamentale.perimetre?.geojson_origine_forages,
                }
              : null
          if (isNullOrUndefined(perimetre?.geojsonOriginePerimetre) && isNotNullNorUndefined(propsHeritees.perimetre?.geojson_origine_perimetre)) {
            perimetre = {
              surface: propsHeritees.perimetre.surface,
              geojson4326Perimetre: propsHeritees.perimetre.geojson4326_perimetre,
              geojsonOriginePerimetre: propsHeritees.perimetre.geojson_origine_perimetre,
              geojsonOrigineGeoSystemeId: propsHeritees.perimetre.geojson_origine_geo_systeme_id,
              geojson4326Points: propsHeritees.perimetre.geojson4326_points,
              geojsonOriginePoints: propsHeritees.perimetre.geojson_origine_points,
              geojson4326Forages: propsHeritees.perimetre.geojson4326_forages,
              geojsonOrigineForages: propsHeritees.perimetre.geojson_origine_forages,
            }
            changes.push('perimetre')
          }

          if (isNotNullNorUndefinedNorEmpty(changes)) {
            console.info(`Trying to fix http://localhost:4180/etapes/${etape.slug} -> ${changes.join(',')}`)
          }

          return titreEtapeUpsert(
            {
              id: etape.id,
              titreDemarcheId: checkedProlongation.demarche_id,
              date: etape.date,
              duree,
              substances,
              titulaireIds,
              ...perimetre,
            },
            userSuper,
            titre.id
          )
        },
        catch: error => ({ message: upsertFailError, extra: error }),
      })
    ),
    Effect.map(() => null),
    Effect.catchAll(error => {
      console.info(error)
      return Effect.succeed(null)
    })
  )
}

const toPercentage = (percentage: number): string => {
  return `${Math.round(percentage * 10000) / 100} %`
}

export const checkProlongations = (pool: Pool): Effect.Effect<CheckedProlongation[], CaminoError<CheckOnePrologationErrors | FixProlongationErrors>> => {
  return pipe(
    getDemarches(pool),
    Effect.flatMap(demarches => {
      const initialAcc: CheckedProlongation[] = []
      return Effect.reduce(
        demarches.map(({ id }) => id),
        initialAcc,
        (acc, demarcheId) => {
          return pipe(
            checkOneProlongation(pool, demarcheId),
            Effect.tap(checkedProlongation => {
              if (isNullOrUndefinedOrEmpty(checkedProlongation.errors) || checkedProlongation.errors.includes(demarchePasProlongationError)) {
                return Effect.succeed(checkedProlongation)
              }

              return fixProlongation(pool, checkedProlongation)
            }),
            Effect.map(checkedProlongation => {
              return [...acc, checkedProlongation]
            })
          )
        }
      )
    }),
    Effect.tap(checkedProlongation => {
      const prolongations = checkedProlongation.filter(({ errors }) => !errors.includes(demarchePasProlongationError))
      const prolongationsEnVrac = prolongations.filter(({ errors }) => errors.length > 0)
      const prolongationsAvecDureeManquante = prolongations.filter(({ errors }) => errors.some(error => error.includes('la durée est obligatoire')))

      const prolongationsParType = prolongationsEnVrac.reduce<{ [key: string]: number }>((acc, prolongation) => {
        const domaine = isNotNullNorUndefined(prolongation.titre_slug) ? `${prolongation.titre_slug.slice(2, 4)}${prolongation.titre_slug.slice(0, 1)}` : 'inconnu'
        if (isNullOrUndefined(acc[domaine])) {
          acc[domaine] = 1
        } else {
          acc[domaine] += 1
        }

        return acc
      }, {})
      console.info(`Prolongations en vrac : ${prolongationsEnVrac.length} (${toPercentage(prolongationsEnVrac.length / prolongations.length)})`)
      console.info(JSON.stringify(prolongationsParType, null, 2))
      console.info(`Prolongations sans durée : ${prolongationsAvecDureeManquante.length} (${toPercentage(prolongationsAvecDureeManquante.length / prolongationsEnVrac.length)})`)
    })
  )
}
