import * as _ from 'lodash-es'
import type { AnyJson, ApiResponse, HydraMetadata } from '~/types/data'
import UrlUtils from '~/services/utils/urlUtils'
import { METADATA_TYPE } from '~/types/enum/data'
import models from '~/models/models'
import ApiResource from '~/models/ApiResource'

/**
 * Normalisation et dé-normalisation du format de données Hydra
 */
class HydraNormalizer {
  static models = models

  /**
   * Normalize the given entity into a Hydra formatted content.
   * @param entity
   */
  public static normalizeEntity(entity: ApiResource): AnyJson {
    const iriEncodedFields =
      Object.getPrototypeOf(entity).constructor.getIriEncodedFields()

    for (const field in iriEncodedFields) {
      const value = entity[field]
      const targetEntity = iriEncodedFields[field].entity

      if (_.isArray(value)) {
        entity[field] = value.map((id: number) => {
          return UrlUtils.makeIRI(targetEntity, id)
        })
      } else {
        entity[field] =
          value !== null ? UrlUtils.makeIRI(targetEntity, value) : null
      }
    }

    return entity.$toJson()
  }

  /**
   * Parse une réponse Hydra et retourne un objet ApiResponse
   *
   * @param {AnyJson} data
   * @param model
   * @return {AnyJson} réponse parsée
   */
  public static denormalize(
    data: AnyJson,
    model?: typeof ApiResource
  ): ApiResponse {
    return {
      data: HydraNormalizer.getData(data, model),
      metadata: HydraNormalizer.getMetadata(data),
    }
  }

  protected static getData(
    hydraData: AnyJson,
    model?: typeof ApiResource
  ): AnyJson {
    if (hydraData['@type'] === 'hydra:Collection') {
      const members = hydraData['hydra:member']
      return members.map((item: AnyJson) =>
        HydraNormalizer.denormalizeItem(item, model)
      )
    } else {
      return HydraNormalizer.denormalizeItem(hydraData, model)
    }
  }

  /**
   * Génère les métadonnées d'un item ou d'une collection
   *
   * @param data
   * @protected
   */
  protected static getMetadata(data: AnyJson): HydraMetadata {
    if (data['@type'] !== 'hydra:Collection') {
      // A single item, no metadata
      return { type: METADATA_TYPE.ITEM }
    }

    const metadata: HydraMetadata = {
      totalItems: data['hydra:totalItems'],
    }

    if (data['hydra:view']) {
      /**
       * Extract the page number from the IRIs in the hydra:view section
       */
      const extractPageNumber = (
        pos: string,
        default_: number | undefined = undefined
      ): number | undefined => {
        const iri = data['hydra:view']['hydra:' + pos]
        if (!iri) {
          return default_
        }
        return UrlUtils.getParameter(
          data['hydra:view']['hydra:' + pos],
          'page',
          default_
        ) as number | undefined
      }

      // TODO: utile d'ajouter la page en cours?
      metadata.firstPage = extractPageNumber('first', 1)
      metadata.lastPage = extractPageNumber('last', 1)
      metadata.nextPage = extractPageNumber('next')
      metadata.previousPage = extractPageNumber('previous')
    }

    metadata.type = METADATA_TYPE.COLLECTION

    return metadata
  }

  /**
   * Dénormalise un item d'une réponse hydra
   *
   * @param item
   * @param model
   * @protected
   */
  protected static denormalizeItem(
    item: AnyJson,
    model?: typeof ApiResource
  ): AnyJson {
    if (model) {
      return HydraNormalizer.denormalizeEntity(model, item)
    }

    if (!Object.prototype.hasOwnProperty.call(item, '@id')) {
      // Not hydra formatted
      console.error(
        'De-normalization error : the item is not hydra formatted',
        item
      )
      return item
    }

    if (item['@id'].match(/\/api\/enum\/\w+/)) {
      return HydraNormalizer.denormalizeEnum(item)
    }

    let entity = null

    // On essaie de déterminer la nature de l'objet à partir de son id
    try {
      const iri = HydraNormalizer.parseEntityIRI(item['@id'])
      entity = iri.entity
    } catch (e) {
      console.error('De-normalization error : could not parse the IRI', item)
      return item
    }

    if (
      entity &&
      Object.prototype.hasOwnProperty.call(HydraNormalizer.models, entity)
    ) {
      model = HydraNormalizer.models[entity]
      return HydraNormalizer.denormalizeEntity(model, item)
    }

    throw new Error(
      'De-normalization error : Could not determine the type of the entity ' +
        item['@id'] +
        ' (found: ' +
        entity +
        ')'
    )
  }

  protected static denormalizeEntity(model: typeof ApiResource, item: AnyJson) {
    // eslint-disable-next-line new-cap
    const instance = new model(item)
    const iriEncodedFields = model.getIriEncodedFields()

    for (const field in iriEncodedFields) {
      const value = instance[field]
      if (_.isEmpty(value)) {
        continue
      }

      const targetEntity = iriEncodedFields[field].entity

      if (_.isArray(value)) {
        instance[field] = value.map((iri: string) => {
          return HydraNormalizer.getIdFromEntityIri(iri, targetEntity)
        })
      } else {
        instance[field] = HydraNormalizer.getIdFromEntityIri(
          value,
          targetEntity
        )
      }
    }

    return instance
  }

  protected static denormalizeEnum(item: AnyJson): AnyJson {
    return item
  }

  /**
   * Parse the given IRI
   * @param iri
   * @protected
   */
  protected static parseEntityIRI(iri: string) {
    const rx = /\/api\/(\w+)\/(\d+)/
    const match = rx.exec(iri)
    if (!match) {
      throw new Error('could not parse the IRI : ' + JSON.stringify(iri))
    }

    return {
      entity: match[1],
      id: parseInt(match[2]),
    }
  }

  /**
   * Retrieve the entitie's id from the given IRI
   * Throws an error if the IRI does not match the expected entity
   *
   * @param iri
   * @param expectedEntity
   * @protected
   */
  protected static getIdFromEntityIri(
    iri: string,
    expectedEntity: string
  ): number | string {
    const { entity, id } = HydraNormalizer.parseEntityIRI(iri)
    if (entity !== expectedEntity) {
      throw new Error(
        "IRI entity does not match the field's target entity (" +
          entity +
          ' != ' +
          expectedEntity +
          ')'
      )
    }
    return id
  }
}

export default HydraNormalizer
