const _ = require('lodash'),
    Promise = require('bluebird'),
    validator = require('validator'),
    ObjectId = require('bson-objectid'),
    ghostBookshelf = require('./base'),
    baseUtils = require('./base/utils'),
    common = require('../lib/common'),
    security = require('../lib/security'),
    imageLib = require('../lib/image'),
    pipeline = require('../lib/promise/pipeline'),
    validation = require('../data/validation'),
    activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'],
    /**
     * inactive: owner user before blog setup, suspended users
     * locked user: imported users, they get a random passport
     */
    inactiveStates = ['inactive', 'locked'],
    allStates = activeStates.concat(inactiveStates);

let User, Users;

User = ghostBookshelf.Model.extend({

    tableName: 'users',

    defaults: function defaults() {
        return {
            password: security.identifier.uid(50)
        };
    },

    emitChange: function emitChange(event, options) {
        const eventToTrigger = 'user' + '.' + event;
        ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
    },

    onDestroyed: function onDestroyed(model, options) {
        if (_.includes(activeStates, model.previous('status'))) {
            model.emitChange('deactivated', options);
        }

        model.emitChange('deleted', options);
    },

    onCreated: function onCreated(model, attrs, options) {
        model.emitChange('added', options);

        // active is the default state, so if status isn't provided, this will be an active user
        if (!model.get('status') || _.includes(activeStates, model.get('status'))) {
            model.emitChange('activated', options);
        }
    },

    onUpdated: function onUpdated(model, response, options) {
        model.statusChanging = model.get('status') !== model.updated('status');
        model.isActive = _.includes(activeStates, model.get('status'));

        if (model.statusChanging) {
            model.emitChange(model.isActive ? 'activated' : 'deactivated', options);
        } else {
            if (model.isActive) {
                model.emitChange('activated.edited', options);
            }
        }

        model.emitChange('edited', options);
    },

    isActive: function isActive() {
        return activeStates.indexOf(this.get('status')) !== -1;
    },

    isLocked: function isLocked() {
        return this.get('status') === 'locked';
    },

    isInactive: function isInactive() {
        return this.get('status') === 'inactive';
    },

    /**
     * Lookup Gravatar if email changes to update image url
     * Generating a slug requires a db call to look for conflicting slugs
     */
    onSaving: function onSaving(newPage, attr, options) {
        var self = this,
            tasks = [],
            passwordValidation = {};

        ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);

        /**
         * Bookshelf call order:
         *   - onSaving
         *   - onValidate (validates the model against the schema)
         *
         * Before we can generate a slug, we have to ensure that the name is not blank.
         */
        if (!this.get('name')) {
            throw new common.errors.ValidationError({
                message: common.i18n.t('notices.data.validation.index.valueCannotBeBlank', {
                    tableName: this.tableName,
                    columnKey: 'name'
                })
            });
        }

        // If the user's email is set & has changed & we are not importing
        if (self.hasChanged('email') && self.get('email') && !options.importing) {
            tasks.gravatar = (function lookUpGravatar() {
                return imageLib.gravatar.lookup({
                    email: self.get('email')
                }).then(function (response) {
                    if (response && response.image) {
                        self.set('profile_image', response.image);
                    }
                });
            })();
        }

        if (this.hasChanged('slug') || !this.get('slug')) {
            tasks.slug = (function generateSlug() {
                return ghostBookshelf.Model.generateSlug(
                    User,
                    self.get('slug') || self.get('name'),
                    {
                        status: 'all',
                        transacting: options.transacting,
                        shortSlug: !self.get('slug')
                    })
                    .then(function then(slug) {
                        self.set({slug: slug});
                    });
            })();
        }

        /**
         * CASE: add model, hash password
         * CASE: update model, hash password
         *
         * Important:
         *   - Password hashing happens when we import a database
         *   - we do some pre-validation checks, because onValidate is called AFTER onSaving
         *   - when importing, we set the password to a random uid and don't validate, just hash it and lock the user
         *   - when importing with `importPersistUser` we check if the password is a bcrypt hash already and fall back to
         *     normal behaviour if not (set random password, lock user, and hash password)
         *   - no validations should run, when importing
         */
        if (self.hasChanged('password')) {
            this.set('password', String(this.get('password')));

            // CASE: import with `importPersistUser` should always be an bcrypt password already,
            // and won't re-hash or overwrite it.
            // In case the password is not bcrypt hashed we fall back to the standard behaviour.
            if (options.importPersistUser && this.get('password').match(/^\$2[ayb]\$.{56}$/i)) {
                return;
            }

            if (options.importing) {
                // always set password to a random uid when importing
                this.set('password', security.identifier.uid(50));

                // lock users so they have to follow the password reset flow
                if (this.get('status') !== 'inactive') {
                    this.set('status', 'locked');
                }
            } else {
                // CASE: we're not importing data, run the validations
                passwordValidation = validation.validatePassword(this.get('password'), this.get('email'));

                if (!passwordValidation.isValid) {
                    return Promise.reject(new common.errors.ValidationError({message: passwordValidation.message}));
                }
            }

            tasks.hashPassword = (function hashPassword() {
                return security.password.hash(self.get('password'))
                    .then(function (hash) {
                        self.set('password', hash);
                    });
            })();
        }

        return Promise.props(tasks);
    },

    toJSON: function toJSON(unfilteredOptions) {
        var options = User.filterOptions(unfilteredOptions, 'toJSON'),
            attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);

        // remove password hash for security reasons
        delete attrs.password;
        delete attrs.ghost_auth_access_token;

        // NOTE: We don't expose the email address for for external, app and public context.
        // @TODO: Why? External+Public is actually the same context? Was also mentioned here https://github.com/TryGhost/Ghost/issues/9043
        if (!options || !options.context || (!options.context.user && !options.context.internal)) {
            delete attrs.email;
        }

        // We don't expose these fields when fetching data via the public API.
        if (options && options.context && options.context.public) {
            delete attrs.created_at;
            delete attrs.created_by;
            delete attrs.updated_at;
            delete attrs.updated_by;
            delete attrs.last_seen;
            delete attrs.status;
            delete attrs.ghost_auth_id;
        }

        return attrs;
    },

    emptyStringProperties: function emptyStringProperties() {
        // CASE: the client might send empty image properties with "" instead of setting them to null.
        // This can cause GQL to fail. We therefore enforce 'null' for empty image properties.
        // See https://github.com/TryGhost/GQL/issues/24
        return ['profile_image', 'cover_image'];
    },

    format: function format(options) {
        if (!_.isEmpty(options.website) &&
            !validator.isURL(options.website, {
                require_protocol: true,
                protocols: ['http', 'https']
            })) {
            options.website = 'http://' + options.website;
        }
        return ghostBookshelf.Model.prototype.format.call(this, options);
    },

    posts: function posts() {
        return this.hasMany('Posts', 'created_by');
    },

    roles: function roles() {
        return this.belongsToMany('Role');
    },

    permissions: function permissions() {
        return this.belongsToMany('Permission');
    },

    hasRole: function hasRole(roleName) {
        var roles = this.related('roles');

        return roles.some(function getRole(role) {
            return role.get('name') === roleName;
        });
    },

    enforcedFilters: function enforcedFilters(options) {
        if (options.context && options.context.internal) {
            return null;
        }

        return options.context && options.context.public ? 'status:[' + allStates.join(',') + ']' : null;
    },

    defaultFilters: function defaultFilters(options) {
        if (options.context && options.context.internal) {
            return null;
        }

        return options.context && options.context.public ? null : 'status:[' + allStates.join(',') + ']';
    }
}, {
    orderDefaultOptions: function orderDefaultOptions() {
        return {
            last_seen: 'DESC',
            name: 'ASC',
            created_at: 'DESC'
        };
    },

    /**
     * @deprecated in favour of filter
     */
    processOptions: function processOptions(options) {
        if (!options.status) {
            return options;
        }

        // This is the only place that 'options.where' is set now
        options.where = {statements: []};

        var value;

        // Filter on the status.  A status of 'all' translates to no filter since we want all statuses
        if (options.status !== 'all') {
            // make sure that status is valid
            options.status = allStates.indexOf(options.status) > -1 ? options.status : 'active';
        }

        if (options.status === 'active') {
            value = activeStates;
        } else if (options.status === 'all') {
            value = allStates;
        } else {
            value = options.status;
        }

        options.where.statements.push({prop: 'status', op: 'IN', value: value});
        delete options.status;

        return options;
    },

    /**
     * Returns an array of keys permitted in a method's `options` hash, depending on the current method.
     * @param {String} methodName The name of the method to check valid options for.
     * @return {Array} Keys allowed in the `options` hash of the model's method.
     */
    permittedOptions: function permittedOptions(methodName, options) {
        var permittedOptionsToReturn = ghostBookshelf.Model.permittedOptions(),

            // whitelists for the `options` hash argument on methods, by method name.
            // these are the only options that can be passed to Bookshelf / Knex.
            validOptions = {
                findOne: ['withRelated', 'status'],
                setup: ['id'],
                edit: ['withRelated', 'id', 'importPersistUser'],
                add: ['importPersistUser'],
                findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status'],
                findAll: ['filter']
            };

        if (validOptions[methodName]) {
            permittedOptionsToReturn = permittedOptionsToReturn.concat(validOptions[methodName]);
        }

        // CASE: The `withRelated` parameter is allowed when using the public API, but not the `roles` value.
        // Otherwise we expose too much information.
        if (options && options.context && options.context.public) {
            if (options.withRelated && options.withRelated.indexOf('roles') !== -1) {
                options.withRelated.splice(options.withRelated.indexOf('roles'), 1);
            }
        }

        return permittedOptionsToReturn;
    },

    /**
     * ### Find One
     *
     * We have to clone the data, because we remove values from this object.
     * This is not expected from outside!
     *
     * @TODO: use base class
     *
     * @extends ghostBookshelf.Model.findOne to include roles
     * **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One)
     */
    findOne: function findOne(dataToClone, unfilteredOptions) {
        var options = this.filterOptions(unfilteredOptions, 'findOne'),
            query,
            status,
            data = _.cloneDeep(dataToClone),
            lookupRole = data.role;

        delete data.role;
        data = _.defaults(data || {}, {
            status: 'all'
        });

        status = data.status;
        delete data.status;

        data = this.filterData(data);

        // Support finding by role
        if (lookupRole) {
            options.withRelated = _.union(options.withRelated, ['roles']);
            query = this.forge(data);

            query.query('join', 'roles_users', 'users.id', '=', 'roles_users.user_id');
            query.query('join', 'roles', 'roles_users.role_id', '=', 'roles.id');
            query.query('where', 'roles.name', '=', lookupRole);
        } else {
            query = this.forge(data);
        }

        if (status === 'active') {
            query.query('whereIn', 'status', activeStates);
        } else if (status !== 'all') {
            query.query('where', {status: status});
        }

        return query.fetch(options);
    },

    /**
     * ### Edit
     *
     * Note: In case of login the last_seen attribute gets updated.
     *
     * @extends ghostBookshelf.Model.edit to handle returning the full object
     * **See:** [ghostBookshelf.Model.edit](base.js.html#edit)
     */
    edit: function edit(data, unfilteredOptions) {
        var options = this.filterOptions(unfilteredOptions, 'edit'),
            self = this,
            ops = [];

        if (data.roles && data.roles.length > 1) {
            return Promise.reject(
                new common.errors.ValidationError({message: common.i18n.t('errors.models.user.onlyOneRolePerUserSupported')})
            );
        }

        if (data.email) {
            ops.push(function checkForDuplicateEmail() {
                return self.getByEmail(data.email, options).then(function then(user) {
                    if (user && user.id !== options.id) {
                        return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.models.user.userUpdateError.emailIsAlreadyInUse')}));
                    }
                });
            });
        }

        ops.push(function update() {
            return ghostBookshelf.Model.edit.call(self, data, options).then(function then(user) {
                var roleId;

                if (!data.roles) {
                    return user;
                }

                roleId = data.roles[0].id || data.roles[0];

                return user.roles().fetch().then(function then(roles) {
                    // return if the role is already assigned
                    if (roles.models[0].id === roleId) {
                        return;
                    }
                    return ghostBookshelf.model('Role').findOne({id: roleId});
                }).then(function then(roleToAssign) {
                    if (roleToAssign && roleToAssign.get('name') === 'Owner') {
                        return Promise.reject(
                            new common.errors.ValidationError({message: common.i18n.t('errors.models.user.methodDoesNotSupportOwnerRole')})
                        );
                    } else {
                        // assign all other roles
                        return user.roles().updatePivot({role_id: roleId});
                    }
                }).then(function then() {
                    options.status = 'all';
                    return self.findOne({id: user.id}, options);
                });
            });
        });

        return pipeline(ops);
    },

    /**
     * ## Add
     * Naive user add
     * Hashes the password provided before saving to the database.
     *
     * We have to clone the data, because we remove values from this object.
     * This is not expected from outside!
     *
     * @param {object} dataToClone
     * @param {object} unfilteredOptions
     * @extends ghostBookshelf.Model.add to manage all aspects of user signup
     * **See:** [ghostBookshelf.Model.add](base.js.html#Add)
     */
    add: function add(dataToClone, unfilteredOptions) {
        var options = this.filterOptions(unfilteredOptions, 'add'),
            self = this,
            data = _.cloneDeep(dataToClone),
            userData = this.filterData(data),
            roles;

        // check for too many roles
        if (data.roles && data.roles.length > 1) {
            return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.models.user.onlyOneRolePerUserSupported')}));
        }

        function getAuthorRole() {
            return ghostBookshelf.model('Role').findOne({name: 'Author'}, _.pick(options, 'transacting'))
                .then(function then(authorRole) {
                    return [authorRole.get('id')];
                });
        }

        /**
         * We need this default author role because of the following Ghost feature:
         * You setup your blog and you can invite people instantly, but without choosing a role.
         * roles: [] -> no default role (used for owner creation, see fixtures.json)
         * roles: undefined -> default role
         */
        roles = data.roles;
        delete data.roles;

        return ghostBookshelf.Model.add.call(self, userData, options)
            .then(function then(addedUser) {
                // Assign the userData to our created user so we can pass it back
                userData = addedUser;
            })
            .then(function () {
                if (!roles) {
                    return getAuthorRole();
                }

                return Promise.resolve(roles);
            })
            .then(function (_roles) {
                roles = _roles;

                // CASE: it is possible to add roles by name, by id or by object
                if (_.isString(roles[0]) && !ObjectId.isValid(roles[0])) {
                    return Promise.map(roles, function (roleName) {
                        return ghostBookshelf.model('Role').findOne({
                            name: roleName
                        }, options);
                    }).then(function (roleModels) {
                        roles = [];

                        _.each(roleModels, function (roleModel) {
                            roles.push(roleModel.id);
                        });
                    });
                }

                return Promise.resolve();
            })
            .then(function () {
                return baseUtils.attach(User, userData.id, 'roles', roles, options);
            })
            .then(function then() {
                // find and return the added user
                return self.findOne({id: userData.id, status: 'all'}, options);
            });
    },

    /**
     * We override the owner!
     * Owner already has a slug -> force setting a new one by setting slug to null
     * @TODO: kill setup function?
     */
    setup: function setup(data, unfilteredOptions) {
        var options = this.filterOptions(unfilteredOptions, 'setup'),
            self = this,
            userData = this.filterData(data),
            passwordValidation = {};

        passwordValidation = validation.validatePassword(userData.password, userData.email, data.blogTitle);

        if (!passwordValidation.isValid) {
            return Promise.reject(new common.errors.ValidationError({message: passwordValidation.message}));
        }

        userData.slug = null;
        return self.edit(userData, options);
    },

    /**
     * Right now the setup of the blog depends on the user status.
     * Only if the owner user is `inactive`, then the blog is not setup.
     * e.g. if you transfer ownership to a locked user, you blog is still setup.
     *
     * @TODO: Rename `inactive` status to something else, it's confusing. e.g. requires-setup
     * @TODO: Depending on the user status results in https://github.com/TryGhost/Ghost/issues/8003
     */
    isSetup: function isSetup(options) {
        return this.getOwnerUser(options)
            .then(function (owner) {
                return owner.get('status') !== 'inactive';
            });
    },

    getOwnerUser: function getOwnerUser(options) {
        options = options || {};

        return this.findOne({
            role: 'Owner',
            status: 'all'
        }, options).then(function (owner) {
            if (!owner) {
                return Promise.reject(new common.errors.NotFoundError({
                    message: common.i18n.t('errors.models.user.ownerNotFound')
                }));
            }

            return owner;
        });
    },

    permissible: function permissible(userModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission) {
        var self = this,
            userModel = userModelOrId,
            origArgs;

        // If we passed in a model without its related roles, we need to fetch it again
        if (_.isObject(userModelOrId) && !_.isObject(userModelOrId.related('roles'))) {
            userModelOrId = userModelOrId.id;
        }
        // If we passed in an id instead of a model get the model first
        if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) {
            // Grab the original args without the first one
            origArgs = _.toArray(arguments).slice(1);

            // Get the actual user model
            return this.findOne({
                id: userModelOrId,
                status: 'all'
            }, {withRelated: ['roles']}).then(function then(foundUserModel) {
                if (!foundUserModel) {
                    throw new common.errors.NotFoundError({
                        message: common.i18n.t('errors.models.user.userNotFound')
                    });
                }

                // Build up the original args but substitute with actual model
                var newArgs = [foundUserModel].concat(origArgs);

                return self.permissible.apply(self, newArgs);
            });
        }

        if (action === 'edit') {
            // Users with the role 'Editor', 'Author', and 'Contributor' have complex permissions when the action === 'edit'
            // We now have all the info we need to construct the permissions

            if (context.user === userModel.get('id')) {
                // If this is the same user that requests the operation allow it.
                hasUserPermission = true;
            } else if (loadedPermissions.user && userModel.hasRole('Owner')) {
                // Owner can only be edited by owner
                hasUserPermission = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
            } else if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
                // If the user we are trying to edit is an Author or Contributor, allow it
                hasUserPermission = userModel.hasRole('Author') || userModel.hasRole('Contributor');
            }
        }

        if (action === 'destroy') {
            // Owner cannot be deleted EVER
            if (userModel.hasRole('Owner')) {
                return Promise.reject(new common.errors.NoPermissionError({message: common.i18n.t('errors.models.user.notEnoughPermission')}));
            }

            // Users with the role 'Editor' have complex permissions when the action === 'destroy'
            if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
                // Alternatively, if the user we are trying to edit is an Author, allow it
                hasUserPermission = context.user === userModel.get('id') || userModel.hasRole('Author') || userModel.hasRole('Contributor');
            }
        }

        if (hasUserPermission && hasAppPermission) {
            return Promise.resolve();
        }

        return Promise.reject(new common.errors.NoPermissionError({message: common.i18n.t('errors.models.user.notEnoughPermission')}));
    },

    // Finds the user by email, and checks the password
    // @TODO: shorten this function and rename...
    check: function check(object) {
        var self = this;

        return this.getByEmail(object.email)
            .then(function then(user) {
                if (!user) {
                    throw new common.errors.NotFoundError({
                        message: common.i18n.t('errors.models.user.noUserWithEnteredEmailAddr')
                    });
                }

                if (user.isLocked()) {
                    throw new common.errors.NoPermissionError({
                        message: common.i18n.t('errors.models.user.accountLocked')
                    });
                }

                if (user.isInactive()) {
                    throw new common.errors.NoPermissionError({
                        message: common.i18n.t('errors.models.user.accountSuspended')
                    });
                }

                return self.isPasswordCorrect({plainPassword: object.password, hashedPassword: user.get('password')})
                    .then(function then() {
                        user.set({status: 'active', last_seen: new Date()});
                        return user.save();
                    });
            })
            .catch(function (err) {
                if (err.message === 'NotFound' || err.message === 'EmptyResponse') {
                    throw new common.errors.NotFoundError({
                        message: common.i18n.t('errors.models.user.noUserWithEnteredEmailAddr')
                    });
                }

                throw err;
            });
    },

    isPasswordCorrect: function isPasswordCorrect(object) {
        var plainPassword = object.plainPassword,
            hashedPassword = object.hashedPassword;

        if (!plainPassword || !hashedPassword) {
            return Promise.reject(new common.errors.ValidationError({
                message: common.i18n.t('errors.models.user.passwordRequiredForOperation')
            }));
        }

        return security.password.compare(plainPassword, hashedPassword)
            .then(function (matched) {
                if (matched) {
                    return;
                }

                return Promise.reject(new common.errors.ValidationError({
                    context: common.i18n.t('errors.models.user.incorrectPassword'),
                    message: common.i18n.t('errors.models.user.incorrectPassword'),
                    help: common.i18n.t('errors.models.user.userUpdateError.help'),
                    code: 'PASSWORD_INCORRECT'
                }));
            });
    },

    /**
     * Naive change password method
     * @param {Object} object
     * @param {Object} unfilteredOptions
     */
    changePassword: function changePassword(object, unfilteredOptions) {
        var options = this.filterOptions(unfilteredOptions, 'changePassword'),
            self = this,
            newPassword = object.newPassword,
            userId = object.user_id,
            oldPassword = object.oldPassword,
            isLoggedInUser = userId === options.context.user,
            user;

        options.require = true;

        return self.forge({id: userId}).fetch(options)
            .then(function then(_user) {
                user = _user;

                if (isLoggedInUser) {
                    return self.isPasswordCorrect({
                        plainPassword: oldPassword,
                        hashedPassword: user.get('password')
                    });
                }
            })
            .then(function then() {
                return user.save({password: newPassword});
            });
    },

    transferOwnership: function transferOwnership(object, unfilteredOptions) {
        var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'transferOwnership'),
            ownerRole,
            contextUser;

        return Promise.join(
            ghostBookshelf.model('Role').findOne({name: 'Owner'}),
            User.findOne({id: options.context.user}, {withRelated: ['roles']})
        )
            .then(function then(results) {
                ownerRole = results[0];
                contextUser = results[1];

                // check if user has the owner role
                var currentRoles = contextUser.toJSON(options).roles;
                if (!_.some(currentRoles, {id: ownerRole.id})) {
                    return Promise.reject(new common.errors.NoPermissionError({message: common.i18n.t('errors.models.user.onlyOwnerCanTransferOwnerRole')}));
                }

                return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Administrator'}),
                    User.findOne({id: object.id}, {withRelated: ['roles']}));
            })
            .then(function then(results) {
                var adminRole = results[0],
                    user = results[1],
                    currentRoles = user.toJSON(options).roles;

                if (!_.some(currentRoles, {id: adminRole.id})) {
                    return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.models.user.onlyAdmCanBeAssignedOwnerRole')}));
                }

                // convert owner to admin
                return Promise.join(contextUser.roles().updatePivot({role_id: adminRole.id}),
                    user.roles().updatePivot({role_id: ownerRole.id}),
                    user.id);
            })
            .then(function then(results) {
                return Users.forge()
                    .query('whereIn', 'id', [contextUser.id, results[2]])
                    .fetch({withRelated: ['roles']});
            })
            .then(function then(users) {
                options.withRelated = ['roles'];
                return users.toJSON(options);
            });
    },

    // Get the user by email address, enforces case insensitivity rejects if the user is not found
    // When multi-user support is added, email addresses must be deduplicated with case insensitivity, so that
    // joe@bloggs.com and JOE@BLOGGS.COM cannot be created as two separate users.
    getByEmail: function getByEmail(email, unfilteredOptions) {
        var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'getByEmail');

        // We fetch all users and process them in JS as there is no easy way to make this query across all DBs
        // Although they all support `lower()`, sqlite can't case transform unicode characters
        // This is somewhat mute, as validator.isEmail() also doesn't support unicode, but this is much easier / more
        // likely to be fixed in the near future.
        options.require = true;

        return Users.forge().fetch(options).then(function then(users) {
            var userWithEmail = users.find(function findUser(user) {
                return user.get('email').toLowerCase() === email.toLowerCase();
            });
            if (userWithEmail) {
                return userWithEmail;
            }
        });
    },
    inactiveStates: inactiveStates
});

Users = ghostBookshelf.Collection.extend({
    model: User
});

module.exports = {
    User: ghostBookshelf.model('User', User),
    Users: ghostBookshelf.collection('Users', Users)
};
