// valide la date et la position de l'étape en fonction des autres étapes
import { NonEmptyArray, isNonEmptyArray, isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
import type { ITitreEtape } from '../../types'

import { Etape, TitreEtapeForMachine, titreEtapeForMachineValidator, toMachineEtapes } from '../rules-demarches/machine-common'
import { ETAPE_IS_BROUILLON, ETAPE_IS_NOT_BROUILLON, EtapeId, EtapeTypeEtapeStatutWithMainStep } from 'camino-common/src/etape'
import { DemarcheId } from 'camino-common/src/demarche'
import { ApiMachineInfo, CaminoMachines, demarcheEnregistrementDemandeDateFind } from '../rules-demarches/machines'
import { CaminoDate } from 'camino-common/src/date'
import { EtapesTypes, EtapeTypeId } from 'camino-common/src/static/etapesTypes'
import { titreEtapesSortAscByOrdre } from '../utils/titre-etapes-sort'
import { getEtapesTDE, isTDEExist } from 'camino-common/src/static/titresTypes_demarchesTypes_etapesTypes/index'
import { etapeTypeDateFinCheck } from '../../api/_format/etapes-types'
import { getEtapesStatuts } from 'camino-common/src/static/etapesTypesEtapesStatuts'
import { MachineInfo } from 'camino-common/src/machines'
import { DemarchesTypes } from 'camino-common/src/static/demarchesTypes'

const titreDemarcheEtapesBuild = <T extends Pick<Partial<ITitreEtape>, 'id'>>(titreEtape: T, suppression: boolean, titreDemarcheEtapes?: T[] | null): T[] => {
  if (isNullOrUndefinedOrEmpty(titreDemarcheEtapes)) {
    return [titreEtape]
  }

  // si nous n’ajoutons pas une nouvelle étape
  // on supprime l’étape en cours de modification ou de suppression
  const titreEtapes = titreDemarcheEtapes.reduce((acc: T[], te) => {
    if (te.id !== titreEtape.id) {
      acc = [...acc, te]
    }

    // modification
    if (!suppression && te.id === titreEtape.id) {
      acc = [...acc, titreEtape]
    }

    return acc
  }, [])

  // création
  if (!titreEtape.id) {
    return [...titreEtapes, titreEtape]
  }

  return titreEtapes
}

// vérifie que la modification de la démarche
// est valide par rapport aux définitions des types d'étape
export const titreDemarcheUpdatedEtatValidate = (
  machineInfo: MachineInfo,
  demarches: { id: DemarcheId }[],
  titreEtape: Pick<Partial<ITitreEtape>, 'id'> &
    Pick<
      ITitreEtape,
      'statutId' | 'typeId' | 'date' | 'contenu' | 'surface' | 'communes' | 'isBrouillon' | 'hasTitreFrom' | 'concurrence' | 'demarcheIdsConsentement' | 'dateDebut' | 'dateFin' | 'duree'
    >,
  titreDemarcheEtapes?:
    | Pick<
        ITitreEtape,
        | 'id'
        | 'statutId'
        | 'typeId'
        | 'date'
        | 'ordre'
        | 'contenu'
        | 'communes'
        | 'surface'
        | 'isBrouillon'
        | 'hasTitreFrom'
        | 'concurrence'
        | 'demarcheIdsConsentement'
        | 'dateDebut'
        | 'dateFin'
        | 'duree'
      >[]
    | null,
  suppression = false
): { valid: true; errors: null } | { valid: false; errors: NonEmptyArray<string> } => {
  const titreDemarcheEtapesNew = titreDemarcheEtapesBuild(titreEtape, suppression, titreDemarcheEtapes)
  let machine: ApiMachineInfo
  if (isNotNullNorUndefinedNorEmpty(titreDemarcheEtapesNew)) {
    const firstEtapeDate = demarcheEnregistrementDemandeDateFind(titreDemarcheEtapesNew)
    machine = new ApiMachineInfo(MachineInfo.withDate(machineInfo.titreTypeId, machineInfo.demarcheTypeId, machineInfo.demarcheId, firstEtapeDate))
  } else {
    return { valid: true, errors: null }
  }

  const titreDemarchesErrors: string[] = []

  // vérifie que la démarche existe dans le titre
  if (!demarches.some(({ id }) => id === machineInfo.demarcheId)) {
    titreDemarchesErrors.push('le titre ne contient pas la démarche en cours de modification')
  }
  // on récupère tous les type d'étapes et les statuts associés applicable à la date souhaitée
  try {
    const etapeTypesWithStatusPossibles = getPossiblesEtapesTypes(machine, titreEtape.typeId, titreEtape.id, titreEtape.date, titreDemarcheEtapes ?? [])
    const statutPossiblesPourCetteEtape = etapeTypesWithStatusPossibles[titreEtape.typeId]
    if (isNullOrUndefined(statutPossiblesPourCetteEtape) || !statutPossiblesPourCetteEtape.etapeStatutIds.includes(titreEtape.statutId)) {
      if (isNotNullNorUndefined(machine.machine)) {
        return { valid: false, errors: [`les étapes de la démarche machine ${machine.machineId} ne sont pas valides`] }
      } else {
        return { valid: false, errors: ['les étapes de la démarche TDE ne sont pas valides'] }
      }
    }
  } catch (e: any) {
    console.warn('une erreur est survenue', e)
    titreDemarchesErrors.push(e.message)
  }

  if (isNotNullNorUndefined(machine.machine)) {
    // vérifie que toutes les étapes existent dans l’arbre
    try {
      const ok = machine.machine.isEtapesOk(
        machine.machine.orderMachine(
          toMachineEtapes(
            titreDemarcheEtapesNew.map(etape => ({
              ...etape,
              dateDebut: etape.dateDebut ?? null,
              dateFin: etape.dateFin ?? null,
              duree: etape.duree ?? null,
              surface: etape.surface ?? null,
              communes: etape.communes ?? null,
            }))
          )
        )
      )
      if (!ok) {
        titreDemarchesErrors.push('la démarche machine n’est pas valide')
      }
    } catch (e) {
      console.warn('une erreur est survenue', e)
      titreDemarchesErrors.push('la démarche n’est pas valide')
    }
  }

  if (isNonEmptyArray(titreDemarchesErrors)) {
    return { valid: false, errors: titreDemarchesErrors }
  }

  return { valid: true, errors: null }
}

export const getPossiblesEtapesTypes = (
  machine: ApiMachineInfo,
  etapeTypeId: EtapeTypeId | undefined,
  etapeId: EtapeId | undefined,
  date: CaminoDate,
  demarcheEtapes: Pick<ITitreEtape, 'typeId' | 'date' | 'isBrouillon' | 'id' | 'ordre' | 'statutId' | 'communes'>[]
): EtapeTypeEtapeStatutWithMainStep => {
  let etapesTypes: EtapeTypeEtapeStatutWithMainStep = {}
  if (isNotNullNorUndefined(machine.machine)) {
    const etapes = demarcheEtapes.map(etape => titreEtapeForMachineValidator.parse(etape))

    etapesTypes = etapesTypesPossibleACetteDateOuALaPlaceDeLEtape(machine.machine, etapes, etapeId ?? null, date)
  } else {
    // si on modifie une étape
    // vérifie que son type est possible sur la démarche
    if (isNotNullNorUndefined(etapeTypeId)) {
      if (!isTDEExist(machine.titreTypeId, machine.demarcheTypeId, etapeTypeId)) {
        const demarcheType = DemarchesTypes[machine.demarcheTypeId]
        throw new Error(`étape ${EtapesTypes[etapeTypeId].nom} inexistante pour une démarche ${demarcheType.nom} pour un titre ${machine.titreTypeId}.`)
      }
    }
    // dans un premier temps on récupère toutes les étapes possibles pour cette démarche
    let etapesTypesTDE = getEtapesTDE(machine.titreTypeId, machine.demarcheTypeId)

    const etapeTypesExistants = demarcheEtapes.map(({ typeId }) => typeId)
    etapesTypesTDE = etapesTypesTDE
      .filter(typeId => etapeTypeId === typeId || !etapeTypesExistants.includes(typeId) || !EtapesTypes[typeId].unique)
      .filter(etapeTypeId => etapeTypeDateFinCheck(etapeTypeId, demarcheEtapes))
    etapesTypes = etapesTypesTDE.reduce<EtapeTypeEtapeStatutWithMainStep>((acc, etapeTypeId) => {
      acc[etapeTypeId] = { etapeStatutIds: getEtapesStatuts(etapeTypeId).map(({ id }) => id), mainStep: false }

      return acc
    }, {})
  }

  // On ne peut pas avoir 2 fois le même type d'étape en brouillon
  const etapeTypeIdsInBrouillon = demarcheEtapes
    .filter(({ isBrouillon, id }) => {
      if (isNullOrUndefined(etapeId)) {
        return isBrouillon === ETAPE_IS_BROUILLON
      } else {
        const etape = demarcheEtapes.find(myEtape => myEtape.id === etapeId)
        if (etape?.isBrouillon === ETAPE_IS_NOT_BROUILLON) {
          return false
        }
        return id !== etapeId && isBrouillon === ETAPE_IS_BROUILLON
      }
    })
    .map(({ typeId }) => typeId)

  for (const etapeTypeIdInBrouillon of etapeTypeIdsInBrouillon) {
    delete etapesTypes[etapeTypeIdInBrouillon]
  }

  return etapesTypes
}

const etapesTypesPossibleACetteDateOuALaPlaceDeLEtape = (machine: CaminoMachines, etapes: TitreEtapeForMachine[], titreEtapeId: string | null, date: CaminoDate): EtapeTypeEtapeStatutWithMainStep => {
  const sortedEtapes = titreEtapesSortAscByOrdre(etapes).filter(etape => etape.id !== titreEtapeId)
  const etapesAvant: Etape[] = []
  const etapesPendant: Etape[] = []

  const etapesApres: Etape[] = []

  etapesAvant.push(...toMachineEtapes(sortedEtapes.filter(etape => etape.date < date)))
  etapesPendant.push(...toMachineEtapes(sortedEtapes.filter(etape => etape.date === date)))
  etapesApres.push(...toMachineEtapes(sortedEtapes.slice(etapesAvant.length + etapesPendant.length)))

  if (!machine.isEtapesOk(etapesAvant)) {
    return {}
  }
  const etapesPossibles = []

  for (let i = 0; i <= etapesPendant.length; i++) {
    const etapeEnCours = [...etapesAvant, ...etapesPendant.slice(0, i)]
    const etapesPossiblesRaw = machine.possibleNextEtapes(etapeEnCours, date)
    if (etapesPossiblesRaw.valid) {
      for (const et of etapesPossiblesRaw.value) {
        const newEtapes = [...etapeEnCours]

        const items = { ...et, date }
        newEtapes.push(items)
        newEtapes.push(...etapesPendant.slice(i))

        newEtapes.push(...etapesApres)

        if (machine.isEtapesOk(newEtapes)) {
          etapesPossibles.push(et)
        }
      }
    }
  }

  return etapesPossibles.reduce<EtapeTypeEtapeStatutWithMainStep>((acc, { etapeTypeId, etapeStatutId, mainStep }) => {
    if (isNullOrUndefined(acc[etapeTypeId])) {
      acc[etapeTypeId] = { etapeStatutIds: [etapeStatutId], mainStep }
    } else {
      if (!acc[etapeTypeId].etapeStatutIds.includes(etapeStatutId)) {
        acc[etapeTypeId].etapeStatutIds.push(etapeStatutId)
      }
      acc[etapeTypeId].mainStep = acc[etapeTypeId].mainStep || mainStep
    }

    return acc
  }, {})
}
