const vnModel = require('vn-loopback/common/models/vn-model');
const {Email} = require('vn-print');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
const LoopBackContext = require('loopback-context');
const UserError = require('vn-loopback/util/user-error');

module.exports = function(Self) {
    vnModel(Self);

    require('../methods/vn-user/sign-in')(Self);
    require('../methods/vn-user/acl')(Self);
    require('../methods/vn-user/recover-password')(Self);
    require('../methods/vn-user/privileges')(Self);
    require('../methods/vn-user/validate-auth')(Self);
    require('../methods/vn-user/renew-token')(Self);
    require('../methods/vn-user/share-token')(Self);
    require('../methods/vn-user/update-user')(Self);
    require('../methods/vn-user/validate-token')(Self);

    Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');

    // Validations

    Self.validatesFormatOf('email', {
        message: 'Invalid email',
        allowNull: true,
        allowBlank: false,
        with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/
    });

    Self.validatesUniquenessOf('name', {
        message: `A client with that Web User name already exists`
    });

    Self.remoteMethod('getCurrentUserData', {
        description: 'Gets the current user data',
        accepts: [
            {
                arg: 'ctx',
                type: 'Object',
                http: {source: 'context'}
            }
        ],
        returns: {
            type: 'Object',
            root: true
        },
        http: {
            verb: 'GET',
            path: '/getCurrentUserData'
        }
    });

    Self.getCurrentUserData = async function(ctx) {
        let userId = ctx.req.accessToken.userId;
        return await Self.findById(userId, {
            fields: ['id', 'name', 'nickname']
        });
    };

    /**
     * Checks if user has a role.
     *
     * @param {Integer} userId The user id
     * @param {String} name The role name
     * @param {Object} options Options
     * @return {Boolean} %true if user has the role, %false otherwise
     */
    Self.hasRole = async function(userId, name, options) {
        const roles = await Self.getRoles(userId, options);
        return roles.some(role => role == name);
    };

    /**
     * Get all user roles.
     *
     * @param {Integer} userId The user id
     * @param {Object} options Options
     * @return {Object} User role list
     */
    Self.getRoles = async(userId, options) => {
        const result = await Self.rawSql(
            `SELECT r.name
            FROM account.user u
            JOIN account.roleRole rr ON rr.role = u.role
            JOIN account.role r ON r.id = rr.inheritsFrom
            WHERE u.id = ?`, [userId], options);

        const roles = [];
        for (const role of result)
            roles.push(role.name);

        return roles;
    };

    Self.on('resetPasswordRequest', async function(info) {
        const loopBackContext = LoopBackContext.getCurrentContext();
        const httpCtx = {req: loopBackContext.active};
        const httpRequest = httpCtx.req.http.req;
        const headers = httpRequest.headers;
        const origin = headers.origin;

        const defaultHash = '/reset-password?access_token=$token$';
        const recoverHashes = {
            hedera: 'verificationToken=$token$'
        };

        const app = info.options?.app;
        let recoverHash = app ? recoverHashes[app] : defaultHash;
        recoverHash = recoverHash.replace('$token$', info.accessToken.id);

        const user = await Self.app.models.VnUser.findById(info.user.id);

        const params = {
            recipient: info.email,
            lang: user.lang,
            url: origin + '/#!' + recoverHash
        };

        const options = Object.assign({}, info.options);
        for (const param in options)
            params[param] = options[param];

        const email = new Email(options.emailTemplate, params);

        return email.send();
    });

    /**
     * Sign-in validate
     * @param {String} user The user
     * @param {Object} userToken Options
     * @param {Object} token accessToken
     * @param {Object} ctx context
     */
    Self.signInValidate = async(user, userToken, token, ctx) => {
        const [[key, value]] = Object.entries(Self.userUses(user));
        const isOwner = Self.rawSql(`SELECT ? = ? `, [userToken[key], value]);
        if (!isOwner) {
            await Self.app.models.SignInLog.create({
                userName: user,
                token: token.id,
                userFk: userToken.id,
                ip: ctx.req.ip,
                owner: isOwner
            });
            throw new UserError('Try again');
        }
    };

    /**
     * Validate login params
     * @param {String} user The user
     * @param {String} password
      * @param {Object} ctx context
     */
    Self.validateLogin = async function(user, password, ctx) {
        const loginInfo = Object.assign({password}, Self.userUses(user));
        const token = await Self.login(loginInfo, 'user');

        const userToken = await token.user.get();

        if (ctx)
            await Self.signInValidate(user, userToken, token, ctx);

        try {
            await Self.app.models.Account.sync(userToken.name, password);
        } catch (err) {
            console.warn(err);
        }

        return {token: token.id, ttl: token.ttl};
    };

    Self.userUses = function(user) {
        return user.indexOf('@') !== -1
            ? {email: user}
            : {username: user};
    };

    const _setPassword = Self.prototype.setPassword;
    Self.prototype.setPassword = async function(newPassword, options, cb) {
        if (cb === undefined && typeof options === 'function') {
            cb = options;
            options = undefined;
        }

        const myOptions = {};
        let tx;

        if (typeof options == 'object')
            Object.assign(myOptions, options);

        if (!myOptions.transaction) {
            tx = await Self.beginTransaction({});
            myOptions.transaction = tx;
        }
        options = myOptions;

        try {
            await Self.rawSql(`CALL account.user_checkPassword(?)`, [newPassword], options);
            await _setPassword.call(this, newPassword, options);
            await this.updateAttribute('passExpired', null, options);
            await Self.app.models.Account.sync(this.name, newPassword, null, options);
            tx && await tx.commit();
            cb && cb();
        } catch (err) {
            tx && await tx.rollback();
            if (cb) cb(err); else throw err;
        }
    };

    Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls =
        Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
            .filter(acl => acl.property != 'changePassword');

    Self.userSecurity = async(ctx, userId, options) => {
        const models = Self.app.models;
        const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken;
        const ctxToken = {req: {accessToken}};

        if (userId === accessToken.userId) return;

        const myOptions = {};
        if (typeof options == 'object')
            Object.assign(myOptions, options);

        const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions);
        if (hasHigherPrivileges) return;

        const hasMediumPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'mediumPrivileges', myOptions);
        const user = await models.VnUser.findById(userId, {fields: ['id', 'emailVerified']}, myOptions);
        if (!user.emailVerified && hasMediumPrivileges) return;

        throw new ForbiddenError();
    };

    Self.observe('after save', async ctx => {
        const instance = ctx?.instance;
        const newEmail = instance?.email;
        const oldEmail = ctx?.hookState?.oldInstance?.email;
        if (!ctx.isNewInstance && (!newEmail || !oldEmail || newEmail == oldEmail)) return;

        const loopBackContext = LoopBackContext.getCurrentContext();
        const httpCtx = {req: loopBackContext.active};
        const httpRequest = httpCtx.req.http.req;
        const headers = httpRequest.headers;
        const origin = headers.origin;
        const url = origin.split(':');

        const env = process.env.NODE_ENV;
        const liliumUrl = await Self.app.models.Url.findOne({
            where: {
                and: [
                    {appName: 'lilium'},
                    {environment: env}
                ]
            }
        });

        class Mailer {
            async send(verifyOptions, cb) {
                try {
                    const url = new URL(verifyOptions.verifyHref);
                    if (process.env.NODE_ENV) url.port = '';

                    const email = new Email('email-verify', {
                        url: url.href,
                        recipient: verifyOptions.to
                    });
                    await email.send();

                    cb(null, verifyOptions.to);
                } catch (err) {
                    cb(err);
                }
            }
        }

        const options = {
            type: 'email',
            to: newEmail,
            from: {},
            redirect: `${liliumUrl.url}verifyEmail?userId=${instance.id}`,
            template: false,
            mailer: new Mailer,
            host: url[1].split('/')[2],
            port: url[2],
            protocol: url[0],
            user: Self
        };

        await instance.verify(options, ctx.options);
    });
};