<commit_before>'use strict';

var pg = require('pg');
var LargeObjectManager = require('pg-large-object').LargeObjectManager;
var shortid = require('shortid');
var moment = require('moment');

module.exports = function(options) {
  var seneca = this;

  var PARENT_GUARDIAN_PROFILE_ENTITY = 'cd/profiles';
  var plugin = 'cd-profiles';
  var _ = require('lodash');
  var async = require('async');
  var uuid = require('node-uuid');
  var hiddenFields = require('./data/hidden-fields.js');
  var fs = require('fs');
  var path = require('path');
  var so = seneca.options();

  var syncedFields = [
    'name',
    'email',
    'phone'
  ]

  var mentorPublicFields = [
    'name',
    'languagesSpoken',
    'programmingLanguages',
    'linkedin',
    'twitter',
    'userTypes',
    'dojos',
    'badges',
    'optionalHiddenFields'
  ];

  var championPublicFields = [
    'id',
    'name',
    'languagesSpoken',
    'programmingLanguages',
    'linkedin',
    'twitter',
    'userTypes',
    'projects',
    'notes',
    'dojos',
    'optionalHiddenFields'
  ];

  var attendeeO13PublicFields = [
    'alias',
    'linkedin',
    'twitter',
    'badges',
    'userTypes',
    'optionalHiddenFields'
  ];

  var fieldWhiteList = {
    'mentor': mentorPublicFields,
    'champion': championPublicFields,
    'attendee-o13': attendeeO13PublicFields
  };

  ///var allowedOptionalFieldsYouth = ['dojos', 'linkedin', 'twitter', 'badges'];
  var allowedOptionalFieldsYouth = _.filter(hiddenFields, function(field){
    if(_.contains(field.allowedUserTypes, 'attendee-o13')) return field.modelName;
  });

  //var allowedOptionalFieldsChampion = ['notes', 'projects'];
  var allowedOptionalFieldsChampion = _.map(hiddenFields, function(field){
    if(_.contains(field.allowedUserTypes, 'champion')) return field.modelName;
  });

  var allowedOptionalFieldsMentor = _.map(hiddenFields, function (field) {
    if(_.contains(field.allowedUserTypes, 'mentor')) return field.modelName;
  });

  var allowedOptionalFields = {
    'champion': allowedOptionalFieldsChampion,
    'attendee-o13': allowedOptionalFieldsYouth,
    'mentor': allowedOptionalFieldsMentor
  };

  var immutableFields = ['userType', 'avatar'];

  var youthBlackList = ['name'];

  var requiredProfileFields = ['name', 'alias', 'dob', 'country', 'place', 'address'];

  //var userTypes = ['champion', 'mentor', 'parent-guardian', 'attendee-o13', 'attendee-u13'];
  //var userTypes = ['attendee-u13', 'attendee-o13', 'parent-guardian', 'mentor', 'champion'];


  seneca.add({role: plugin, cmd: 'create'}, cmd_create);
  seneca.add({role: plugin, cmd: 'list'}, cmd_list);
  seneca.add({role: plugin, cmd: 'load'}, cmd_load);
  seneca.add({role: plugin, cmd: 'save-youth-profile'}, cmd_save_youth_profile);
  seneca.add({role: plugin, cmd: 'save'}, cmd_save);
  seneca.add({role: plugin, cmd: 'update-youth-profile'}, cmd_update_youth);
  seneca.add({role: plugin, cmd: 'invite-parent-guardian'}, cmd_invite_parent_guardian);
  seneca.add({role: plugin, cmd: 'search'}, cmd_search);
  seneca.add({role: plugin, cmd: 'accept_parent_invite'}, cmd_accept_parent_invite);
  seneca.add({role: plugin, cmd: 'load_hidden_fields'}, cmd_load_hidden_fields);
  seneca.add({role: plugin, cmd: 'list_query'}, cmd_list_query);
  seneca.add({role: plugin, cmd: 'change_avatar'}, cmd_change_avatar);
  seneca.add({role: plugin, cmd: 'get_avatar'}, cmd_get_avatar);
  seneca.add({role: plugin, cmd: 'load_parents_for_user'}, cmd_load_parents_for_user);
  seneca.add({role: plugin, cmd: 'invite_ninja'}, cmd_invite_ninja);
  seneca.add({role: plugin, cmd: 'approve_invite_ninja'}, cmd_approve_invite_ninja);


  function cmd_search(args, done){
    if(!args.query){
      return done(new Error('Empty query'));
    }

    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$(args.query, done);
  }

  function cmd_create(args, done){
    var profile = args.profile;

    var profileKeys = _.keys(profile);
    var missingKeys = _.difference(requiredProfileFields, profileKeys);
    if(_.isEmpty(missingKeys)) profile.requiredFieldsComplete = true;

    if(args.user !== profile.userId) return done(null, new Error('Profiles can only be saved by the profile user.'));

    if(profile.id){
      profile = _.omit(profile, immutableFields);
    }

    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, function(err, profile){
      if(err) return done(err);
      var updatedFields = {};
      updatedFields.id = profile.userId;
      _.each(syncedFields, function(field){
        updatedFields[field] = profile[field];
      })
      seneca.act({role:'cd-users', cmd:'update', user: updatedFields}, function(err, res){
        if (err) return done(err);

        var forumProfile = _.clone(profile);
        forumProfile.username = forumProfile.name; 
        
        seneca.act({role:'cd-nodebb-api', cmd:'update', user: forumProfile}, function(err, res){
          if (err) seneca.log.error(err);
          if (res.error) seneca.log.error('NodeBB Profile Sync Error: ' + res.error);

          var query = {userId: profile.userId};
          seneca.act({role: 'cd-profiles', cmd: 'list', query: query, user: args.user}, done);
        });
      });
    });
  }

  //TODO: clean up with async

  function cmd_save_youth_profile(args, done){
    var profile = args.profile;
    profile.parents = [];
    profile.parents.push(args.user);

    if(profile.id){
      profile = _.omit(profile, immutableFields);
    }

    var initUserType =  profile.userTypes[0];
    var password = profile.password;

    var nick = profile.alias || profile.name;

    var user = {
      name: profile.name,
      nick: nick,
      email: profile.email,
      initUserType: {name : initUserType},
      password: password,
      roles: ['basic-user']
    };

    function registerUser(youth, done) {

      if(youth) {
        delete user.email;
        delete user.password;
      }

      seneca.act({role: 'user', cmd: 'register'}, user, function(err, data){
        if(err) return done(err);

        //TODO update errors on front-end
        if(!data.ok) return done(data.why);

        profile.userId = data && data.user && data.user.id;
        profile.userType = data && data.user && data.user.initUserType && data.user.initUserType.name;

        profile = _.omit(profile,['userTypes', 'password']);

        saveChild(profile, args.user, done);

      });
    }

    function addUserToParentsDojos(profile, done) {
      var parentsDojos = [];
      var userType = profile.userType;
      async.each(profile.parents, function (parent, cb) {
        //Load parents dojos
        var query = {userId: parent};

        seneca.act({role: 'cd-dojos', cmd: 'load_usersdojos', query: query}, function(err, usersDojos){
          if(err) return done(err);
          _.each(usersDojos, function (userDojo) {
            parentsDojos.push(userDojo.dojoId);
          });
          cb();
        });
      }, function (err) {
        async.each(parentsDojos, function (parentDojo, cb) {
          var userDojo = {
            userId:profile.userId,
            owner:0,
            dojoId:parentDojo,
            userTypes:[userType]
          };
          seneca.act({role: 'cd-dojos', cmd: 'save_usersdojos', userDojo: userDojo}, cb);
        }, done);
      });

    }

    if(initUserType === 'attendee-o13'){

      async.waterfall([
        async.apply(registerUser, false),
        addUserToParentsDojos
      ], function (err, res) {
        if(err) return done(null, {error: err})
        return done(null, res);
      });

    } else if(initUserType === 'attendee-u13') {

      async.waterfall([
        async.apply(registerUser, true),
        addUserToParentsDojos
      ], function (err, res) {
        if(err) return done(null, {error: err});
        return done(null, res);
      });
    }
  }

  function cmd_update_youth(args, done){
    if(!_.contains(args.profile.parents, args.user)){
      return done(new Error('Not authorized to update profile'));
    }
    var profile = args.profile;
    var derivedFields = ['password','userTypes', 'myChild', 'ownProfileFlag', 'dojos'];

    var fieldsToBeRemoved = _.union(derivedFields, immutableFields);

    profile = _.omit(profile, fieldsToBeRemoved);
    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, function(err, profile){
      if(err){
        return done(err);
      }

      return done(null, profile);
    });
  }

  function saveChild(profile, parentId, done){
    if(_.contains(profile.parents, parentId)){
      seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, function(err, profile){
        if(err){
          return done(err);
        }

        seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId: parentId}, function(err, results){
          var parent = results[0];

          if(err){
            return done(err);
          }

          parent.children = parent.children ? parent.children : [];
          parent.children.push(profile.userId);

          parent.save$(function(err){
            if(err){
              return done(err);
            }

            return done(null, profile);
          });
        });

      });
    } else {
      return done(new Error('Cannot save child'));
    }
  }

  function cmd_list(args, done){
    var query = args.query;
    var publicFields = [];

    async.waterfall([
      getProfile,
      getUsersDojos,
      getDojosForUser,
      assignUserTypesAndUserPermissions,
      addFlags,
      optionalFieldsFilter,
      privateFilter,
      publicProfilesFilter,
      under13Filter,
      resolveChildren,
      resolveParents
      ],function(err, profile){
        if(err) return done(null, {error: err});
        return done(null, profile);
      });

    function getProfile(done){
      var query = args.query;

      if(!query.userId){
        return done(new Error('Internal Error'));
      }

      var publicFields = [];
      seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId:query.userId}, function(err, results){
        if(err){
          return done(err);
        }

        var profile = results[0];
        if(!profile || !profile.userId){
          return done(new Error('Invalid Profile'));
        }

        return done(null, profile);
      });
    }

    function getUsersDojos(profile, done){
      var query = {userId: profile.userId};

      seneca.act({role: 'cd-dojos', cmd: 'load_usersdojos', query: {userId: query.userId}}, function(err, usersDojos){
        if(err){
          return done(err);
        }

        return done(null, profile, usersDojos);
      });
    }

    function getDojosForUser(profile, usersDojos, done){

      seneca.act({role: 'cd-dojos', cmd: 'dojos_for_user', id: profile.userId}, function(err, dojos){
        if(err){
          return done(err);
        }

        profile.dojos = _.map(dojos, function(dojo){
          return {id: dojo.id, name: dojo.name, urlSlug: dojo.urlSlug};
        });

        return done(null, profile, usersDojos);
      });
    }

    function assignUserTypesAndUserPermissions(profile, usersDojos, done){
      profile.userTypes = [];
      profile.userPermissions = [];

      if(_.isEmpty(usersDojos)){
        profile.userTypes.push(profile.userType);
      } else {
        profile.userTypes = _.flatten(_.pluck(usersDojos, 'userTypes'));
        profile.userTypes.push(profile.userType);
      }

      profile.userPermissions = usersDojos.userPermissions;

      return done(null, profile);
    }

    function addFlags(profile, done){
      profile.ownProfileFlag = profile && profile.userId === args.user ? true : false;
      profile.myChild = _.contains(profile.parents, args.user) ? true : false;
      profile.isTicketingAdmin = _.find(profile.userPermissions, function (profileUserPermission) {
        return profileUserPermission.name === 'ticketing-admin';
      });
      return done(null, profile);
    }

    function optionalFieldsFilter(profile, done) {
      seneca.act({role: 'cd-users', cmd: 'load_champions_for_user', userId: profile.userId}, function (err, champions) {
        if(err) return done(err);
        profile.requestingUserIsChampion = _.find(champions, function (champion) {
          return champion.id === args.user;
        });

        seneca.act({role: 'cd-users', cmd: 'load_dojo_admins_for_user', userId: profile.userId}, function (err, dojoAdmins) {
          if(err) return done(err);
          profile.requestingUserIsDojoAdmin = _.find(dojoAdmins, function (dojoAdmin) {
            return dojoAdmin.id === args.user;
          });

          var allowedFields = [];

          if(_.contains(profile.userTypes, 'attendee-o13')){
            allowedFields = _.union(allowedFields, allowedOptionalFields['attendee-o13']);
          }

          if(_.contains(profile.userTypes, 'champion')){
            allowedFields = _.union(allowedFields, allowedOptionalFields['champion']);
          }

          if(_.contains(profile.userTypes, 'mentor')) {
            allowedFields = _.union(allowedFields, allowedOptionalFields['mentor']);
          }

          var keysToOmit = [];
          if(!profile.ownProfileFlag && !profile.myChild && !profile.isTicketingAdmin && !profile.requestingUserIsChampion && !profile.requestingUserIsDojoAdmin){
            _.forOwn(profile.optionalHiddenFields, function(value, key){
              if(value && _.contains(allowedFields, key)){
                keysToOmit.push(key);
              }
            });
          }
          profile = _.omit(profile, keysToOmit);
          return done(null, profile);
        });
      });
    }

    function privateFilter(profile, done){
      if(profile.ownProfileFlag || profile.myChild || profile.isTicketingAdmin || profile.requestingUserIsChampion || profile.requestingUserIsDojoAdmin) {
        return done(null, profile);
      }

      if(profile.private){
        profile = {};
      }

      return done(null, profile);
    }

    //TODO cdf-admin role should be able to see all profiles
    function publicProfilesFilter(profile, done) {
      var publicProfileFlag = !profile.requestingUserIsDojoAdmin && !profile.requestingUserIsChampion && !profile.ownProfileFlag && !profile.myChild && !profile.isTicketingAdmin && ( !_.contains(profile.userTypes, 'attendee-u13') || !_.contains(profile.userTypes, 'parent-guardian'));
      if(publicProfileFlag){
         _.each(profile.userTypes, function(userType) {
          publicFields = _.union(publicFields, fieldWhiteList[userType]);
        });

        if(_.contains(profile.userTypes, 'attendee-o13')){
          publicFields = _.remove(publicFields, function(publicField){
            var idx =  youthBlackList.indexOf(publicField);

            return idx > -1 ? false : true;
          });
        }

        //Add optional hidden fields to publicFields if they are set to false.
        _.forOwn(profile.optionalHiddenFields, function(value, key){
          if(!value){
            publicFields.push(key);
          }
        });

        profile = _.pick(profile, publicFields);
        return done(null, profile);
      } else {
        return done(null, profile);
      }
    }

    function under13Filter(profile, done){
      //Ensure that only parents of children can retrieve their full public profile
      if(_.contains(profile.userTypes, 'attendee-u13') && !_.contains(profile.parents, args.user) && !profile.requestingUserIsChampion && !profile.requestingUserIsDojoAdmin) {
        profile = {};
        return done(null, profile);
      }
      return done(null, profile);
    }


    function resolveChildren(profile, done){
      var resolvedChildren = [];

      if(!_.isEmpty(profile.children) && _.contains(profile.userTypes, 'parent-guardian')){
        async.each(profile.children, function(child, callback){
          seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId: child}, function(err, results){
            if(err){
              return callback(err);
            }
            resolvedChildren.push(results[0]);
            return callback();
          });
        }, function(err){
          if(err){
            return done(err);
          }

          profile.resolvedChildren = resolvedChildren;

          return done(null, profile);
        });
      } else {
        profile.resolvedChildren = resolvedChildren;

        return done(null, profile);
      }
    }

    function resolveParents(profile, done){
      var resolvedParents = [];

      if(!_.isEmpty(profile.parents)){
        async.each(profile.parents, function(parent, callback){
          seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId: parent}, function(err, results){
            if(err){
              return callback(err);
            }
            resolvedParents.push(results[0]);
            return callback();
          });
        }, function(err){
          if(err){
            return done(err);
          }

          profile.resolvedParents = resolvedParents;

          return done(null, profile);
        });
      } else {
        profile.resolvedParents = resolvedParents;

        return done(null, profile);
      }
    }
  }

  function cmd_save(args, done) {
    var profile = args.profile;

    var profileKeys = _.keys(profile);
    var missingKeys = _.difference(requiredProfileFields, profileKeys);
    if(_.isEmpty(missingKeys)) profile.requiredFieldsComplete = true;

    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, done);
  }

  function cmd_invite_parent_guardian(args, done){
    var data = args.data;
    var invitedParentEmail = data.invitedParentEmail;
    var childId = data.childId;
    var emailSubject = data.emailSubject;
    var requestingUserId = args.user;
    var zenHostname = args.zenHostname;

    var childQuery = {
      userId: childId
    };

    async.waterfall([
      resolveParent,
      resolveChild,
      updateChildProfile,
      sendEmail,
    ], function (err, result) {
      if(err) return done(null, {ok: false, why: err.message});
      return done(null, result);
    });

    function resolveParent(done) {
      seneca.act({role: plugin, cmd: 'search', query: {email: invitedParentEmail}}, function (err, results) {
        if(err) return done(err);
        return done(null, results[0]);
      });
    }

    function resolveChild(parentProfile, done){
      if(!parentProfile) return done(new Error('Parent profile does not exist.'));
      seneca.act({role: plugin, cmd: 'search'}, {query: childQuery}, function(err, results){
        if(err) return done(err);
        if(_.isEmpty(results)) return done(new Error('Unable to find child profile'));
        return done(null, parentProfile, results[0]);
      });
    }

    function updateChildProfile(parentProfile, childProfile, done) {
      var inviteToken = {
        id: shortid.generate(),
        childId: childId,
        parentId: parentProfile.userId,
        parentEmail: invitedParentEmail,
        timestamp: new Date()
      };

      if(!childProfile.parentInvites) childProfile.parentInvites = [];
      childProfile.parentInvites.push(inviteToken);
      childProfile.parentInvites = _.chain(childProfile.parentInvites)
        .sortBy(function (parentInvite) {
          return parentInvite.timestamp;
        })
        .reverse()
        .uniq(function (parentInvite) {
          return parentInvite.parentEmail;
        })
        .value();

      seneca.act({role: plugin, cmd: 'save', profile: childProfile}, function (err, result) {
        if(err) return done(err);
        return done(null, parentProfile, result, inviteToken);
      });
    }

    function sendEmail(parentProfile, childProfile, inviteToken, done){
      if(!childProfile || !parentProfile) return done(new Error('An error has occured while sending email'));

      var content = {
        link: 'http://' + zenHostname + '/dashboard/accept_parent_guardian_request/' + childProfile.id + '/' + inviteToken.id,
        childName: childProfile.name,
        parentName: parentProfile.name,
        year: moment(new Date()).format('YYYY')
      };

      var locality = args.locality || 'en_US';
      var code = 'invite-parent-guardian-' + locality;
      var to =  invitedParentEmail;
      seneca.act({role:'email-notifications', cmd: 'send', to:to, content:content, code: code, subject: emailSubject}, done);
    }

  }

  function cmd_accept_parent_invite(args, done) {
    var data = args.data;
    var inviteTokenId = data.inviteToken;
    var childProfileId = data.childProfileId;
    var requestingUserId = args.user;

    async.waterfall([
      validateRequestingUserIsParent,
      loadInvite,
      updateParentProfile,
      updateNinjaProfile,
      removeInviteToken
    ], done);

    function validateRequestingUserIsParent(done) {
      seneca.act({role: plugin, cmd: 'list_query', query: {userId: requestingUserId}}, function (err, requestingUserProfiles) {
        if(err) return done(err);
        var requestingUserProfile = requestingUserProfiles[0];
        if(requestingUserProfile.userType === 'parent-guardian') return done();
        seneca.act({role: 'cd-dojos', cmd: 'load_usersdojos', query: {userId: requestingUserId}}, function (err, usersDojos) {
          if(err) return done(err);
          var parentTypeFound = _.find(usersDojos, function (userDojo) {
            return _.contains(userDojo.userTypes, 'parent-guardian');
          });
          if(parentTypeFound) return done();
          return done(new Error('You must have the parent/guardian user type to accept this invite'));
        });
      });
    }

    function loadInvite(done) {
      seneca.act({role: plugin, cmd: 'load', id: childProfileId}, function (err, childProfile) {
        if(err) return done(err);
        var inviteTokenFound = _.find(childProfile.parentInvites, function (parentInvite) {
          return parentInvite.id === inviteTokenId;
        });
        if(!inviteTokenFound) return done(new Error('Invite token not found'));
        if(requestingUserId !== inviteTokenFound.parentId) return done(new Error('Only the invited parent can approve this request.'));
        return done(null, inviteTokenFound);
      });
    }

    function updateParentProfile(inviteToken, done) {
      seneca.act({role: plugin, cmd: 'list_query', query:{userId: inviteToken.parentId}}, function (err, parentProfiles) {
        if(err) return done(err);
        var parentProfile = parentProfiles[0];
        if(!parentProfile.children) parentProfile.children = [];
        parentProfile.children.push(inviteToken.childId);
        parentProfile.children = _.uniq(parentProfile.children);
        seneca.act({role: plugin, cmd: 'save', profile: parentProfile}, function (err, parentProfile) {
          if(err) return done(err);
          return done(null, inviteToken);
        });
      });
    }

    function updateNinjaProfile(inviteToken, done) {
      seneca.act({role: plugin, cmd: 'list_query', query:{userId: inviteToken.childId}}, function (err, ninjaProfiles) {
        if(err) return done(err);
        var ninjaProfile = ninjaProfiles[0];
        if(!ninjaProfile.parents) ninjaProfile.parents = [];
        ninjaProfile.parents.push(inviteToken.parentId);
        ninjaProfile.parents = _.uniq(ninjaProfile.parents);
        seneca.act({role: plugin, cmd: 'save', profile: ninjaProfile}, done);
      });
    }

    function removeInviteToken(ninjaProfile, done) {
      ninjaProfile.parentInvites = _.without(ninjaProfile.parentInvites, _.findWhere(ninjaProfile.parentInvites, {id: inviteTokenId}));
      seneca.act({role: plugin, cmd: 'save', profile: ninjaProfile}, done);
    }

  }

  function cmd_load_hidden_fields(args, done){
    done(null, hiddenFields);
  }

  function cmd_change_avatar(args, done){
    var file = args.file;
    if(!_.contains(args.fileType, 'image')) return done(new Error('Avatar upload: file must be an image.'));
    if(file.length > 5242880) return done(new Error('Avatar upload: max file size of 5242880 bytes exceeded.'));
    //pg conf properties
    options.postgresql.database= options.postgresql.name;
    options.postgresql.user= options.postgresql.username;

    pg.connect(options.postgresql, function (err, client) {
      if (err) { return seneca.log.error('Could not connect to postgres', err); }

      var man = new LargeObjectManager(client);

      client.query('BEGIN', function (err) {
        if (err) {
          seneca.log.error('Unable to create transaction');
          done(err);
          return;
        }

        var bufferSize = 16384;
        man.createAndWritableStream(bufferSize, function (err, oid, stream) {
          var noop = function () {};
          var avatarInfo = {
            oid: oid.toString(),
            sizeBytes: 0,
            name: args.fileName,
            type: args.fileType
          };

          if (err) {
            seneca.log.error('Unable to create a new large object');
            client.end();
            done(err);
            done = noop;
            return;
          }

          var buf = new Buffer(file, 'base64');

          stream.write(buf, 'base64', function() {
            stream.end();
          });

          stream.on('data', function (chunk) {
            seneca.log.info('got ' + chunk.length + ' bytes of data');
            avatarInfo.sizeBytes += chunk.length;
          });

          stream.on('finish', function () {
            seneca.log.info('Uploaded largeObject. committing...', oid);
            client.query('COMMIT', function () {
              client.end();
              seneca.log.info('Saved LargeObject', oid);

              //update profile record with avatarInfo
              var profile = {
                id: args.profileId,
                avatar: avatarInfo
              }
              seneca.act({role: plugin, cmd: 'save'}, {profile: profile},function(err, profile){
                if(err){
                  return done(err);
                }

                done(undefined, profile);
                done = noop;
              })

            });
          });

          stream.on('error', function (err) {
            seneca.log.error('postgresql filestore error', err);
            done(err);
            done = noop;<commit_msg>updated to now update the picture on the forum when the picture is updated<commit_after>'use strict';

var pg = require('pg');
var LargeObjectManager = require('pg-large-object').LargeObjectManager;
var shortid = require('shortid');
var moment = require('moment');

module.exports = function(options) {
  var seneca = this;

  var PARENT_GUARDIAN_PROFILE_ENTITY = 'cd/profiles';
  var plugin = 'cd-profiles';
  var _ = require('lodash');
  var async = require('async');
  var uuid = require('node-uuid');
  var hiddenFields = require('./data/hidden-fields.js');
  var fs = require('fs');
  var path = require('path');
  var so = seneca.options();

  var syncedFields = [
    'name',
    'email',
    'phone'
  ]

  var mentorPublicFields = [
    'name',
    'languagesSpoken',
    'programmingLanguages',
    'linkedin',
    'twitter',
    'userTypes',
    'dojos',
    'badges',
    'optionalHiddenFields'
  ];

  var championPublicFields = [
    'id',
    'name',
    'languagesSpoken',
    'programmingLanguages',
    'linkedin',
    'twitter',
    'userTypes',
    'projects',
    'notes',
    'dojos',
    'optionalHiddenFields'
  ];

  var attendeeO13PublicFields = [
    'alias',
    'linkedin',
    'twitter',
    'badges',
    'userTypes',
    'optionalHiddenFields'
  ];

  var fieldWhiteList = {
    'mentor': mentorPublicFields,
    'champion': championPublicFields,
    'attendee-o13': attendeeO13PublicFields
  };

  ///var allowedOptionalFieldsYouth = ['dojos', 'linkedin', 'twitter', 'badges'];
  var allowedOptionalFieldsYouth = _.filter(hiddenFields, function(field){
    if(_.contains(field.allowedUserTypes, 'attendee-o13')) return field.modelName;
  });

  //var allowedOptionalFieldsChampion = ['notes', 'projects'];
  var allowedOptionalFieldsChampion = _.map(hiddenFields, function(field){
    if(_.contains(field.allowedUserTypes, 'champion')) return field.modelName;
  });

  var allowedOptionalFieldsMentor = _.map(hiddenFields, function (field) {
    if(_.contains(field.allowedUserTypes, 'mentor')) return field.modelName;
  });

  var allowedOptionalFields = {
    'champion': allowedOptionalFieldsChampion,
    'attendee-o13': allowedOptionalFieldsYouth,
    'mentor': allowedOptionalFieldsMentor
  };

  var immutableFields = ['userType', 'avatar'];

  var youthBlackList = ['name'];

  var requiredProfileFields = ['name', 'alias', 'dob', 'country', 'place', 'address'];

  //var userTypes = ['champion', 'mentor', 'parent-guardian', 'attendee-o13', 'attendee-u13'];
  //var userTypes = ['attendee-u13', 'attendee-o13', 'parent-guardian', 'mentor', 'champion'];


  seneca.add({role: plugin, cmd: 'create'}, cmd_create);
  seneca.add({role: plugin, cmd: 'list'}, cmd_list);
  seneca.add({role: plugin, cmd: 'load'}, cmd_load);
  seneca.add({role: plugin, cmd: 'save-youth-profile'}, cmd_save_youth_profile);
  seneca.add({role: plugin, cmd: 'save'}, cmd_save);
  seneca.add({role: plugin, cmd: 'update-youth-profile'}, cmd_update_youth);
  seneca.add({role: plugin, cmd: 'invite-parent-guardian'}, cmd_invite_parent_guardian);
  seneca.add({role: plugin, cmd: 'search'}, cmd_search);
  seneca.add({role: plugin, cmd: 'accept_parent_invite'}, cmd_accept_parent_invite);
  seneca.add({role: plugin, cmd: 'load_hidden_fields'}, cmd_load_hidden_fields);
  seneca.add({role: plugin, cmd: 'list_query'}, cmd_list_query);
  seneca.add({role: plugin, cmd: 'change_avatar'}, cmd_change_avatar);
  seneca.add({role: plugin, cmd: 'get_avatar'}, cmd_get_avatar);
  seneca.add({role: plugin, cmd: 'load_parents_for_user'}, cmd_load_parents_for_user);
  seneca.add({role: plugin, cmd: 'invite_ninja'}, cmd_invite_ninja);
  seneca.add({role: plugin, cmd: 'approve_invite_ninja'}, cmd_approve_invite_ninja);


  function cmd_search(args, done){
    if(!args.query){
      return done(new Error('Empty query'));
    }

    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$(args.query, done);
  }

  function cmd_create(args, done){
    var profile = args.profile;

    var profileKeys = _.keys(profile);
    var missingKeys = _.difference(requiredProfileFields, profileKeys);
    if(_.isEmpty(missingKeys)) profile.requiredFieldsComplete = true;

    if(args.user !== profile.userId) return done(null, new Error('Profiles can only be saved by the profile user.'));

    if(profile.id){
      profile = _.omit(profile, immutableFields);
    }

    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, function(err, profile){
      if(err) return done(err);
      var updatedFields = {};
      updatedFields.id = profile.userId;
      _.each(syncedFields, function(field){
        updatedFields[field] = profile[field];
      })
      seneca.act({role:'cd-users', cmd:'update', user: updatedFields}, function(err, res){
        if (err) return done(err);

        var forumProfile = _.clone(profile);
        forumProfile.username = forumProfile.name; 
        
        seneca.act({role:'cd-nodebb-api', cmd:'update', user: forumProfile}, function(err, res){
          if (err) seneca.log.error(err);
          if (res.error) seneca.log.error('NodeBB Profile Sync Error: ' + res.error);

          var query = {userId: profile.userId};
          seneca.act({role: 'cd-profiles', cmd: 'list', query: query, user: args.user}, done);
        });
      });
    });
  }

  //TODO: clean up with async

  function cmd_save_youth_profile(args, done){
    var profile = args.profile;
    profile.parents = [];
    profile.parents.push(args.user);

    if(profile.id){
      profile = _.omit(profile, immutableFields);
    }

    var initUserType =  profile.userTypes[0];
    var password = profile.password;

    var nick = profile.alias || profile.name;

    var user = {
      name: profile.name,
      nick: nick,
      email: profile.email,
      initUserType: {name : initUserType},
      password: password,
      roles: ['basic-user']
    };

    function registerUser(youth, done) {

      if(youth) {
        delete user.email;
        delete user.password;
      }

      seneca.act({role: 'user', cmd: 'register'}, user, function(err, data){
        if(err) return done(err);

        //TODO update errors on front-end
        if(!data.ok) return done(data.why);

        profile.userId = data && data.user && data.user.id;
        profile.userType = data && data.user && data.user.initUserType && data.user.initUserType.name;

        profile = _.omit(profile,['userTypes', 'password']);

        saveChild(profile, args.user, done);

      });
    }

    function addUserToParentsDojos(profile, done) {
      var parentsDojos = [];
      var userType = profile.userType;
      async.each(profile.parents, function (parent, cb) {
        //Load parents dojos
        var query = {userId: parent};

        seneca.act({role: 'cd-dojos', cmd: 'load_usersdojos', query: query}, function(err, usersDojos){
          if(err) return done(err);
          _.each(usersDojos, function (userDojo) {
            parentsDojos.push(userDojo.dojoId);
          });
          cb();
        });
      }, function (err) {
        async.each(parentsDojos, function (parentDojo, cb) {
          var userDojo = {
            userId:profile.userId,
            owner:0,
            dojoId:parentDojo,
            userTypes:[userType]
          };
          seneca.act({role: 'cd-dojos', cmd: 'save_usersdojos', userDojo: userDojo}, cb);
        }, done);
      });

    }

    if(initUserType === 'attendee-o13'){

      async.waterfall([
        async.apply(registerUser, false),
        addUserToParentsDojos
      ], function (err, res) {
        if(err) return done(null, {error: err})
        return done(null, res);
      });

    } else if(initUserType === 'attendee-u13') {

      async.waterfall([
        async.apply(registerUser, true),
        addUserToParentsDojos
      ], function (err, res) {
        if(err) return done(null, {error: err});
        return done(null, res);
      });
    }
  }

  function cmd_update_youth(args, done){
    if(!_.contains(args.profile.parents, args.user)){
      return done(new Error('Not authorized to update profile'));
    }
    var profile = args.profile;
    var derivedFields = ['password','userTypes', 'myChild', 'ownProfileFlag', 'dojos'];

    var fieldsToBeRemoved = _.union(derivedFields, immutableFields);

    profile = _.omit(profile, fieldsToBeRemoved);
    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, function(err, profile){
      if(err){
        return done(err);
      }

      return done(null, profile);
    });
  }

  function saveChild(profile, parentId, done){
    if(_.contains(profile.parents, parentId)){
      seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, function(err, profile){
        if(err){
          return done(err);
        }

        seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId: parentId}, function(err, results){
          var parent = results[0];

          if(err){
            return done(err);
          }

          parent.children = parent.children ? parent.children : [];
          parent.children.push(profile.userId);

          parent.save$(function(err){
            if(err){
              return done(err);
            }

            return done(null, profile);
          });
        });

      });
    } else {
      return done(new Error('Cannot save child'));
    }
  }

  function cmd_list(args, done){
    var query = args.query;
    var publicFields = [];

    async.waterfall([
      getProfile,
      getUsersDojos,
      getDojosForUser,
      assignUserTypesAndUserPermissions,
      addFlags,
      optionalFieldsFilter,
      privateFilter,
      publicProfilesFilter,
      under13Filter,
      resolveChildren,
      resolveParents
      ],function(err, profile){
        if(err) return done(null, {error: err});
        return done(null, profile);
      });

    function getProfile(done){
      var query = args.query;

      if(!query.userId){
        return done(new Error('Internal Error'));
      }

      var publicFields = [];
      seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId:query.userId}, function(err, results){
        if(err){
          return done(err);
        }

        var profile = results[0];
        if(!profile || !profile.userId){
          return done(new Error('Invalid Profile'));
        }

        return done(null, profile);
      });
    }

    function getUsersDojos(profile, done){
      var query = {userId: profile.userId};

      seneca.act({role: 'cd-dojos', cmd: 'load_usersdojos', query: {userId: query.userId}}, function(err, usersDojos){
        if(err){
          return done(err);
        }

        return done(null, profile, usersDojos);
      });
    }

    function getDojosForUser(profile, usersDojos, done){

      seneca.act({role: 'cd-dojos', cmd: 'dojos_for_user', id: profile.userId}, function(err, dojos){
        if(err){
          return done(err);
        }

        profile.dojos = _.map(dojos, function(dojo){
          return {id: dojo.id, name: dojo.name, urlSlug: dojo.urlSlug};
        });

        return done(null, profile, usersDojos);
      });
    }

    function assignUserTypesAndUserPermissions(profile, usersDojos, done){
      profile.userTypes = [];
      profile.userPermissions = [];

      if(_.isEmpty(usersDojos)){
        profile.userTypes.push(profile.userType);
      } else {
        profile.userTypes = _.flatten(_.pluck(usersDojos, 'userTypes'));
        profile.userTypes.push(profile.userType);
      }

      profile.userPermissions = usersDojos.userPermissions;

      return done(null, profile);
    }

    function addFlags(profile, done){
      profile.ownProfileFlag = profile && profile.userId === args.user ? true : false;
      profile.myChild = _.contains(profile.parents, args.user) ? true : false;
      profile.isTicketingAdmin = _.find(profile.userPermissions, function (profileUserPermission) {
        return profileUserPermission.name === 'ticketing-admin';
      });
      return done(null, profile);
    }

    function optionalFieldsFilter(profile, done) {
      seneca.act({role: 'cd-users', cmd: 'load_champions_for_user', userId: profile.userId}, function (err, champions) {
        if(err) return done(err);
        profile.requestingUserIsChampion = _.find(champions, function (champion) {
          return champion.id === args.user;
        });

        seneca.act({role: 'cd-users', cmd: 'load_dojo_admins_for_user', userId: profile.userId}, function (err, dojoAdmins) {
          if(err) return done(err);
          profile.requestingUserIsDojoAdmin = _.find(dojoAdmins, function (dojoAdmin) {
            return dojoAdmin.id === args.user;
          });

          var allowedFields = [];

          if(_.contains(profile.userTypes, 'attendee-o13')){
            allowedFields = _.union(allowedFields, allowedOptionalFields['attendee-o13']);
          }

          if(_.contains(profile.userTypes, 'champion')){
            allowedFields = _.union(allowedFields, allowedOptionalFields['champion']);
          }

          if(_.contains(profile.userTypes, 'mentor')) {
            allowedFields = _.union(allowedFields, allowedOptionalFields['mentor']);
          }

          var keysToOmit = [];
          if(!profile.ownProfileFlag && !profile.myChild && !profile.isTicketingAdmin && !profile.requestingUserIsChampion && !profile.requestingUserIsDojoAdmin){
            _.forOwn(profile.optionalHiddenFields, function(value, key){
              if(value && _.contains(allowedFields, key)){
                keysToOmit.push(key);
              }
            });
          }
          profile = _.omit(profile, keysToOmit);
          return done(null, profile);
        });
      });
    }

    function privateFilter(profile, done){
      if(profile.ownProfileFlag || profile.myChild || profile.isTicketingAdmin || profile.requestingUserIsChampion || profile.requestingUserIsDojoAdmin) {
        return done(null, profile);
      }

      if(profile.private){
        profile = {};
      }

      return done(null, profile);
    }

    //TODO cdf-admin role should be able to see all profiles
    function publicProfilesFilter(profile, done) {
      var publicProfileFlag = !profile.requestingUserIsDojoAdmin && !profile.requestingUserIsChampion && !profile.ownProfileFlag && !profile.myChild && !profile.isTicketingAdmin && ( !_.contains(profile.userTypes, 'attendee-u13') || !_.contains(profile.userTypes, 'parent-guardian'));
      if(publicProfileFlag){
         _.each(profile.userTypes, function(userType) {
          publicFields = _.union(publicFields, fieldWhiteList[userType]);
        });

        if(_.contains(profile.userTypes, 'attendee-o13')){
          publicFields = _.remove(publicFields, function(publicField){
            var idx =  youthBlackList.indexOf(publicField);

            return idx > -1 ? false : true;
          });
        }

        //Add optional hidden fields to publicFields if they are set to false.
        _.forOwn(profile.optionalHiddenFields, function(value, key){
          if(!value){
            publicFields.push(key);
          }
        });

        profile = _.pick(profile, publicFields);
        return done(null, profile);
      } else {
        return done(null, profile);
      }
    }

    function under13Filter(profile, done){
      //Ensure that only parents of children can retrieve their full public profile
      if(_.contains(profile.userTypes, 'attendee-u13') && !_.contains(profile.parents, args.user) && !profile.requestingUserIsChampion && !profile.requestingUserIsDojoAdmin) {
        profile = {};
        return done(null, profile);
      }
      return done(null, profile);
    }


    function resolveChildren(profile, done){
      var resolvedChildren = [];

      if(!_.isEmpty(profile.children) && _.contains(profile.userTypes, 'parent-guardian')){
        async.each(profile.children, function(child, callback){
          seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId: child}, function(err, results){
            if(err){
              return callback(err);
            }
            resolvedChildren.push(results[0]);
            return callback();
          });
        }, function(err){
          if(err){
            return done(err);
          }

          profile.resolvedChildren = resolvedChildren;

          return done(null, profile);
        });
      } else {
        profile.resolvedChildren = resolvedChildren;

        return done(null, profile);
      }
    }

    function resolveParents(profile, done){
      var resolvedParents = [];

      if(!_.isEmpty(profile.parents)){
        async.each(profile.parents, function(parent, callback){
          seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).list$({userId: parent}, function(err, results){
            if(err){
              return callback(err);
            }
            resolvedParents.push(results[0]);
            return callback();
          });
        }, function(err){
          if(err){
            return done(err);
          }

          profile.resolvedParents = resolvedParents;

          return done(null, profile);
        });
      } else {
        profile.resolvedParents = resolvedParents;

        return done(null, profile);
      }
    }
  }

  function cmd_save(args, done) {
    var profile = args.profile;

    var profileKeys = _.keys(profile);
    var missingKeys = _.difference(requiredProfileFields, profileKeys);
    if(_.isEmpty(missingKeys)) profile.requiredFieldsComplete = true;

    seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).save$(profile, done);
  }

  function cmd_invite_parent_guardian(args, done){
    var data = args.data;
    var invitedParentEmail = data.invitedParentEmail;
    var childId = data.childId;
    var emailSubject = data.emailSubject;
    var requestingUserId = args.user;
    var zenHostname = args.zenHostname;

    var childQuery = {
      userId: childId
    };

    async.waterfall([
      resolveParent,
      resolveChild,
      updateChildProfile,
      sendEmail,
    ], function (err, result) {
      if(err) return done(null, {ok: false, why: err.message});
      return done(null, result);
    });

    function resolveParent(done) {
      seneca.act({role: plugin, cmd: 'search', query: {email: invitedParentEmail}}, function (err, results) {
        if(err) return done(err);
        return done(null, results[0]);
      });
    }

    function resolveChild(parentProfile, done){
      if(!parentProfile) return done(new Error('Parent profile does not exist.'));
      seneca.act({role: plugin, cmd: 'search'}, {query: childQuery}, function(err, results){
        if(err) return done(err);
        if(_.isEmpty(results)) return done(new Error('Unable to find child profile'));
        return done(null, parentProfile, results[0]);
      });
    }

    function updateChildProfile(parentProfile, childProfile, done) {
      var inviteToken = {
        id: shortid.generate(),
        childId: childId,
        parentId: parentProfile.userId,
        parentEmail: invitedParentEmail,
        timestamp: new Date()
      };

      if(!childProfile.parentInvites) childProfile.parentInvites = [];
      childProfile.parentInvites.push(inviteToken);
      childProfile.parentInvites = _.chain(childProfile.parentInvites)
        .sortBy(function (parentInvite) {
          return parentInvite.timestamp;
        })
        .reverse()
        .uniq(function (parentInvite) {
          return parentInvite.parentEmail;
        })
        .value();

      seneca.act({role: plugin, cmd: 'save', profile: childProfile}, function (err, result) {
        if(err) return done(err);
        return done(null, parentProfile, result, inviteToken);
      });
    }

    function sendEmail(parentProfile, childProfile, inviteToken, done){
      if(!childProfile || !parentProfile) return done(new Error('An error has occured while sending email'));

      var content = {
        link: 'http://' + zenHostname + '/dashboard/accept_parent_guardian_request/' + childProfile.id + '/' + inviteToken.id,
        childName: childProfile.name,
        parentName: parentProfile.name,
        year: moment(new Date()).format('YYYY')
      };

      var locality = args.locality || 'en_US';
      var code = 'invite-parent-guardian-' + locality;
      var to =  invitedParentEmail;
      seneca.act({role:'email-notifications', cmd: 'send', to:to, content:content, code: code, subject: emailSubject}, done);
    }

  }

  function cmd_accept_parent_invite(args, done) {
    var data = args.data;
    var inviteTokenId = data.inviteToken;
    var childProfileId = data.childProfileId;
    var requestingUserId = args.user;

    async.waterfall([
      validateRequestingUserIsParent,
      loadInvite,
      updateParentProfile,
      updateNinjaProfile,
      removeInviteToken
    ], done);

    function validateRequestingUserIsParent(done) {
      seneca.act({role: plugin, cmd: 'list_query', query: {userId: requestingUserId}}, function (err, requestingUserProfiles) {
        if(err) return done(err);
        var requestingUserProfile = requestingUserProfiles[0];
        if(requestingUserProfile.userType === 'parent-guardian') return done();
        seneca.act({role: 'cd-dojos', cmd: 'load_usersdojos', query: {userId: requestingUserId}}, function (err, usersDojos) {
          if(err) return done(err);
          var parentTypeFound = _.find(usersDojos, function (userDojo) {
            return _.contains(userDojo.userTypes, 'parent-guardian');
          });
          if(parentTypeFound) return done();
          return done(new Error('You must have the parent/guardian user type to accept this invite'));
        });
      });
    }

    function loadInvite(done) {
      seneca.act({role: plugin, cmd: 'load', id: childProfileId}, function (err, childProfile) {
        if(err) return done(err);
        var inviteTokenFound = _.find(childProfile.parentInvites, function (parentInvite) {
          return parentInvite.id === inviteTokenId;
        });
        if(!inviteTokenFound) return done(new Error('Invite token not found'));
        if(requestingUserId !== inviteTokenFound.parentId) return done(new Error('Only the invited parent can approve this request.'));
        return done(null, inviteTokenFound);
      });
    }

    function updateParentProfile(inviteToken, done) {
      seneca.act({role: plugin, cmd: 'list_query', query:{userId: inviteToken.parentId}}, function (err, parentProfiles) {
        if(err) return done(err);
        var parentProfile = parentProfiles[0];
        if(!parentProfile.children) parentProfile.children = [];
        parentProfile.children.push(inviteToken.childId);
        parentProfile.children = _.uniq(parentProfile.children);
        seneca.act({role: plugin, cmd: 'save', profile: parentProfile}, function (err, parentProfile) {
          if(err) return done(err);
          return done(null, inviteToken);
        });
      });
    }

    function updateNinjaProfile(inviteToken, done) {
      seneca.act({role: plugin, cmd: 'list_query', query:{userId: inviteToken.childId}}, function (err, ninjaProfiles) {
        if(err) return done(err);
        var ninjaProfile = ninjaProfiles[0];
        if(!ninjaProfile.parents) ninjaProfile.parents = [];
        ninjaProfile.parents.push(inviteToken.parentId);
        ninjaProfile.parents = _.uniq(ninjaProfile.parents);
        seneca.act({role: plugin, cmd: 'save', profile: ninjaProfile}, done);
      });
    }

    function removeInviteToken(ninjaProfile, done) {
      ninjaProfile.parentInvites = _.without(ninjaProfile.parentInvites, _.findWhere(ninjaProfile.parentInvites, {id: inviteTokenId}));
      seneca.act({role: plugin, cmd: 'save', profile: ninjaProfile}, done);
    }

  }

  function cmd_load_hidden_fields(args, done){
    done(null, hiddenFields);
  }

  function cmd_change_avatar(args, done){
    var hostname = args.zenHostname;
    var file = args.file;
    if(!_.contains(args.fileType, 'image')) return done(new Error('Avatar upload: file must be an image.'));
    if(file.length > 5242880) return done(new Error('Avatar upload: max file size of 5242880 bytes exceeded.'));
    //pg conf properties
    options.postgresql.database= options.postgresql.name;
    options.postgresql.user= options.postgresql.username;

    pg.connect(options.postgresql, function (err, client) {
      if (err) { return seneca.log.error('Could not connect to postgres', err); }

      var man = new LargeObjectManager(client);

      client.query('BEGIN', function (err) {
        if (err) {
          seneca.log.error('Unable to create transaction');
          done(err);
          return;
        }

        var bufferSize = 16384;
        man.createAndWritableStream(bufferSize, function (err, oid, stream) {
          var noop = function () {};
          var avatarInfo = {
            oid: oid.toString(),
            sizeBytes: 0,
            name: args.fileName,
            type: args.fileType
          };

          if (err) {
            seneca.log.error('Unable to create a new large object');
            client.end();
            done(err);
            done = noop;
            return;
          }

          var buf = new Buffer(file, 'base64');

          stream.write(buf, 'base64', function() {
            stream.end();
          });

          stream.on('data', function (chunk) {
            seneca.log.info('got ' + chunk.length + ' bytes of data');
            avatarInfo.sizeBytes += chunk.length;
          });

          stream.on('finish', function () {
            seneca.log.info('Uploaded largeObject. committing...', oid);
            client.query('COMMIT', function () {
              client.end();
              seneca.log.info('Saved LargeObject', oid);

              //update profile record with avatarInfo
              var profile = {
                id: args.profileId,
                avatar: avatarInfo
              }

              seneca.act({role: plugin, cmd: 'save'}, {profile: profile},function(err, profile){
                if(err){
                  return done(err);
                }

                seneca.make$(PARENT_GUARDIAN_PROFILE_ENTITY).load$(profile.id, function(err, profile){
                  if (err) seneca.log.error(err);
                  
                  var protocol = process.env.PROTOCOL || 'http';

                  var forumProfile = _.clone(profile);
                  forumProfile.username = forumProfile.name;
  
                  forumProfile.uploadedpicture = protocol + '://'+hostname+'/api/1.0/profiles/'+profile.id+'/avatar_img'
                  forumProfile.picture = protocol + '://'+hostname+'/api/1.0/profiles/'+profile.id+'/avatar_img'
                      
                    seneca.act({role:'cd-nodebb-api', cmd:'update', user: forumProfile}, function(err, res){
                      if (err) seneca.log.error(err);
                      if (res.error) seneca.log.error('NodeBB Profile Sync Error: ' + res.error);
  
                    done(undefined, profile);
                    done = noop;
                  });
                });
              });
            });
          });

          stream.on('error', function (err) {
            seneca.log.error('postgresql filestore error', err);
            done(err);
            done = noop;
{'fraction_non_alphanumeric': 0.1333694616261469, 'fraction_numerical': 0.002765683105039897, 'mean_word_length': 2.688077188146106, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7074853', 'n_tokens_mistral': 15944, 'n_tokens_neox': 15566, 'n_words': 3256}