'use strict';

const {
  flatMap,
  reject,
  isNil,
  isArray,
  prop,
  xor,
  eq,
  uniq,
  map,
  difference,
  differenceWith,
  pipe,
} = require('lodash/fp');
const pmap = require('p-map');
const { EDITOR_CODE } = require('../constants');
const { getBoundActionsBySubject, BOUND_ACTIONS_FOR_FIELDS } = require('../../domain/role');
const { getService } = require('../../utils');
const permissionDomain = require('../../domain/permission/index');

/**
 * Delete permissions of roles in database
 * @param rolesIds ids of roles
 * @returns {Promise<array>}
 */
const deleteByRolesIds = async rolesIds => {
  const permissionsToDelete = await strapi.query('admin::permission').findMany({
    select: ['id'],
    where: {
      role: { id: rolesIds },
    },
  });

  if (permissionsToDelete.length > 0) {
    await deleteByIds(permissionsToDelete.map(prop('id')));
  }
};

/**
 * Delete permissions
 * @param ids ids of permissions
 * @returns {Promise<array>}
 */
const deleteByIds = async ids => {
  for (const id of ids) {
    await strapi.query('admin::permission').delete({ where: { id } });
  }
};

/**
 * Create many permissions
 * @param permissions
 * @returns {Promise<*[]|*>}
 */
const createMany = async permissions => {
  const createdPermissions = [];
  for (const permission of permissions) {
    const newPerm = await strapi.query('admin::permission').create({ data: permission });
    createdPermissions.push(newPerm);
  }

  return permissionDomain.toPermission(createdPermissions);
};

/**
 * Update a permission
 * @returns {Promise<*[]|*>}
 * @param params
 * @param attributes
 */
const update = async (params, attributes) => {
  const updatedPermission = await strapi
    .query('admin::permission')
    .update({ where: params, data: attributes });

  return permissionDomain.toPermission(updatedPermission);
};

/**
 * Find assigned permissions in the database
 * @param params query params to find the permissions
 * @returns {Promise<Permission[]>}
 */
const findMany = async (params = {}) => {
  const rawPermissions = await strapi.query('admin::permission').findMany(params);

  return permissionDomain.toPermission(rawPermissions);
};

/**
 * Find all permissions for a user
 * @param user - user
 * @returns {Promise<Permission[]>}
 */
const findUserPermissions = async user => {
  return findMany({ where: { role: { users: { id: user.id } } } });
};

const filterPermissionsToRemove = async permissions => {
  const { actionProvider } = getService('permission');

  const permissionsToRemove = [];

  for (const permission of permissions) {
    const { subjects, options = {} } = actionProvider.get(permission.action) || {};
    const { applyToProperties } = options;

    const invalidProperties = await Promise.all(
      (applyToProperties || []).map(async property => {
        const applies = await actionProvider.appliesToProperty(
          property,
          permission.action,
          permission.subject
        );

        return applies && isNil(permissionDomain.getProperty(property, permission));
      })
    );

    const isRegisteredAction = actionProvider.has(permission.action);
    const hasInvalidProperties = isArray(applyToProperties) && invalidProperties.every(eq(true));
    const isInvalidSubject = isArray(subjects) && !subjects.includes(permission.subject);

    // If the permission has an invalid action, an invalid subject or invalid properties, then add it to the toBeRemoved collection
    if (!isRegisteredAction || isInvalidSubject || hasInvalidProperties) {
      permissionsToRemove.push(permission);
    }
  }

  return permissionsToRemove;
};

/**
 * Removes permissions in database that don't exist anymore
 * @returns {Promise<>}
 */
const cleanPermissionsInDatabase = async () => {
  const pageSize = 200;

  const contentTypeService = getService('content-type');

  const total = await strapi.query('admin::permission').count();
  const pageCount = Math.ceil(total / pageSize);

  for (let page = 0; page < pageCount; page++) {
    // 1. Find invalid permissions and collect their ID to delete them later
    const results = await strapi
      .query('admin::permission')
      .findMany({ limit: pageSize, offset: page * pageSize });

    const permissions = permissionDomain.toPermission(results);
    const permissionsToRemove = await filterPermissionsToRemove(permissions);
    const permissionsIdToRemove = map(prop('id'), permissionsToRemove);

    // 2. Clean permissions' fields (add required ones, remove the non-existing ones)
    const remainingPermissions = permissions.filter(
      permission => !permissionsIdToRemove.includes(permission.id)
    );

    const permissionsWithCleanFields = contentTypeService.cleanPermissionFields(
      remainingPermissions
    );

    // Update only the ones that need to be updated
    const permissionsNeedingToBeUpdated = differenceWith(
      (a, b) => {
        return a.id === b.id && xor(a.properties.fields, b.properties.fields).length === 0;
      },
      permissionsWithCleanFields,
      remainingPermissions
    );

    const updatePromiseProvider = permission => {
      return update({ id: permission.id }, permission);
    };

    // Execute all the queries, update the database
    await Promise.all([
      deleteByIds(permissionsIdToRemove),
      pmap(permissionsNeedingToBeUpdated, updatePromiseProvider, {
        concurrency: 100,
        stopOnError: true,
      }),
    ]);
  }
};

const ensureBoundPermissionsInDatabase = async () => {
  if (strapi.EE) {
    return;
  }

  const contentTypes = Object.values(strapi.contentTypes);
  const editorRole = await strapi.query('admin::role').findOne({
    where: { code: EDITOR_CODE },
  });

  if (isNil(editorRole)) {
    return;
  }

  for (const contentType of contentTypes) {
    const boundActions = getBoundActionsBySubject(editorRole, contentType.uid);

    const permissions = await findMany({
      where: {
        subject: contentType.uid,
        action: boundActions,
        role: { id: editorRole.id },
      },
    });

    if (permissions.length === 0) {
      return;
    }

    const fields = pipe(
      flatMap(permissionDomain.getProperty('fields')),
      reject(isNil),
      uniq
    )(permissions);

    // Handle the scenario where permissions are missing
    const missingActions = difference(map('action', permissions), boundActions);

    if (missingActions.length > 0) {
      const permissions = pipe(
        // Create a permission skeleton from the action id
        map(action => ({ action, subject: contentType.uid, role: editorRole.id })),
        // Use the permission domain to create a clean permission from the given object
        map(permissionDomain.create),
        // Adds the fields property if the permission action is eligible
        map(permission =>
          BOUND_ACTIONS_FOR_FIELDS.includes(permission.action)
            ? permissionDomain.setProperty('fields', fields, permission)
            : permission
        )
      )(missingActions);

      await createMany(permissions);
    }
  }
};

module.exports = {
  createMany,
  findMany,
  deleteByRolesIds,
  deleteByIds,
  findUserPermissions,
  cleanPermissionsInDatabase,
  ensureBoundPermissionsInDatabase,
};
