'use strict';

const _ = require('lodash');
const { map, filter, each } = require('lodash/fp');
const { AbilityBuilder, Ability } = require('@casl/ability');
const sift = require('sift');

const allowedOperations = [
  '$or',
  '$eq',
  '$ne',
  '$in',
  '$nin',
  '$lt',
  '$lte',
  '$gt',
  '$gte',
  '$exists',
  '$elemMatch',
];
const operations = _.pick(sift, allowedOperations);

const conditionsMatcher = conditions => {
  return sift.createQueryTester(conditions, { operations });
};

module.exports = conditionProvider => ({
  /**
   * Generate an ability based on the given user (using associated roles & permissions)
   * @param user
   * @param options
   * @returns {Promise<Ability>}
   */
  async generateUserAbility(user, options) {
    const permissions = await strapi.admin.services.permission.findUserPermissions(user);
    const abilityCreator = this.generateAbilityCreatorFor(user);

    return abilityCreator(permissions, options);
  },

  /**
   * Create an ability factory for a specific user
   * @param user
   * @returns {function(*, *): Promise<Ability>}
   */
  generateAbilityCreatorFor(user) {
    return async (permissions, options) => {
      const { can, build } = new AbilityBuilder(Ability);
      const registerFn = this.createRegisterFunction(can);

      for (const permission of permissions) {
        await this.evaluatePermission({ permission, user, options, registerFn });
      }

      return build({ conditionsMatcher });
    };
  },

  /**
   * Register new rules using `registerFn` based on valid permission's conditions
   * @param permission
   * @param user
   * @param options
   * @param registerFn
   * @returns {Promise<void>}
   */
  async evaluatePermission({ permission, user, options, registerFn }) {
    const { action, fields, conditions } = permission;
    const subject = permission.subject || 'all';

    // Permissions with empty fields array should be removed
    if (Array.isArray(fields) && fields.length === 0) {
      return;
    }

    // Directly registers the permission if there is no condition to check/evaluate
    if (_.isUndefined(conditions) || _.isEmpty(conditions)) {
      return registerFn({ action, subject, fields, condition: true });
    }

    // Replace each condition name by its associated value
    const resolveConditions = map(conditionProvider.getById);

    // Only keep the handler of each condition
    const pickHandlers = map(_.property('handler'));

    // Filter conditions, only keeps objects and functions
    const filterValidConditions = filter(_.isObject);

    // Evaluate the conditions if they're a function, returns the object otherwise
    const evaluateConditions = conditions =>
      Promise.all(conditions.map(cond => (_.isFunction(cond) ? cond(user, options) : cond)));

    // Only keeps 'true' booleans or objects as condition's result
    const filterValidResults = filter(result => result === true || _.isObject(result));

    // Transform each result into registerFn options
    const transformToRegisterOptions = map(result => ({
      action,
      subject,
      fields,
      condition: result,
    }));

    // Register each result using the registerFn
    const registerResults = each(registerFn);

    await Promise.resolve(conditions)
      .then(resolveConditions)
      .then(pickHandlers)
      .then(filterValidConditions)
      .then(evaluateConditions)
      .then(filterValidResults)
      .then(transformToRegisterOptions)
      .then(registerResults);
  },

  /**
   * Encapsulate a register function with custom params to fit `evaluatePermission`'s syntax
   * @param can
   * @returns {function({action?: *, subject?: *, fields?: *, condition?: *}): *}
   */
  createRegisterFunction(can) {
    return ({ action, subject, fields, condition }) => {
      return can(action, subject, fields, _.isObject(condition) ? condition : undefined);
    };
  },

  /**
   * Check many permissions based on an ability
   */
  checkMany: _.curry((ability, permissions) => {
    return permissions.map(({ action, subject, field }) => ability.can(action, subject, field));
  }),
});
