const ActionModel = require('../models/action');
const CommentModel = require('../models/comment');
const UserModel = require('../models/user');
const _ = require('lodash');
const errors = require('../errors');

const incrActionCounts = async (action, value) => {
  const ACTION_TYPE = action.action_type.toLowerCase();

  const query = { id: action.item_id };
  const update = {
    [`action_counts.${ACTION_TYPE}`]: value,
  };

  if (action.group_id && action.group_id.length > 0) {
    const GROUP_ID = action.group_id.toLowerCase();

    update[`action_counts.${ACTION_TYPE}_${GROUP_ID}`] = value;
  }

  switch (action.item_type) {
    case 'USERS':
      return UserModel.update(query, {
        $inc: update,
      });
    case 'COMMENTS':
      return CommentModel.update(query, {
        $inc: update,
      });
    default:
      return;
  }
};

/**
 * findOnlyOneAndUpdate will perform a fondOneAndUpdate on the mongo collection
 * and ensure that the found record wasn't already found. This is essentially
 * a "findOneOrUpsert".
 *
 * @param {object} query the query operation for the mongo findOneAndUpdate op
 * @param {object} update the update operation for the mongo findOneAndUpdate op
 * @param {object} options the options operation for the mongo findOneAndUpdate op
 */
const findOnlyOneAndUpdate = async (query, update, options = {}) =>
  new Promise((resolve, reject) => {
    ActionModel.findOneAndUpdate(
      query,
      update,
      Object.assign({}, options, {
        // Use raw result to get `updatedExisting`.
        passRawResult: true,

        // Ensure that if it's new, we return the new object created.
        new: true,

        // Perform an upsert in the event that this doesn't exist.
        upsert: true,

        // Set the default values if not provided based on the mongoose models.
        setDefaultsOnInsert: true,
      }),
      (err, doc, raw) => {
        if (err) {
          return reject(err);
        }
        if (raw.lastErrorObject.updatedExisting) {
          return reject(new errors.ErrAlreadyExists(raw.value));
        }
        return resolve(raw.value);
      }
    );
  });

module.exports = class ActionsService {
  /**
   * Finds an action by the id.
   *
   * @param {String} id  identifier of the action (uuid)
   */
  static findById(id) {
    return ActionModel.findOne({ id });
  }

  /**
   * Inserts an action.
   *
   * @param {String} action   the new action to the item
   * @return {Promise}
   */
  static async create(action) {
    // Actions are made unique by using a query that can be reproducible, i.e.,
    // not containing user modifiable values.
    let foundAction = await findOnlyOneAndUpdate(
      {
        action_type: action.action_type,
        item_type: action.item_type,
        item_id: action.item_id,
        user_id: action.user_id,
        group_id: action.group_id,
      },
      {
        // Only set when not existing.
        $setOnInsert: action,
      }
    );

    await incrActionCounts(action, 1);

    return foundAction;
  }

  /**
   * Finds actions in an array of ids.
   *
   * @param {String} ids array of user identifiers (uuid)
   */
  static async findByItemIdArray(item_ids) {
    let actions = await ActionModel.find({
      item_id: { $in: item_ids },
    });

    if (actions === null) {
      return [];
    }

    return actions;
  }

  /**
   * Get the actions for a specific user on the specific items.
   *
   * @param {String} userID the id of the user to find their actions for
   * @param {Array<String>} itemIDs the ids of the items to find their actions
   *                                for
   */
  static getUserActions(userID, itemIDs) {
    return ActionModel.find({
      user_id: userID,
      item_id: {
        $in: itemIDs,
      },
    });
  }

  /**
   * delete will remove the record from the collection if it exists. Otherwise
   * it will do nothing. This will then return the deleted action.
   *
   * @param {object} param the action to use as the query source, for which we
   *                       only look at the id, user_id.
   */
  static async delete({ id, user_id }) {
    let action = await ActionModel.findOneAndRemove({
      id,
      user_id,
    });
    if (!action) {
      return;
    }

    await incrActionCounts(action, -1);

    return action;
  }

  /**
   * Finds all comments ids for a specific action.
   *
   * @param {String} action_type type of action
   * @param {String} item_type type of item the action is on
   */
  static findCommentsIdByActionType(action_type, item_type) {
    return ActionModel.find(
      {
        action_type: action_type,
        item_type: item_type,
      },
      'item_id'
    );
  }
};
