import { $accessProfile } from '@/services/profile/accessProfile'
import { $organizationProfile } from '@/services/profile/organizationProfile'
import { Ability } from '@casl/ability'
import * as _ from 'lodash'
import { $roleUtils } from '~/services/rights/roleUtils'
import { AbilitiesType, AnyJson, AnyStore } from '~/types/interfaces'
import Serializer from '~/services/serializer/serializer'
import { DENORMALIZER_TYPE } from '~/types/enums'

/**
 * Classe permettant de mener des opérations sur les habilités
 */
class AbilitiesUtils {
  private readonly $store: AnyStore = {} as AnyStore
  private readonly $ability: Ability = {} as Ability
  private factory: AnyJson = {}

  /**
   * @constructor
   */
  constructor (store: AnyStore, ability: Ability) {
    this.$store = store
    this.$ability = ability
  }

  /**
   * Retourne la factory des services
   *
   * @return {AnyJson} factory
   */
  getFactory () {
    return this.factory
  }

  /**
   * Initialise les services factories
   */
  initFactory () {
    this.factory = {
      access: $accessProfile,
      organization: $organizationProfile(this.$store)
    }
    this.setAbilitiesAndStore()
  }

  /**
   * Initialise les Abilities pour le service AccessProfile
   */
  setAbilitiesAndStore(){
    this.factory.access.setAbility(this.$ability, this.$store)
    this.factory.access.setStore(this.$store)
  }

  /**
   * Définit les abilities de l'utilisateur à chaque fois qu'on met à jour son profile
   */
  setAbilities () {
    // Nécessaire pour que l'update des habilités soit correcte après la phase SSR
    this.$ability.update(this.$store.state.profile.access.abilities)

    // Au moment où l'on effectue un SetProfile via le store Organization, il faut aller récupérer
    // les différentes abilitées que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
    const unsubscribe = this.$store.subscribeAction({
      after: (action, _state) => {
        switch (action.type) {
          case 'profile/organization/setProfile':
            //On récupère les abilités
            const abilities: Array<AbilitiesType> = this.getAbilities();

            //On les store puis on update le service ability pour le mettre à jour.
            this.$store.commit('profile/access/setAbilities', abilities)
            this.$ability.update(abilities)
            // Unsubscribe pour éviter les memory leaks
            unsubscribe()
            break
        }
      }
    })
  }

  /**
   * Récupération de l'ensemble des abilities, qu'elles soient par Roles ou par Config
   *
   * @return {Array<AbilitiesType>}
   */
  getAbilities():Array<AbilitiesType> {
    const abilitiesByRoles: Array<AbilitiesType> = this.getAbilitiesByRoles(this.$store.state.profile.access.roles)
    this.$ability.update(abilitiesByRoles)
    this.initFactory()
    return abilitiesByRoles.concat(this.getAbilitiesByConfig('./config/abilities/config.yaml'))
  }

  /**
   * Adaptation et transformations des roles en abilities
   *
   * @param {Array<string>} roles
   * @return {Array<AbilitiesType>}
   */
  getAbilitiesByRoles (roles: Array<string>): Array<AbilitiesType> {
    roles = $roleUtils.transformUnderscoreToHyphenBeforeCompleteMigration(roles)
    return $roleUtils.transformRoleToAbilities(roles)
  }

  /**
   * - Parcourt la config d'abilities en Yaml
   * - filtres la config pour ne garder que les abilities autorisées
   * - transform la config restante en Object Abilities
   * @param {string} configPath
   * @return {Array<AbilitiesType>}
   */
  getAbilitiesByConfig (configPath: string): Array<AbilitiesType> {
    let abilitiesByConfig: Array<AbilitiesType> = []
    try {
      const doc = Serializer.denormalize({ path: configPath }, DENORMALIZER_TYPE.YAML)
      const abilitiesAvailable = doc.abilities
      const abilitiesFiltered = this.abilitiesAvailableFilter(abilitiesAvailable)
      abilitiesByConfig = this.transformAbilitiesConfigToAbility(abilitiesFiltered)
    } catch (e) {
      throw new Error(e.message)
    }
    return abilitiesByConfig
  }

  /**
   * Filtre toutes les abilities possible suivant si l'utilisateur est autorisé ou non à les posséder
   *
   * @param {AnyJson} abilitiesAvailable
   * @return {AnyJson}
   */
  abilitiesAvailableFilter (abilitiesAvailable:AnyJson): AnyJson {
    return _.pickBy(abilitiesAvailable, (ability:any) => {
      const services = ability.services
      return this.canHaveTheAbility(services)
    })
  }

  /**
   * Transform une config d'abilities en un tableau d'Abilities
   *
   * @param {AnyJson} abilitiesAvailable
   * @return {Array<AbilitiesType>}
   */
  transformAbilitiesConfigToAbility (abilitiesAvailable: AnyJson): Array<AbilitiesType> {
    const abilitiesByConfig: Array<AbilitiesType> = []
    _.each(abilitiesAvailable, (ability, subject) => {
      const myAbility: AbilitiesType = {
        action: ability.action,
        subject
      }
      abilitiesByConfig.push(myAbility)
    })
    return abilitiesByConfig
  }

  /**
   * Parcourt les fonctions par services et établit si oui ou non l'habilité est autorisée
   *
   * @param {AnyJson} functionByServices
   * @return {boolean}
   */
  canHaveTheAbility(functionByservices: AnyJson) {
    let hasAbility: boolean = true;
    _.each(functionByservices, (functions, service) => {
      if (hasAbility) {
        const nbFunctions: number = functions.length
        let cmpt: number = 0

        while (hasAbility && nbFunctions > cmpt) {
          const f: string = functions[cmpt]['function'];
          const parameters: any = functions[cmpt]['parameters'] ?? null;
          const result: boolean = functions[cmpt]['result'] ?? true;

          hasAbility = result !== null ? this.factory[service].handler()[f](parameters) == result : this.factory[service].handler()[f](parameters)
          cmpt++
        }
      }
    })
    return hasAbility
  }
}

export const $abilitiesUtils = (store: AnyStore, ability:Ability) => new AbilitiesUtils(store, ability)
