const { map, get, find, groupBy, property } = require('lodash');
const uuid = require('uuid/v4');
const { UNSUBSCRIBE_SUBJECT } = require('./config');
const { getOrganizationName, getNotificationBody } = require('./util');
const { checkDigests } = require('./digests');

// processNewNotifications will handle notifications that are collected after an
// event hook. These notifications will be batched by user and optionally
// queued for digesting or sent immediately depending on the user's settings.
const processNewNotifications = async (ctx, notifications) =>
  Promise.all(
    map(
      // Group all the notifications so we don't have to redo the digest check
      // multiple times for the same user.
      groupBy(notifications, 'notification.userID'),
      async (notifications, userID) => {
        // Check to see if the user has digesting enabled.
        const hasDigesting = await checkDigests(ctx, userID);
        if (hasDigesting) {
          // User has digesting enabled, queue the notifications to be sent
          // at a later time.
          return queueNotifications(ctx, userID, notifications);
        }

        // User does not have digesting enabled, send the messages the old
        // way.
        return sendNotificationsBatch(ctx, notifications);
      }
    )
  );

// queueNotifications will queue the notifications onto the User.
const queueNotifications = async (ctx, userID, notifications) => {
  // Mutate the notification payloads to what we can store in Mongo safely.
  const digests = notifications.map(
    ({ notification, handler: { category } }) => ({ notification, category })
  );

  // Pull out some useful tools.
  const { connectors: { models: { User } } } = ctx;

  ctx.log.info(
    { notifications: notifications.length, userID },
    'now queueing notifications for digesting'
  );

  // Push the digests into Mongo.
  await User.update(
    { id: userID },
    { $push: { 'metadata.notifications.digests': { $each: digests } } }
  );
};

// sendNotificationBatch will send a given set of notifications for several
// users that do not have digesting enabled.
const sendNotificationsBatch = async (ctx, notifications) => {
  // Get the notification name for the subject.
  const organizationName = await getOrganizationName(ctx);
  if (!organizationName) {
    ctx.log.error(
      'could not send the notification, organization name not in settings'
    );
    return;
  }

  return Promise.all(
    map(
      notifications,
      async ({ handler, notification: { userID, context } }) => {
        const { connectors: { services: { I18n: { t } } } } = ctx;
        const { category } = handler;

        // Compose the subject for the email.
        const subject = t(
          `talk-plugin-notifications.categories.${category}.subject`,
          organizationName
        );

        // Load the content into the comment.
        const body = await getNotificationBody(ctx, handler, context);

        // Send the email now.
        return sendNotification(ctx, userID, subject, body);
      }
    )
  );
};

// sendNotification will send the notification to the specified user with the
// given context.
const sendNotification = async (
  ctx,
  userID,
  subject,
  body,
  template = 'notification'
) => {
  const {
    connectors: {
      secrets: { jwt },
      config: { JWT_ISSUER, JWT_AUDIENCE },
      services: { Mailer },
    },
  } = ctx;

  try {
    const organizationName = await getOrganizationName(ctx);
    if (!organizationName) {
      ctx.log.error(
        'could not send the notification, organization name not in settings'
      );
      return;
    }

    // unsubscribeToken is the token used to perform the one-click
    // unsubscribe.
    const unsubscribeToken = jwt.sign({
      jti: uuid(),
      iss: JWT_ISSUER,
      aud: JWT_AUDIENCE,
      sub: UNSUBSCRIBE_SUBJECT,
      user: userID,
    });

    // Send the notification to the user.
    const task = await Mailer.send({
      template,
      locals: { body, organizationName, unsubscribeToken },
      subject,
      user: userID,
    });

    ctx.log.info({ jobID: task.id }, 'sent the notification');
  } catch (err) {
    ctx.log.error(
      { err, message: err.message },
      'could not send the notification, an error occurred'
    );
    return;
  }
};

// filterSuperseded will filter all the possible notifications and only send
// those notifications that are not superseded by another type of notification.
const filterSuperseded = (
  { handler: { category }, notification: { userID: destinationUserID } },
  index,
  notifications
) =>
  !notifications.some(
    ({
      handler: { supersedesCategories = [] },
      notification: { userID: notificationUserID },
    }) =>
      // Only allow notifications to supersede another notification if that
      // notification is also destined for the same user.
      notificationUserID === destinationUserID &&
      // If another notification that is destined for the same user also exists
      // and declares that it supersedes this one, return true so we can filter
      // this one from the list.
      supersedesCategories.some(
        supersededCategory => supersededCategory === category
      )
  );

const USER_CONFIRMATION_QUERY = `
  query CheckUserConfirmation($userID: ID!) {
    user(id: $userID) {
      profiles {
        provider
        ... on LocalUserProfile {
          confirmedAt
        }
      }
    }
  }
`;

// filterVerifiedNotification checks to see if a user has a verified email
// address, and if they do, returns the notification payload again, otherwise,
// returns undefined.
const filterVerifiedNotification = ctx => async notification => {
  // Grab the user that we're supposed to be sending the notification to.
  const { notification: { userID } } = notification;

  // Check their confirmed status. This should have already been hit by the
  // loaders, so we shouldn't make any more database requests.
  const { errors, data } = await ctx.graphql(USER_CONFIRMATION_QUERY, {
    userID,
  });
  if (errors) {
    ctx.log.error(
      { err: errors },
      'could not query for user confirmation status'
    );
    return;
  }

  // Get the first local profile from the user.
  const profile = find(get(data, 'user.profiles', []), ['provider', 'local']);
  if (!profile) {
    ctx.log.warn({ user_id: userID }, 'user did not have a local profile');
    return;
  }

  // Pull out the confirmed status from the profile.
  const confirmed = get(profile, 'confirmedAt', null) !== null;
  if (!confirmed) {
    ctx.log.info(
      { user_id: userID },
      'user did not have their local profile confirmed, but had settings enabled, not mailing'
    );
    return;
  }

  return notification;
};

// filterVerified performs filtering in a complicated way because we can't use
// Promise.all on a Array.prototype.filter call.
const filterVerified = async (ctx, notifications) => {
  notifications = await Promise.all(
    notifications.map(filterVerifiedNotification(ctx))
  );

  // This acts as a poor-mans identity filter to remove all falsy values.
  return notifications.filter(property('notification'));
};

module.exports = {
  processNewNotifications,
  filterSuperseded,
  filterVerified,
  getNotificationBody,
  sendNotification,
};
