const _ = require('lodash');
const DataLoader = require('dataloader');
const {objectCacheKeyFn} = require('./util');

const ActionModel = require('../../models/action');
const CommentModel = require('../../models/comment');

/**
 * Returns the assets which have had comments made within the last time period.
 */
const getAssetActivityMetrics = ({loaders: {Assets}}, {from, to, limit}) => {
  let assetMetrics = [];

  return CommentModel.aggregate([
    {$match: {
      parent_id: null,
      created_at: {
        $gt: from,
        $lt: to
      }
    }},
    {$group: {
      _id: '$asset_id',
      commentCount: {
        $sum: 1
      }
    }},
    {$project: {
      _id: false,
      asset_id: '$_id',
      commentCount: '$commentCount'
    }},
    {$sort: {
      commentCount: -1
    }},
    {$limit: limit}
  ])
  .then((results) => {
    assetMetrics = results;

    return Assets.getByID.loadMany(results.map((result) => result.asset_id));
  })
  .then((assets) => assets.map((asset, i) => {

    // We're leveraging the fact that the comments returned by the aggregation
    // query are in the request order that we just made, it's what the
    // Assets.getByID loader does.
    asset.commentCount = assetMetrics[i].commentCount;

    return asset;
  }));
};

/**
 * Returns a list of assets with action metadata included on the models.
 */
const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sort, limit}) => {

  // Get the recent actions.
  let actionSummaries = await Metrics.getRecentActions.load({from, to});

  let commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
    if (action_type !== sort) {
      return acc;
    }

    if (!(item_id in acc)) {
      acc[item_id] = [];
    }

    acc[item_id].push({action_type, count});

    return acc;
  }, {});

  // Collect just the comment id's.
  let commentIDs = Object.keys(commentMetrics);

  // Find those comments.
  let comments = await Comments.get.loadMany(commentIDs);

  let commentResults = _.groupBy(comments, 'asset_id');

  let assetMetrics = Object.keys(commentResults)
    .map((asset_id) => {
      let ids = commentResults[asset_id].map((comment) => comment.id);
      let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type');

      let action_summaries = Object.keys(summaries).map((action_type) => ({
        action_type,
        actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0),
        actionableItemCount: summaries[action_type].length
      }));

      return {action_summaries, id: asset_id};
    })
  
    .filter((asset) => {
      let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sort));
      if (contextActionSummary === null || contextActionSummary.actionCount === 0) {
        return false;
      }

      return true;
    })

    // Sort these metrics by the predefined sort order. This will ensure that
    // if the action summary does not exist on the object, that it is less
    // prefered over the one that does have it.
    .sort((a, b) => {
      let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort));
      let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort));

      // Both of them had an actionCount, hence we can determine that we could
      // compare the actual values directly.
      return bActionSummary.actionCount - aActionSummary.actionCount;
    });

  // Only keep the top `limit`.
  assetMetrics = assetMetrics.slice(0, limit);

  // Determine the assets that we need to return.
  let assets = await Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id));

  // Join up the assets that are returned by their id.
  let groupedAssets = _.groupBy(assets, 'id');

  // Return from the sorted asset metrics and return their assetes.
  return assetMetrics.map(({id, action_summaries}) => {
    if (id in groupedAssets) {
      let asset = groupedAssets[id][0];

      // Add the action summaries to the asset.
      asset.action_summaries = action_summaries;

      return asset;
    }

    return null;
  }).filter((asset) => asset != null);
};

/**
 * Returns a list of comments that are retrieved based on most activity within
 * the indicated time range.
 */
const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sort, limit}) => {

  let commentActionSummaries = {};

  let actionSummaries = await Metrics.getRecentActions.load({from, to});

  actionSummaries.sort((a, b) => {
    let aActionSummary = a.action_type === sort ? a : null;
    let bActionSummary = b.action_type === sort ? b : null;

    // If either a or b don't have this action type, then one of them will
    // automatically win.
    if (aActionSummary == null || bActionSummary == null) {
      if (bActionSummary != null) {
        return 1;
      }

      if (aActionSummary != null) {
        return -1;
      }

      return 0;
    }

    // Both of them had an actionCount, hence we can determine that we could
    // compare the actual values directly.
    return bActionSummary.count - aActionSummary.count;
  });

  commentActionSummaries = _.groupBy(actionSummaries, 'item_id');

  // Grab the comment id's for comment where they have at least one of the
  // actions being sorted by.
  let commentIDs = Object.keys(commentActionSummaries).filter((item_id) => {
    let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sort);
    if (contextActionSummary == null) {
      return false;
    }

    return true;
  });

  // Only keep the top `limit`.
  commentIDs = commentIDs.slice(0, limit);

  // If there are no comment's to get, then just continue with an empty
  // array.
  if (commentIDs.length === 0) {
    return [];
  }

  // Find those comments, this is the final stage, so let's get all the
  // fields.
  let comments = await Comments.get.loadMany(commentIDs);

  return comments.map((comment) => {

    // Add in the action summaries genrerated.
    comment.action_summaries = commentActionSummaries[comment.id];

    return comment;
  });
};

const getRecentActions = (context, {from, to}) => {
  return ActionModel.aggregate([

    // Find all actions that were created in the time range.
    {$match: {
      item_type: 'COMMENTS',
      created_at: {
        $gt: from,
        $lt: to
      }
    }},

    // Count all those items.
    {$group: {
      _id: {
        item_id: '$item_id',
        action_type: '$action_type'
      },
      count: {
        $sum: 1
      }
    }},

    // Project the count to a better field.
    {$project: {
      item_id: '$_id.item_id',
      action_type: '$_id.action_type',
      count: '$count'
    }}
  ]);
};

module.exports = (context) => ({
  Metrics: {
    getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
      batch: false,
      cacheKeyFn: objectCacheKeyFn('from', 'to')
    }),
    Assets: {
      get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit}),
      getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}),
    },
    Comments: {
      get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit}),
    }
  }
});
