text: 'use strict';
  
  const _ = require('lodash');
  const mongoose = require('mongoose');
  
  const utilsModels = require('strapi-utils').models;
  const utils = require('./utils');
  const relations = require('./relations');
  const { findComponentByGlobalId } = require('./utils/helpers');
  
  const isPolymorphicAssoc = assoc => {
    return assoc.nature.toLowerCase().indexOf('morph') !== -1;
  };
  
  module.exports = ({ models, target }, ctx) => {
    const { instance } = ctx;
  
    function mountModel(model) {
      const definition = models[model];
      definition.orm = 'mongoose';
      definition.associations = [];
      definition.globalName = _.upperFirst(_.camelCase(definition.globalId));
      definition.loadedModel = {};
      // Set the default values to model settings.
      _.defaults(definition, {
        primaryKey: '_id',
        primaryKeyType: 'string',
      });
  
      const componentAttributes = Object.keys(definition.attributes).filter(key =>
        ['component', 'dynamiczone'].includes(definition.attributes[key].type)
      );
  
      const scalarAttributes = Object.keys(definition.attributes).filter(key => {
        const { type } = definition.attributes[key];
        return type !== undefined && type !== null && type !== 'component' && type !== 'dynamiczone';
      });
  
      const relationalAttributes = Object.keys(definition.attributes).filter(key => {
        const { type } = definition.attributes[key];
        return type === undefined;
      });
  
      // handle component and dynamic zone attrs
      if (componentAttributes.length > 0) {
        // create join morph collection thingy
        componentAttributes.forEach(name => {
          definition.loadedModel[name] = [
            {
              kind: String,
              ref: {
                type: mongoose.Schema.Types.ObjectId,
                refPath: `${name}.kind`,
              },
            },
          ];
        });
      }
  
      // handle scalar attrs
      scalarAttributes.forEach(name => {
        const attr = definition.attributes[name];
  
        definition.loadedModel[name] = {
          ...attr,
          ...utils(instance).convertType(name, attr),
        };
      });
  
      // handle relational attrs
      relationalAttributes.forEach(name => {
        buildRelation({
          definition,
          model,
          instance,
          name,
          attribute: definition.attributes[name],
        });
      });
  
      const schema = new instance.Schema(
        _.omitBy(definition.loadedModel, ({ type }) => type === 'virtual')
      );
  
      const findLifecycles = ['find', 'findOne', 'findOneAndUpdate', 'findOneAndRemove'];
  
      /*
          Override populate path for polymorphic association.
          It allows us to make Upload.find().populate('related')
          instead of Upload.find().populate('related.item')
        */
      const morphAssociations = definition.associations.filter(isPolymorphicAssoc);
  
      const populateFn = createOnFetchPopulateFn({
        componentAttributes,
        morphAssociations,
        definition,
      });
  
      findLifecycles.forEach(key => {
        schema.pre(key, populateFn);
      });
  
      // Add virtual key to provide populate and reverse populate
      _.forEach(
        _.pickBy(definition.loadedModel, model => {
          return model.type === 'virtual';
        }),
        (value, key) => {
          schema.virtual(key.replace('_v', ''), {
            ref: value.ref,
            localField: '_id',
            foreignField: value.via,
            justOne: value.justOne || false,
          });
        }
      );
  
      target[model].allAttributes = _.clone(definition.attributes);
  
      // Use provided timestamps if the elemnets in the array are string else use default.
      const timestampsOption = _.get(definition, 'options.timestamps', true);
      if (_.isArray(timestampsOption)) {
        const [createAtCol = 'createdAt', updatedAtCol = 'updatedAt'] = timestampsOption;
  
        schema.set('timestamps', {
          createdAt: createAtCol,
          updatedAt: updatedAtCol,
        });
  
        target[model].allAttributes[createAtCol] = {
          type: 'timestamp',
        };
        target[model].allAttributes[updatedAtCol] = {
          type: 'timestamp',
        };
      } else if (timestampsOption === true) {
        schema.set('timestamps', true);
  
        _.set(definition, 'options.timestamps', ['createdAt', 'updatedAt']);
  
        target[model].allAttributes.createdAt = {
          type: 'timestamp',
        };
        target[model].allAttributes.updatedAt = {
          type: 'timestamp',
        };
      }
      schema.set('minimize', _.get(definition, 'options.minimize', false) === true);
  
      const refToStrapiRef = obj => {
        const ref = obj.ref;
  
        let plainData = ref && typeof ref.toJSON === 'function' ? ref.toJSON() : ref;
  
        if (typeof plainData !== 'object') return ref;
  
        return {
          __contentType: obj.kind,
          ...ref,
        };
      };
  
      schema.options.toObject = schema.options.toJSON = {
        virtuals: true,
        transform: function(doc, returned) {
          // Remover $numberDecimal nested property.
  
          Object.keys(returned)
            .filter(key => returned[key] instanceof mongoose.Types.Decimal128)
            .forEach(key => {
              // Parse to float number.
              returned[key] = parseFloat(returned[key].toString());
            });
  
          morphAssociations.forEach(association => {
            if (
              Array.isArray(returned[association.alias]) &&
              returned[association.alias].length > 0
            ) {
              // Reformat data by bypassing the many-to-many relationship.
              switch (association.nature) {
                case 'oneMorphToOne':
                  returned[association.alias] = refToStrapiRef(returned[association.alias][0]);
  
                  break;
  
                case 'manyMorphToMany':
                case 'manyMorphToOne': {
                  returned[association.alias] = returned[association.alias].map(obj =>
                    refToStrapiRef(obj)
                  );
  
                  break;
                }
                default:
              }
            }
          });
  
          componentAttributes.forEach(name => {
            const attribute = definition.attributes[name];
            const { type } = attribute;
  
            if (type === 'component') {
              if (Array.isArray(returned[name])) {
                const components = returned[name].map(el => el.ref);
                // Reformat data by bypassing the many-to-many relationship.
                returned[name] =
                  attribute.repeatable === true ? components : _.first(components) || null;
              }
            }
  
            if (type === 'dynamiczone') {
              if (returned[name]) {
                const components = returned[name].map(el => {
                  return {
                    __component: findComponentByGlobalId(el.kind).uid,
                    ...el.ref,
                  };
                });
  
                returned[name] = components;
              }
            }
          });
        },
      };
  
      // Instantiate model.
      const Model = instance.model(definition.globalId, schema, definition.collectionName);
  
      const handleIndexesErrors = () => {
        Model.on('index', error => {
          if (error) {
            if (error.code === 11000) {
              strapi.log.error(
                `Unique constraint fails, make sure to update your data and restart to apply the unique constraint.\n\t- ${error.message}`
              );
            } else {
              strapi.log.error(`An index error happened, it wasn't applied.\n\t- ${error.message}`);
            }
          }
        });
      };
  
      // Only sync indexes in development env while it's not possible to create complex indexes directly from models
      // In other environments it will simply create missing indexes (those defined in the models but not present in db)
      if (strapi.app.env === 'development') {
        // Ensure indexes are synced with the model, prevent duplicate index errors
        // Side-effect: Delete all the indexes not present in the model.json
        Model.syncIndexes(null, handleIndexesErrors);
      } else {
        handleIndexesErrors();
      }
  
      // Expose ORM functions through the `target` object.
      target[model] = _.assign(Model, target[model]);
  
      // Push attributes to be aware of model schema.
      target[model]._attributes = definition.attributes;
      target[model].updateRelations = relations.update;
      target[model].deleteRelations = relations.deleteRelations;
    }
  
    // Parse every authenticated model.
    Object.keys(models).map(mountModel);
  };
  
  const createOnFetchPopulateFn = ({ morphAssociations, componentAttributes, definition }) => {
    return function() {
      const populatedPaths = this.getPopulatedPaths();
  
      morphAssociations.forEach(association => {
        const { alias, nature } = association;
  
        if (['oneToManyMorph', 'manyToManyMorph'].includes(nature)) {
          this.populate(alias);
        } else if (populatedPaths.includes(alias)) {
          _.set(this._mongooseOptions.populate, [alias, 'path'], `${alias}.ref`);
        }
      });
  
      if (definition.modelType === 'component') {
        definition.associations
          .filter(assoc => !isPolymorphicAssoc(assoc))
          .filter(ast => ast.autoPopulate !== false)
          .forEach(ast => {
            this.populate({ path: ast.alias });
          });
      }
  
      componentAttributes.forEach(key => {
        this.populate({ path: `${key}.ref` });
      });
    };
  };
  
  const buildRelation = ({ definition, model, instance, attribute, name }) => {
    const { nature, verbose } =
      utilsModels.getNature({
        attribute,
        attributeName: name,
        modelName: model.toLowerCase(),
      }) || {};
  
    // Build associations key
    utilsModels.defineAssociations(model.toLowerCase(), definition, attribute, name);
  
    const getRef = (name, plugin) => {
      return plugin ? strapi.plugins[plugin].models[name].globalId : strapi.models[name].globalId;
    };
  
    const setField = (name, val) => {
      definition.loadedModel[name] = val;
    };
  
    const { ObjectId } = instance.Schema.Types;
  
    switch (verbose) {
      case 'hasOne': {
        const ref = getRef(attribute.model, attribute.plugin);
  
        setField(name, { type: ObjectId, ref });
  
        break;
      }
      case 'hasMany': {
        const FK = _.find(definition.associations, {
          alias: name,
        });
  
        const ref = getRef(attribute.collection, attribute.plugin);
  
        if (FK) {
          setField(name, {
            type: 'virtual',
            ref,
            via: FK.via,
            justOne: false,
          });
  
          // Set this info to be able to see if this field is a real database's field.
          attribute.isVirtual = true;
        } else {
          setField(name, [{ type: ObjectId, ref }]);
        }
        break;
      }
      case 'belongsTo': {
        const FK = _.find(definition.associations, {
          alias: name,
        });
  
        const ref = getRef(attribute.model, attribute.plugin);
  
        if (
          FK &&
          FK.nature !== 'oneToOne' &&
          FK.nature !== 'manyToOne' &&
          FK.nature !== 'oneWay' &&
          FK.nature !== 'oneToMorph'
        ) {
          setField(name, {
            type: 'virtual',
            ref,
            via: FK.via,
            justOne: true,
          });
  
          // Set this info to be able to see if this field is a real database's field.
          attribute.isVirtual = true;
        } else {
          setField(name, { type: ObjectId, ref });
        }
  
        break;
      }
      case 'belongsToMany': {
        const ref = getRef(attribute.collection, attribute.plugin);
  
        if (nature === 'manyWay') {
          setField(name, [{ type: ObjectId, ref }]);
        } else {
          const FK = _.find(definition.associations, {
            alias: name,
          });
  
          // One-side of the relationship has to be a virtual field to be bidirectional.
          if ((FK && _.isUndefined(FK.via)) || attribute.dominant !== true) {
            setField(name, {
              type: 'virtual',
              ref,
              via: FK.via,
            });
  
            // Set this info to be able to see if this field is a real database's field.
            attribute.isVirtual = true;
          } else {
            setField(name, [{ type: ObjectId, ref }]);
          }
        }
        break;
      }
      case 'morphOne': {
        const ref = getRef(attribute.model, attribute.plugin);
        setField(name, { type: ObjectId, ref });
        break;
      }
      case 'morphMany': {
        const ref = getRef(attribute.collection, attribute.plugin);
        setField(name, [{ type: ObjectId, ref }]);
        break;
      }
  
      case 'belongsToMorph': {
        setField(name, {
          kind: String,
          [attribute.filter]: String,
          ref: { type: ObjectId, refPath: `${name}.kind` },
        });
        break;
      }
      case 'belongsToManyMorph': {
        setField(name, [
          {
            kind: String,
            [attribute.filter]: String,
            ref: { type: ObjectId, refPath: `${name}.kind` },
          },
        ]);
        break;
      }
      default:
        break;
    }
  };
  