/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache 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://www.apache.org/licenses/LICENSE-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.
 */

'use strict';

const _ = require('lodash');

// Fire me up!

module.exports = {
    implements: 'services/users',
    inject: ['errors', 'settings', 'mongo', 'services/spaces', 'services/mails', 'services/activities', 'services/utils', 'agents-handler']
};

/**
 * @param mongo
 * @param errors
 * @param settings
 * @param {SpacesService} spacesService
 * @param {MailsService} mailsService
 * @param {ActivitiesService} activitiesService
 * @param {UtilsService} utilsService
 * @param {AgentsHandler} agentHnd
 * @returns {UsersService}
 */
module.exports.factory = (errors, settings, mongo, spacesService, mailsService, activitiesService, utilsService, agentHnd) => {
    class UsersService {
        /**
         * Save profile information.
         *
         * @param {String} host - The host
         * @param {Object} user - The user
         * @returns {Promise.<mongo.ObjectId>} that resolves account id of merge operation.
         */
        static create(host, user) {
            return mongo.Account.count().exec()
                .then((cnt) => {
                    user.admin = cnt === 0;
                    user.registered = new Date();
                    user.token = utilsService.randomString(settings.tokenLength);

                    return new mongo.Account(user);
                })
                .then((created) => {
                    return new Promise((resolve, reject) => {
                        mongo.Account.register(created, user.password, (err, registered) => {
                            if (err)
                                reject(err);

                            if (!registered)
                                reject(new errors.ServerErrorException('Failed to register user.'));

                            resolve(registered);
                        });
                    });
                })
                .then((registered) => {
                    registered.resetPasswordToken = utilsService.randomString(settings.tokenLength);

                    return registered.save()
                        .then(() => mongo.Space.create({name: 'Personal space', owner: registered._id}))
                        .then(() => {
                            mailsService.emailUserSignUp(host, registered);

                            return registered;
                        });
                });
        }

        /**
         * Save user.
         *
         * @param {Object} changed - The user
         * @returns {Promise.<mongo.ObjectId>} that resolves account id of merge operation.
         */
        static save(changed) {
            delete changed.admin;

            return mongo.Account.findById(changed._id).exec()
                .then((user) => {
                    if (!changed.password)
                        return Promise.resolve(user);

                    return new Promise((resolve, reject) => {
                        user.setPassword(changed.password, (err, _user) => {
                            if (err)
                                return reject(err);

                            delete changed.password;

                            resolve(_user);
                        });
                    });
                })
                .then((user) => {
                    if (!changed.email || user.email === changed.email)
                        return Promise.resolve(user);

                    return new Promise((resolve, reject) => {
                        mongo.Account.findOne({email: changed.email}, (err, _user) => {
                            // TODO send error to admin
                            if (err)
                                reject(new Error('Failed to check email!'));

                            if (_user && _user._id !== user._id)
                                reject(new Error('User with this email already registered!'));

                            resolve(user);
                        });
                    });
                })
                .then((user) => {
                    if (changed.token && user.token !== changed.token)
                        agentHnd.onTokenReset(user);

                    _.extend(user, changed);

                    return user.save();
                });
        }

        /**
         * Get list of user accounts and summary information.
         *
         * @returns {mongo.Account[]} - returns all accounts with counters object
         */
        static list(params) {
            return Promise.all([
                Promise.all([
                    mongo.Account.aggregate([
                        {$lookup: {from: 'spaces', localField: '_id', foreignField: 'owner', as: 'spaces'}},
                        {$project: {
                            _id: 1,
                            firstName: 1,
                            lastName: 1,
                            admin: 1,
                            email: 1,
                            company: 1,
                            country: 1,
                            lastLogin: 1,
                            lastActivity: 1,
                            spaces: {
                                $filter: {
                                    input: '$spaces',
                                    as: 'space',
                                    cond: {$eq: ['$$space.demo', false]}
                                }
                            }
                        }},
                        { $sort: {firstName: 1, lastName: 1}}
                    ]).exec(),
                    mongo.Cluster.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec(),
                    mongo.Cache.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec(),
                    mongo.DomainModel.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec(),
                    mongo.Igfs.aggregate([{$group: {_id: '$space', count: { $sum: 1 }}}]).exec()
                ]).then(([users, clusters, caches, models, igfs]) => {
                    const clustersMap = _.mapValues(_.keyBy(clusters, '_id'), 'count');
                    const cachesMap = _.mapValues(_.keyBy(caches, '_id'), 'count');
                    const modelsMap = _.mapValues(_.keyBy(models, '_id'), 'count');
                    const igfsMap = _.mapValues(_.keyBy(igfs, '_id'), 'count');

                    _.forEach(users, (user) => {
                        const counters = user.counters = {};

                        counters.clusters = _.sumBy(user.spaces, ({_id}) => clustersMap[_id]) || 0;
                        counters.caches = _.sumBy(user.spaces, ({_id}) => cachesMap[_id]) || 0;
                        counters.models = _.sumBy(user.spaces, ({_id}) => modelsMap[_id]) || 0;
                        counters.igfs = _.sumBy(user.spaces, ({_id}) => igfsMap[_id]) || 0;

                        delete user.spaces;
                    });

                    return users;
                }),
                activitiesService.total(params),
                activitiesService.detail(params)
            ])
                .then(([users, activitiesTotal, activitiesDetail]) => {
                    _.forEach(users, (user) => {
                        user.activitiesTotal = activitiesTotal[user._id];
                        user.activitiesDetail = activitiesDetail[user._id];
                    });

                    return users;
                });
        }

        /**
         * Remove account.
         *
         * @param {String} host.
         * @param {mongo.ObjectId|String} userId - The account id for remove.
         * @returns {Promise.<{rowsAffected}>} - The number of affected rows.
         */
        static remove(host, userId) {
            return mongo.Account.findByIdAndRemove(userId).exec()
                .then((user) => {
                    return spacesService.spaceIds(userId)
                        .then((spaceIds) => Promise.all([
                            mongo.Cluster.remove({space: {$in: spaceIds}}).exec(),
                            mongo.Cache.remove({space: {$in: spaceIds}}).exec(),
                            mongo.DomainModel.remove({space: {$in: spaceIds}}).exec(),
                            mongo.Igfs.remove({space: {$in: spaceIds}}).exec(),
                            mongo.Notebook.remove({space: {$in: spaceIds}}).exec(),
                            mongo.Space.remove({owner: userId}).exec()
                        ]))
                        .catch((err) => console.error(`Failed to cleanup spaces [user=${user.username}, err=${err}`))
                        .then(() => user);
                })
                .then((user) => mailsService.emailUserDeletion(host, user));
        }

        /**
         * Get account information.
         */
        static get(user, viewedUser) {
            if (_.isNil(user))
                return Promise.reject(new errors.AuthFailedException('The user profile service failed the sign in. User profile cannot be loaded.'));

            const becomeUsed = viewedUser && user.admin;

            if (becomeUsed)
                user = _.extend({}, viewedUser, {becomeUsed: true, becameToken: user.token});
            else
                user = user.toJSON();

            return mongo.Space.findOne({owner: user._id, demo: true}).exec()
                .then((demoSpace) => {
                    if (user && demoSpace)
                        user.demoCreated = true;

                    return user;
                });
        }
    }

    return UsersService;
};
