text: /*!
   * Copyright 2015 Apereo Foundation (AF) Licensed under the
   * Educational Community License, Version 2.0 (the "License"); you may
   * not use this file except in compliance with the License. You may
   * obtain a copy of the License at
   *
   *     http://opensource.org/licenses/ECL-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an "AS IS"
   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
   * or implied. See the License for the specific language governing
   * permissions and limitations under the License.
   */
  
  var _ = require('underscore');
  var Chance = require('chance');
  var util = require('util');
  
  var Cassandra = require('oae-util/lib/cassandra');
  var OaeUtil = require('oae-util/lib/util');
  
  var Invitation = require('oae-authz/lib/invitations/model').Invitation;
  var Validator = require('oae-authz/lib/validator').Validator;
  
  var chance = new Chance();
  
  var TOKEN_POOL = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
  
  /**
   * Get the current invitation tokens for the specified emails. For any email that is not associated
   * to a token, one will be randomly generated for it
   *
   * @param  {String[]}   emails                  The emails for which to get or create tokens
   * @param  {Function}   callback                Standard callback function
   * @param  {Object}     callback.err            An error that occurred, if any
   * @param  {Object}     callback.emailTokens    An object keyed by email, whose value is the invitation token associated to it
   */
  var getOrCreateTokensByEmails = module.exports.getOrCreateTokensByEmails = function(emails, callback) {
      getTokensByEmails(emails, function(err, emailTokens) {
          if (err) {
              return callback(err);
          }
  
          var queries = [];
  
          // For each email that did not have an invitation associated to it, persist one and add it
          // to the email tokens hash
          _.each(emails, function(email) {
              if (!emailTokens[email]) {
                  var token = chance.string({'length': 12, 'pool': TOKEN_POOL});
                  queries.push(
                      Cassandra.constructUpsertCQL('AuthzInvitationsTokenByEmail', 'email', email, {'token': token}),
                      Cassandra.constructUpsertCQL('AuthzInvitationsEmailByToken', 'token', token, {'email': email})
                  );
                  emailTokens[email] = token;
              }
          });
  
          // If all the emails had tokens, we can just return without doing any queries
          if (_.isEmpty(queries)) {
              return callback(null, emailTokens);
          }
  
          // Add the missing email tokens to the invitations token tables
          Cassandra.runBatchQuery(queries, function(err) {
              if (err) {
                  return callback(err);
              }
  
              return callback(null, emailTokens);
          });
      });
  };
  
  /**
   * Get the email tokens associated to the specified emails
   *
   * @param  {String[]}   emails                  The emails for which to get the tokens
   * @param  {Function}   callback                Standard callback function
   * @param  {Object}     callback.err            An error that occurred, if any
   * @param  {Object}     callback.emailTokens    An object keyed by email, whose value is the invitation token associated to it
   */
  var getTokensByEmails = module.exports.getTokensByEmails = function(emails, callback) {
      // Get all existing tokens for emails
      Cassandra.runQuery('SELECT * FROM "AuthzInvitationsTokenByEmail" WHERE "email" IN (?)', [emails], function(err, rows) {
          if (err) {
              return callback(err);
          }
  
          var emailTokens = _.chain(rows)
              .map(Cassandra.rowToHash)
              .indexBy('email')
              .mapObject(function(hash) {
                  return hash.token;
              })
              .value();
          return callback(null, emailTokens);
      });
  };
  
  /**
   * Get the email associated to the specified token
   *
   * @param  {String}     token           The token for which to get the associated email
   * @param  {Function}   callback        Standard callback function
   * @param  {Object}     callback.err    An error that occurred, if any
   * @param  {String}     callback.email  The email that was associated to the token
   */
  var getEmailByToken = module.exports.getEmailByToken = function(token, callback) {
      Cassandra.runQuery('SELECT * FROM "AuthzInvitationsEmailByToken" WHERE "token" = ?', [token], function(err, rows) {
          if (err) {
              return callback(err);
          } else if (_.isEmpty(rows)) {
              return callback({'code': 404, 'msg': util.format('There is no email associated to the email token "%s"', token)});
          }
  
          var email = _.chain(rows)
              .map(Cassandra.rowToHash)
              .pluck('email')
              .first()
              .value();
  
          return callback(null, email);
      });
  };
  
  /**
   * Given an email, get all the invitations associated to it
   *
   * @param  {String}         email                   The email for which to get the invitations
   * @param  {Function}       callback                Standard callback function
   * @param  {Object}         callback.err            An error that occurred, if any
   * @param  {Invitation[]}   callback.invitations    All invitations that were sent for the specified email
   */
  var getAllInvitationsByEmail = module.exports.getAllInvitationsByEmail = function(email, callback) {
      _getAllInvitationResourceIdsByEmail(email, function(err, invitationResourceIds) {
          if (err) {
              return callback(err);
          }
  
          return _getInvitations(invitationResourceIds, email, callback);
      });
  };
  
  /**
   * Get all invitations that were sent for a specified resource
   *
   * @param  {String}         resourceId              The id of the resource for which to get invitations
   * @param  {Function}       callback                Standard callback function
   * @param  {Object}         callback.err            An error that occurred, if any
   * @param  {Invitation[]}   callback.invitations    All invitations that were sent for the specified resource
   */
  var getAllInvitationsByResourceId = module.exports.getAllInvitationsByResourceId = function(resourceId, callback, _invitations, _nextToken) {
      _invitations = _invitations || [];
      _nextToken = _nextToken || '';
      Cassandra.runQuery('SELECT * FROM "AuthzInvitations" WHERE "resourceId" = ? AND "email" > ? ORDER BY "email" ASC LIMIT 150', [resourceId, _nextToken], function(err, rows) {
          if (err) {
              return callback(err);
          } else if (_.isEmpty(rows)) {
              return callback(null, _invitations);
          }
  
          var invitations = _.map(rows, Cassandra.rowToHash);
          return getAllInvitationsByResourceId(resourceId, callback, _.union(_invitations, invitations), _.last(invitations).email);
      });
  };
  
  /**
   * Get a unique invitation for the given resource id and email
   *
   * @param  {String}     resourceId                  The id of the resource for which to get the invitation
   * @param  {String}     email                       The email address that was invited to the resource
   * @param  {Function}   callback                    Standard callback function
   * @param  {Object}     callback.err                An error that occurred, if any
   * @param  {Object}     callback.invitationHash     The invitation storage hash associated to the given resource id and email
   */
  var getInvitation = module.exports.getInvitation = function(resourceId, email, callback) {
      Cassandra.runQuery('SELECT * FROM "AuthzInvitations" WHERE "resourceId" = ? AND "email" = ?', [resourceId, email], function(err, rows) {
          if (err) {
              return callback(err);
          } else if (_.isEmpty(rows)) {
              return callback({'code': 404, 'msg': 'An invitation could not be found for the given resource and email'});
          }
  
          return callback(null, Cassandra.rowToHash(_.first(rows)));
      });
  };
  
  /**
   * Create an invitation for each specified email+role for the specified resource
   *
   * @param  {String}     resourceId                  The id of the resource for which to create the invitations
   * @param  {Object}     emailRoles                  An object keyed by email whose value is the role for each invitation to create
   * @param  {String}     inviterUserId               The id of the user who is inviting the specified emails
   * @param  {Function}   callback                    Standard callback function
   * @param  {Object}     callback.err                An error that occurred, if any
   * @param  {Object}     callback.emailTokens        An object keyed by email whose value is the associated invitation token
   * @param  {Object[]}   callback.invitationHashes   All invitations that were created for the email+roles
   */
  var createInvitations = module.exports.createInvitations = function(resourceId, emailRoles, inviterUserId, callback) {
      var validator = new Validator();
      validator.check(resourceId, {'code': 400, 'msg': 'Specified resource must have a valid resource id'}).isResourceId();
      _.each(emailRoles, function(role, email) {
          validator.check(email, {'code': 400, 'msg': 'A valid email must be supplied to invite'}).isEmail();
          validator.check(role, {'code': 400, 'msg': 'A valid role must be supplied to give the invited user'}).isValidRole();
      });
      validator.check(inviterUserId, {'code': 400, 'msg': util.format('Specified inviter id "%s" must be a valid user id')}).isUserId();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Ensure all emails have invitation tokens that can be used to accept invitations
      getOrCreateTokensByEmails(_.keys(emailRoles), function(err, emailTokens) {
          if (err) {
              return callback(err);
          }
  
          // Create the invitation storage hashes that will be persisted
          var invitationHashes = _.map(emailRoles, function(role, email) {
              return {
                  'resourceId': resourceId,
                  'email': email,
                  'inviterUserId': inviterUserId,
                  'role': role
              };
          });
  
          // Create and run the batch set of queries that will create all the invitations
          var queries = _.chain(invitationHashes)
              .map(function(hash) {
                  return [
                      {
                          'query': 'UPDATE "AuthzInvitations" SET "inviterUserId" = ?, "role" = ? WHERE "resourceId" = ? AND "email" = ?',
                          'parameters': [hash.inviterUserId, hash.role, hash.resourceId, hash.email]
                      },
                      {
                          'query': 'INSERT INTO "AuthzInvitationsResourceIdByEmail" ("resourceId", "email") VALUES (?, ?)',
                          'parameters': [hash.resourceId, hash.email]
                      }
                  ];
              })
              .flatten()
              .value();
          Cassandra.runBatchQuery(queries, function(err) {
              if (err) {
                  return callback(err);
              }
  
              return callback(null, emailTokens, invitationHashes);
          });
      });
  };
  
  /**
   * Update the roles associated to the specified invitations for the given resource
   *
   * @param  {String}     resourceId      The id of the resource for which to update the invitation roles
   * @param  {Object}     emailRoles      An object keyed by email whose value is the role for each invitation to update
   * @param  {Function}   callback        Standard callback function
   * @param  {Object}     callback.err    An error that occurred, if any
   */
  var updateInvitationRoles = module.exports.updateInvitationRoles = function(resourceId, emailRoles, callback) {
      var validator = new Validator();
      validator.check(resourceId, {'code': 400, 'msg': 'Specified resource must have a valid resource id'}).isResourceId();
      _.each(emailRoles, function(role, email) {
          validator.check(email, {'code': 400, 'msg': util.format('Invalid email "%s" specified', email)}).isEmail();
          validator.check(role, {'code': 400, 'msg': util.format('Invalid role change "%s" specified', role)}).isValidRoleChange();
          if (role !== false) {
              validator.check(null, {'code': 400, 'msg': util.format('Invalid role "%s" specified', role)}).isString(role);
          }
      });
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      var queries = [];
      _.each(emailRoles, function(role, email) {
          if (_.isString(role)) {
              queries.push({
                  'query': 'UPDATE "AuthzInvitations" SET "role" = ? WHERE "resourceId" = ? AND "email" = ?',
                  'parameters': [role, resourceId, email]
              });
          } else if (role === false) {
              queries.push(
                  {
                      'query': 'DELETE FROM "AuthzInvitations" WHERE "resourceId" = ? AND "email" = ?',
                      'parameters': [resourceId, email]
                  },
                  {
                      'query': 'DELETE FROM "AuthzInvitationsResourceIdByEmail" WHERE "email" = ? AND "resourceId" = ?',
                      'parameters': [email, resourceId]
                  }
              );
          }
      });
  
      Cassandra.runBatchQuery(queries, callback);
  };
  
  /**
   * Delete all invitations that are associated to the given resource id
   *
   * @param  {String}     resourceId      The id of the resource whose invitations to delete
   * @param  {Function}   callback        Standard callback function
   * @param  {Object}     callback.err    An error that occurred, if any
   */
  var deleteInvitationsByResourceId = module.exports.deleteInvitationsByResourceId = function(resourceId, callback) {
      getAllInvitationsByResourceId(resourceId, function(err, invitations) {
          if (err) {
              return callback(err);
          }
  
          var changes = _.chain(invitations)
              .pluck('email')
              .map(function(email) {
                  return [email, false];
              })
              .object()
              .value();
          return updateInvitationRoles(resourceId, changes, callback);
      });
  };
  
  /**
   * Delete all invitations associated to the given email
   *
   * @param  {String}     email           The email whose invitations to delete
   * @param  {Function}   callback        Standard callback function
   * @param  {Object}     callback.err    An error that occurred, if any
   */
  var deleteInvitationsByEmail = module.exports.deleteInvitationsByEmail = function(email, callback) {
      var validator = new Validator();
      validator.check(email, {'code': 400, 'msg': 'Specified email is not valid'}).isEmail();
      if (validator.hasErrors()) {
          return callback(validator.getFirstError());
      }
  
      // Get the active token for the given email
      getOrCreateTokensByEmails([email], function(err, tokenByEmail) {
          if (err) {
              return callback(err);
          }
  
          var token = tokenByEmail[email];
  
          _getAllInvitationResourceIdsByEmail(email, function(err, resourceIds) {
              if (err) {
                  return callback(err);
              }
  
              var queries = [
                  // Delete the email token so a new one can be generated
                  {
                      'query': 'DELETE FROM "AuthzInvitationsEmailByToken" WHERE "token" = ?',
                      'parameters': [token]
                  },
                  {
                      'query': 'DELETE FROM "AuthzInvitationsTokenByEmail" WHERE "email" = ?',
                      'parameters': [email]
                  },
  
                  // Delete the invitations index associated to this email
                  {
                      'query': 'DELETE FROM "AuthzInvitationsResourceIdByEmail" WHERE "email" = ?',
                      'parameters': [email]
                  }
              ];
  
              // Delete all the invitations entries for each resource that invited this email
              _.each(resourceIds, function(resourceId) {
                  queries.push({
                      'query': 'DELETE FROM "AuthzInvitations" WHERE "resourceId" = ? AND "email" = ?',
                      'parameters': [resourceId, email]
                  });
              });
  
              return Cassandra.runBatchQuery(queries, callback);
          });
      });
  };
  
  /**
   * Get all invitation resource ids for the specified email
   *
   * @param  {String}     email                   The email for which to get all invitation resource ids
   * @param  {Function}   callback                Standard callback function
   * @param  {Object}     callback.err            An error that occurred, if any
   * @param  {String[]}   callback.resourceIds    All resource ids that the specified email was invited into
   * @api private
   */
  var _getAllInvitationResourceIdsByEmail = function(email, callback, _resourceIds, _nextToken) {
      _resourceIds = _resourceIds || [];
      _nextToken = _nextToken || '';
  
      var cql = 'SELECT "resourceId" FROM "AuthzInvitationsResourceIdByEmail" WHERE "email" = ?';
      var params = [email];
      if (_nextToken) {
          cql += ' AND "resourceId" > ?';
          params.push(_nextToken);
      }
  
      cql += ' ORDER BY "resourceId" ASC LIMIT 100';
      Cassandra.runQuery(cql, params, function(err, rows) {
          if (err) {
              return callback(err);
          } else if (_.isEmpty(rows)) {
              return callback(null, _resourceIds);
          }
  
          // Join the resource ids we just fetched with our existing list
          var resourceIds = _.chain(rows)
              .map(Cassandra.rowToHash)
              .pluck('resourceId')
              .value();
          _resourceIds = _.union(_resourceIds, resourceIds);
  
          return _getAllInvitationResourceIdsByEmail(email, callback, _resourceIds, _.last(resourceIds));
      });
  };
  
  /**
   * Get invitations associated to the specified resources ids and email
   *
   * @param  {String[]}   resourceIds                 The ids of the resources for which to get the invitations
   * @param  {String}     email                       The email for which to get the invitations
   * @param  {Function}   callback                    Standard callback function
   * @param  {Object}     callback.err                An error that occurred, if any
   * @param  {Object[]}   callback.invitationHashes   The invitation storage objects for the given resource ids and email
   * @api private
   */
  var _getInvitations = function(resourceIds, email, callback) {
      if (_.isEmpty(resourceIds)) {
          return callback(null, []);
      }
  
      Cassandra.runQuery('SELECT * FROM "AuthzInvitations" WHERE "resourceId" IN (?) AND email = ?', [resourceIds, email], function(err, rows) {
          if (err) {
              return callback(err);
          }
  
          return callback(null, _.map(rows, Cassandra.rowToHash));
      });
  };
  