const UserError = require('vn-loopback/util/user-error');
const getFinalState = require('vn-loopback/util/hook').getFinalState;
const isMultiple = require('vn-loopback/util/hook').isMultiple;
const validateTin = require('vn-loopback/util/validateTin');
const validateIban = require('vn-loopback/util/validateIban');
const LoopBackContext = require('loopback-context');

module.exports = Self => {
    // Methods
    require('./client-methods')(Self);

    // Validations

    Self.validatesPresenceOf('street', {
        message: 'Street cannot be empty'
    });

    Self.validatesPresenceOf('city', {
        message: 'City cannot be empty'
    });

    Self.validatesUniquenessOf('fi', {
        message: 'TIN must be unique'
    });

    Self.validatesFormatOf('email', {
        message: 'Invalid email',
        allowNull: true,
        allowBlank: true,
        with: /^[\W]*([\w+\-.%]+@[\w\-.]+\.[A-Za-z]{1,61}[\W]*,{1}[\W]*)*([\w+\-.%]+@[\w\-.]+\.[A-Za-z]{1,61})[\W]*$/
    });

    Self.validatesLengthOf('postcode', {
        allowNull: true,
        allowBlank: true,
        min: 3, max: 10
    });

    Self.validatesFormatOf('street', {
        message: 'Street should be uppercase',
        allowNull: false,
        allowBlank: false,
        with: /^[^a-z]*$/
    });

    Self.validatesFormatOf('socialName', {
        message: 'Social name should be uppercase',
        allowNull: false,
        allowBlank: false,
        with: /^[^a-z]*$/
    });

    Self.validateAsync('socialName', socialNameIsUnique, {
        message: 'The company name must be unique'
    });

    async function socialNameIsUnique(err, done) {
        if (!this.countryFk)
            return done();

        const filter = {
            include: {
                relation: 'country',
                scope: {
                    fields: {
                        isSocialNameUnique: true,
                    },
                },
            },
            where: {
                and: [
                    {socialName: this.socialName},
                    {isActive: true},
                    {id: {neq: this.id}}
                ]
            }
        };

        const client = await Self.app.models.Country.findById(this.countryFk, {fields: ['isSocialNameUnique']});
        const existingClient = await Self.findOne(filter);

        if (existingClient && (existingClient.country().isSocialNameUnique || client.isSocialNameUnique))
            err();

        done();
    }

    Self.validateAsync('iban', ibanNeedsValidation, {
        message: 'The IBAN does not have the correct format'
    });

    async function ibanNeedsValidation(err, done) {
        if (!this.bankEntityFk)
            return done();

        const bankEntity = await Self.app.models.BankEntity.findById(this.bankEntityFk);
        const filter = {
            fields: ['code'],
            where: {id: bankEntity.countryFk}
        };
        const country = await Self.app.models.Country.findOne(filter);

        if (!validateIban(this.iban, country?.code))
            err();
        done();
    }

    Self.validateAsync('fi', tinIsValid, {
        message: 'Invalid TIN'
    });

    async function tinIsValid(err, done) {
        if (!this.isTaxDataChecked)
            return done();

        const filter = {
            fields: ['code'],
            where: {id: this.countryFk}
        };
        const country = await Self.app.models.Country.findOne(filter);
        const code = country ? country.code.toLowerCase() : null;
        const countryCode = this.fi?.toLowerCase().substring(0, 2);

        if (!this.fi || !validateTin(this.fi, code) || (this.isVies && countryCode == code))
            err();
        done();
    }

    Self.validate('payMethod', hasSalesMan, {
        message: 'Cannot change the payment method if no salesperson'
    });

    function hasSalesMan(err) {
        if (this.payMethod && !this.salesPersonUser)
            err();
    }

    Self.validate('isEqualizated', cannotHaveET, {
        message: 'Cannot check Equalization Tax in this NIF/CIF'
    });

    function cannotHaveET(err) {
        if (!this.fi) return;

        const tin = this.fi.toUpperCase();
        const cannotHaveET = /^[A-B]/.test(tin);

        if (cannotHaveET && this.isEqualizated)
            err();
    }

    Self.validateAsync('payMethodFk', hasIban, {
        message: 'That payment method requires an IBAN'
    });

    function hasIban(err, done) {
        Self.app.models.PayMethod.findById(this.payMethodFk, (_, instance) => {
            const isMissingIban = instance && instance.isIbanRequiredForClients && !this.iban;
            if (isMissingIban)
                err();
            done();
        });
    }

    Self.validateAsync('bankEntityFk', hasBic, {
        message: 'That payment method requires a BIC'
    });

    function hasBic(err, done) {
        if (this.iban && !this.bankEntityFk)
            err();
        done();
    }

    Self.validateAsync('isToBeMailed', isToBeMailed, {
        message: 'There is no assigned email for this client'
    });

    function isToBeMailed(err, done) {
        if (this.isToBeMailed == true && !this.email)
            err();
        done();
    }

    Self.validateAsync('defaultAddressFk', isActive,
        {message: 'Unable to default a disabled consignee'}
    );

    async function isActive(err, done) {
        if (!this.defaultAddressFk)
            return done();

        const address = await Self.app.models.Address.findById(this.defaultAddressFk);
        if (address && !address.isActive) err();
        done();
    }

    Self.validateAsync('postCode', hasValidPostcode, {
        message: `The postcode doesn't exist. Please enter a correct one`
    });

    async function hasValidPostcode(err, done) {
        if (!this.postcode)
            return done();

        const models = Self.app.models;
        const postcode = await models.Postcode.findById(this.postcode);

        if (!postcode) err();
        done();
    }

    function isAlpha(value) {
        const regexp = new RegExp(/^[ñça-zA-Z0-9\s]*$/i);

        return regexp.test(value);
    }

    Self.observe('before save', async ctx => {
        const changes = ctx.data || ctx.instance;
        const orgData = ctx.currentInstance;

        const businessTypeFk = changes && changes.businessTypeFk || orgData && orgData.businessTypeFk;
        const isTaxDataChecked = changes && changes.isTaxDataChecked || orgData && orgData.isTaxDataChecked;

        let invalidBusinessType = false;
        if (!ctx.isNewInstance) {
            const isWorker = await Self.app.models.Account.findById(orgData.id);
            const changedFields = Object.keys(changes);
            const hasChangedOtherFields = changedFields.some(key => key !== 'businessTypeFk');

            if (!businessTypeFk && !isTaxDataChecked && !isWorker && !hasChangedOtherFields)
                invalidBusinessType = true;
        }

        if (ctx.isNewInstance) {
            if (!businessTypeFk && !isTaxDataChecked)
                invalidBusinessType = true;
        }

        if (invalidBusinessType)
            throw new UserError(`The type of business must be filled in basic data`);
    });

    Self.observe('before save', async ctx => {
        const changes = ctx.data || ctx.instance;
        const orgData = ctx.currentInstance;
        const models = Self.app.models;

        const loopBackContext = LoopBackContext.getCurrentContext();
        const accessToken = {req: loopBackContext.active.accessToken};

        const editVerifiedDataWithoutTaxDataChecked = models.ACL.checkAccessAcl(accessToken, 'Client', 'editVerifiedDataWithoutTaxDataCheck', 'WRITE');
        const hasChanges = orgData && changes;

        const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked);
        const isTaxDataCheckedChanged = hasChanges && orgData.isTaxDataChecked != isTaxDataChecked;

        const sageTaxType = hasChanges && (changes.sageTaxTypeFk || orgData.sageTaxTypeFk);
        const sageTaxTypeChanged = hasChanges && orgData.sageTaxTypeFk != sageTaxType;

        const sageTransactionType = hasChanges && (changes.sageTransactionTypeFk || orgData.sageTransactionTypeFk);
        const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType;

        const cantEditVerifiedData = isTaxDataCheckedChanged && !editVerifiedDataWithoutTaxDataChecked;
        const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !editVerifiedDataWithoutTaxDataChecked;

        if (cantEditVerifiedData || cantChangeSageData)
            throw new UserError(`You don't have enough privileges`);
    });

    Self.observe('before save', async function(ctx) {
        const changes = ctx.data || ctx.instance;
        const orgData = ctx.currentInstance;
        const finalState = getFinalState(ctx);
        const payMethodWithIban = 4;

        // Validate socialName format
        const hasChanges = orgData && changes;
        const socialName = changes && changes.socialName || orgData && orgData.socialName;
        const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked);

        const socialNameChanged = hasChanges
            && orgData.socialName != socialName;
        const isTaxDataCheckedChanged = hasChanges
            && orgData.isTaxDataChecked != isTaxDataChecked;

        if ((socialNameChanged || isTaxDataCheckedChanged) && !isAlpha(socialName))
            throw new UserError(`The social name has an invalid format`);

        if (changes.salesPerson === null) {
            changes.credit = 0;
            changes.discount = 0;
            changes.payMethodFk = 5; // Credit card
        }

        const payMethodFk = changes.payMethodFk || (orgData && orgData.payMethodFk);
        const dueDay = changes.dueDay || (orgData && orgData.dueDay);

        if (payMethodFk == payMethodWithIban && dueDay == 0)
            changes.dueDay = 5;

        if (isMultiple(ctx)) return;

        if (!ctx.isNewInstance) {
            const isTaxDataCheckedChanged = !orgData.isTaxDataChecked && changes.isTaxDataChecked;
            if (isTaxDataCheckedChanged && !orgData.businessTypeFk)
                throw new UserError(`Can't verify data unless the client has a business type`);
        }
        // Credit changes
        if (changes.credit !== undefined)
            await Self.changeCredit(ctx, finalState, changes);

        // Credit management changes
        if (changes?.rating || changes?.recommendedCredit)
            await Self.changeCreditManagement(ctx, finalState, changes);

        const oldInstance = {};
        if (!ctx.isNewInstance) {
            const newProps = Object.keys(changes);
            Object.keys(orgData.__data).forEach(prop => {
                if (newProps.includes(prop))
                    oldInstance[prop] = orgData[prop];
            });
        }

        ctx.hookState.oldInstance = oldInstance;
        ctx.hookState.newInstance = changes;
    });

    Self.observe('after save', async ctx => {
        if (ctx.isNewInstance) return;

        const hookState = ctx.hookState;
        const newInstance = hookState.newInstance;
        const oldInstance = hookState.oldInstance;
        const instance = ctx.instance;
        const models = Self.app.models;

        const payMethodChanged = oldInstance.payMethodFk != newInstance.payMethodFk;
        const ibanChanged = oldInstance.iban != newInstance.iban;
        const dueDayChanged = oldInstance.dueDay != newInstance.dueDay;

        if (payMethodChanged || ibanChanged || dueDayChanged) {
            const loopBackContext = LoopBackContext.getCurrentContext();
            const httpCtx = {req: loopBackContext.active};
            const httpRequest = httpCtx.req.http.req;
            const $t = httpRequest.__;
            const headers = httpRequest.headers;
            const origin = headers.origin;

            const salesPersonId = instance.salesPersonFk;

            if (salesPersonId) {
                // Send email to client
                if (instance.email) {
                    const {Email} = require('vn-print');
                    const worker = await models.EmailUser.findById(salesPersonId);
                    const params = {
                        id: instance.id,
                        recipientId: instance.id,
                        recipient: instance.email,
                        replyTo: worker.email
                    };
                    const email = new Email('payment-update', params);
                    await email.send();
                }

                const fullUrl = `${origin}/#!/client/${instance.id}/billing-data`;
                const message = $t('Changed client paymethod', {
                    clientId: instance.id,
                    clientName: instance.name,
                    url: fullUrl
                });
                await models.Chat.sendCheckingPresence(httpCtx, salesPersonId, message);
            }
        }

        const workerIdBefore = oldInstance.salesPersonFk;
        const workerIdAfter = newInstance.salesPersonFk;
        const assignmentChanged = workerIdBefore != workerIdAfter;
        if (assignmentChanged)
            await Self.notifyAssignment(instance, workerIdBefore, workerIdAfter);
    });

    // Send notification on client worker assignment
    Self.notifyAssignment = async function notifyAssignment(client, previousWorkerId, currentWorkerId) {
        const loopBackContext = LoopBackContext.getCurrentContext();
        const httpCtx = {req: loopBackContext.active};
        const httpRequest = httpCtx.req.http.req;
        const $t = httpRequest.__;
        const headers = httpRequest.headers;
        const origin = headers.origin;
        const models = Self.app.models;

        let previousWorker = {name: $t('None')};
        let currentWorker = {name: $t('None')};
        if (previousWorkerId) {
            const worker = await models.Worker.findById(previousWorkerId, {
                include: {relation: 'user'}
            });
            previousWorker.user = worker && worker.user().name;
            previousWorker.name = worker && worker.user().nickname;
        }

        if (currentWorkerId) {
            const worker = await models.Worker.findById(currentWorkerId, {
                include: {relation: 'user'}
            });
            currentWorker.user = worker && worker.user().name;
            currentWorker.name = worker && worker.user().nickname;
        }

        const fullUrl = `${origin}/#!/client/${client.id}/basic-data`;
        const message = $t('Client assignment has changed', {
            clientId: client.id,
            clientName: client.name,
            url: fullUrl,
            previousWorkerName: previousWorker.name,
            currentWorkerName: currentWorker.name
        });

        if (previousWorkerId)
            await models.Chat.send(httpCtx, `@${previousWorker.user}`, message);

        if (currentWorkerId)
            await models.Chat.send(httpCtx, `@${currentWorker.user}`, message);
    };

    // Credit change validations
    Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
        const models = Self.app.models;
        const userId = ctx.options.accessToken.userId;
        const accessToken = {req: {accessToken: ctx.options.accessToken}};

        const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE');
        if (!canEditCredit) {
            const lastCredit = await models.ClientCredit.findOne({
                field: ['workerFk', 'amount'],
                where: {
                    clientFk: finalState.id
                },
                order: 'id DESC'
            }, ctx.options);

            if (lastCredit && lastCredit.amount == 0) {
                const zeroCreditEditor =
                    await models.ACL.checkAccessAcl(accessToken, 'Client', 'zeroCreditEditor', 'WRITE');
                const lastCreditIsNotEditable =
                    await models.ACL.checkAccessAcl(
                        {req: {accessToken: {userId: lastCredit.workerFk}}},
                        'Client',
                        'zeroCreditEditor',
                        'WRITE'
                    );

                if (lastCreditIsNotEditable && !zeroCreditEditor)
                    throw new UserError(`You can't change the credit set to zero from a financialBoss`);
            }

            const creditLimits = await models.ClientCreditLimit.find({
                fields: ['roleFk'],
                where: {
                    maxAmount: {gte: changes.credit}
                }
            }, ctx.options);

            const requiredRoles = [];
            for (limit of creditLimits)
                requiredRoles.push(limit.roleFk);

            const userRequiredRoles = await models.RoleMapping.count({
                roleId: {inq: requiredRoles},
                principalType: 'USER',
                principalId: userId
            }, ctx.options);

            if (userRequiredRoles <= 0)
                throw new UserError(`You don't have enough privileges to set this credit amount`);
        }

        await models.ClientCredit.create({
            amount: changes.credit,
            clientFk: finalState.id,
            workerFk: userId
        }, ctx.options);
    };

    Self.changeCreditManagement = async function changeCreditManagement(ctx, finalState, changes) {
        const models = Self.app.models;
        const loopBackContext = LoopBackContext.getCurrentContext();
        const userId = loopBackContext.active.accessToken.userId;

        await models.ClientInforma.create({
            clientFk: finalState.id,
            rating: changes.rating,
            recommendedCredit: changes.recommendedCredit,
            workerFk: userId
        }, ctx.options);
    };

    const app = require('vn-loopback/server/server');
    app.on('started', function() {
        const VnUser = app.models.VnUser;

        VnUser.observe('before save', async ctx => {
            if (ctx.isNewInstance) return;
            if (ctx.currentInstance)
                ctx.hookState.oldInstance = JSON.parse(JSON.stringify(ctx.currentInstance));
        });

        VnUser.observe('after save', async ctx => {
            const changes = ctx.data || ctx.instance;
            if (!ctx.isNewInstance && changes) {
                const oldData = ctx.hookState.oldInstance;
                let hasChanges;

                if (oldData)
                    hasChanges = oldData.name != changes.name || oldData.active != changes.active;

                if (!hasChanges) return;
            }
        });
    });
};