'use strict';

var sharedUrlRoutes = require('../../shared/SharedUrlRoutes');
var SharedConstants = require('../../shared/constants/SharedConstants');
var UtilHttpStatus = require('../../shared/utils/UtilHttpStatus');
var ErrorCodes = require('../../shared/constants/ErrorCodes');
var accounts = require('./AccountController');
var mailHelper = require('../email/EmailHelper');
var ServerConstants = require('../constants/ServerConstants');
var authentication = require('../authentication/Authentication');
var basePath = require('../../server/web/BaseURLPath');
var requestIp = require('request-ip');
var logger = require('../utils/Logger').loggers.get('auth');
var UtilError = require("../utils/UtilError");
var UtilHost = require("../utils/UtilHost");
var redisAccountDataService = require("../redis/RedisAccountDataService");
var ruleEngine = require('../ruleengine/RuleEngine');
var PasswordChecker = require("../account/password/PasswordChecker");

const { asyncMiddleware } = require('../middleware/AsyncMiddleware');
const { FORBIDDEN_ERROR, NOT_FOUND_ERROR } = require("../../shared/utils/UtilError");
const { isEmpty } = require("lodash/lang");

let socket;

const checkRuleStatus = (rule, ruleResult, successFunc, callback) => {
    var status = ruleResult ? ruleResult.status : SharedConstants.STATUS_SUCCESS;
    if(status && status.toLowerCase() === SharedConstants.STATUS_ERROR){
        var msg = `Error in ${rule} rule processing: ` + JSON.stringify(ruleResult);
        logger.error(msg);
        callback(new Error(msg));
    } else {
        successFunc();
    }
};

const processLoginRules = (account, accountControlDocument, loginSuccessFunc, callback) => {
    const docContext = { user: account,  document: account, accountControlDocument };
    ruleEngine.doPreLoginServer(account, docContext, (ruleResult) => {
        // Account doesn't have access to the accountTemplate.
        // Skip running login rulesets.
        // https://mantis.formbird.com/view.php?id=17956#c75864
        if (!isEmpty(ruleResult) 
            && (ruleResult.errorName === FORBIDDEN_ERROR || ruleResult.errorName === NOT_FOUND_ERROR)) {
          loginSuccessFunc();
          callback();
          return;
        }
        checkRuleStatus('PreLoginServer', ruleResult, () => {
            loginSuccessFunc();
            ruleEngine.doPostLoginServer(account, docContext, (ruleResult) => {
                checkRuleStatus('PostLoginServer', ruleResult, callback, callback);
            });
        }, callback);
    });
};

const processLogoutRules = (account, accountControlDocument, logoutSuccessFunc, callback) => {
    const docContext = { user: account,  document: account, accountControlDocument };
    ruleEngine.doPreLogoutServer(account, docContext, ruleResult => {
        if (!isEmpty(ruleResult) 
            && (ruleResult.errorName === FORBIDDEN_ERROR || ruleResult.errorName === NOT_FOUND_ERROR)) {
            logoutSuccessFunc();
            callback();
            return;
        }

        checkRuleStatus('PreLogoutServer', ruleResult, () => {
            logoutSuccessFunc();
            ruleEngine.doPostLogoutServer(account, docContext, ruleResult => {
                checkRuleStatus('PostLogoutServer', ruleResult, callback, callback);
            });
        }, callback);
    });
};

exports.doActivateAccount = function(req, res) {

    var token = req.body.token;

    accounts.activateAccount(token, function (error, user) {
        if (user) {
            res.send({ message: "Your account has been activated." });
        } else {
            res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send("An error occurred during activation. Please try again.");
        }
    });
};

exports.doSendResetLink = function(req, res) {

    var fullDomainUrl = UtilHost.getFullDomainURL(req);

    accounts.sendResetLink(req.body, fullDomainUrl, function cb(error) {

        if (error) {

            if(error instanceof UtilError.AccountNotFoundError){
                res.status(UtilHttpStatus.BAD_REQUEST).send("Account not yet registered.");
            }else{
                logger.error("Error sending password reset email: " + error.message);
                res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send("The server failed to send the password reset link.");
            }

            return;
        }

        res.send({ message: "A reset email has been sent to you. Please check your mail box then reset your password." });

    });
};

exports.doResetPassword = function(req, res) {

    var email = req.body.email;
    logger.info("Resetting password for " + email);

    var hash = req.body.hash;
    var pass = req.body.pass;
    var repass = req.body.repass;

    var result = {};
    if (pass !== repass) {
        res.status(UtilHttpStatus.BAD_REQUEST).send("Passwords don't match.");
        return;
    }

    var fails = PasswordChecker.evaluatePassword(pass, config.get("passwordCreationRules"));
    if(fails.length > 0){
        res.status(UtilHttpStatus.BAD_REQUEST).send({
            message: "Password invalid.",
            passwordFailures: fails
        });
        return;
    }

    accounts.resetPassword(email, hash, pass, function (err, status) {

        if (status === 'ok') {
            res.send({message: "Your password has been reset." });
        } else {
            res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send("Failed to reset password.");
        }

    });
};

exports.getLoggedUser = function (req, res) {
    var userId = req.user ? req.user.documentId : null;

    accounts.findOne(userId).then(function successFunc(account) {

        if (account) {
            let userSession = getRedisSessionObject(req, account);
            req.user = account;
            req.session.passport.user = userSession;

            res.send({
                user : account
            });

            return;
        }

        res.status(UtilHttpStatus.NOT_FOUND).send("Could not find the requested account.");

    }, function errorFunc(err) {
        logger.error("Error fetching logged user account.: " + JSON.stringify(err));
        res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send("Error fetching logged user account.");
    });
};

const getRedisSessionObject = (req, user) =>{

    let fullPath = req.protocol + '://' + req.get('host') + req.originalUrl;

  return   {
        "email" : user.email,
        "documentId": user.documentId,
        "url" : fullPath,
        "subDomain": req.subdomains,
        "domain": req.hostname,
        "ipAddress": requestIp.getClientIp(req)
    };

};

const addUserToSession = (user, req, next) => {
    let userSession = getRedisSessionObject(req, user);
    redisAccountDataService.setAccountSessionIds(user.documentId, req.session.id);
    req.logIn(userSession, function(err) {
        if (err) { return next(err); }
    });
};

exports.getLoggedUserData = function(req, res) {
    var userId = req.user ? req.user.documentId : null;

    accounts.findOne(userId).then(async function successFunc(account) {

        if (account) {
            let userSession = getRedisSessionObject(req, account);
            req.user = account;
            req.session.passport.user = userSession;

            const groupConfigDocument = await accounts.getAccountGroupConfigDocument(userId);

            accounts.getAccountControlDocument(userId)
                .then(function(controlDocument){
                    res.send({
                        user : account,
                        controlDoc: controlDocument,
                        groupConfigDoc: groupConfigDocument
                    });
                }).catch(function(err) {
                    res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send(err.message ? err.message : err);
                });

            return;
        }

        res.status(UtilHttpStatus.NOT_FOUND).send("Could not find the requested account.");

    }, function errorFunc(err) {
        logger.error("Error fetching logged user account.: " + JSON.stringify(err));
        res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send("Error fetching logged user account.");
    });
};

exports.getLoggedUserAccountControlDocument = function(req, res) {

    var userId = req.user ? req.user.documentId : null;

    accounts.getAccountControlDocument(userId)
        .then(function(controlDocument){
            res.send(controlDocument);
        })
        .catch(function(err){
            res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send(err.message ? err.message : err);
        });

};

exports.checkAuthentication = function(req, res, next) {
    var isAuthorized = req.isAuthenticated();
    if ((req.user && isAuthorized) || req.originalUrl ===  "/" + sharedUrlRoutes.serverRoutes.healthCheck) {
        return next();
    }

    var email = req.cookies.email;
    var hash = req.cookies.hash;
    if (email !== undefined || hash !== undefined) {
        accounts.autoLocalLogin(email, hash, function (user) {
            if (user !== null) {
                next();
            } else {
                res.sendStatus(ServerConstants.HTTP_STATUS_LOGIN_REJECTED);
            }
        });
    } else {
        var apiKey = req.get("apiKey") || req.params.apiKey || req.query.apiKey;

        var clientIp = requestIp.getClientIp(req);
        if (apiKey) {
            accounts.doApiKeyLogin(apiKey, clientIp, function(err, user) {
                if (user) {
                    addUserToSession(user, req, next);
                    next();
                } else {
                    res.sendStatus(ServerConstants.HTTP_STATUS_LOGIN_REJECTED);
                }
            });
        } else {
            res.sendStatus(ServerConstants.HTTP_STATUS_LOGIN_REJECTED);
        }
    }
};

exports.login = function(req, res, next) {

    var authConfig = config.get("authentication");

    if (authConfig && authConfig.disableLocalLogin) {
        res.status(UtilHttpStatus.FORBIDDEN).send("Login failed. Local login disabled.");
        return;
    }

    authentication.passport.authenticate('local',
        function (error, result) {
            if (error) {
                var message = "Login failed. " + error.message;
                if (error.errorCode === ErrorCodes.ERROR_PASSWORD_RESET_REQUIRED) {
                    res.status(UtilHttpStatus.LOCKED).send({
                        message,
                        resetPassRequirements: error.resetPassRequirements
                    });
                } else {
                    res.status(UtilHttpStatus.BAD_REQUEST).send(message);
                }

                return;
            }

            if (!result) {
                var message = "Login failed. Please check that you have entered the correct username and password.";

                if (error && error.errorCode === ErrorCodes.ERROR_DUPLICATE_ACCOUNT_EMAIL) {
                    message = "There is a problem with your account. " +
                        "Please see an administrator and quote error code: " + error.errorCode;
                }

                res.status(UtilHttpStatus.BAD_REQUEST).send(message);

                var clientAddress = req.headers["x-forwarded-for"];
                if(clientAddress == null){
                    logger.error("'x-forwarded-for' header not found. Returned client IP will be unreliable. " +
                        "Please configure the reverse proxy (usually nginx) to add this. Refer to issue 11138."
                    );
                    clientAddress = req._remoteAddress;
                }

                logger.info('Login attempt failed. ' +
                    'Email: \'' + req.body.email + '\', ' +
                    'Client: \'' + clientAddress  + '\', ' +
                    'Detail: \'' + (error ? error : 'Invalid email or password.') + '\''
                );

                return;
            }

            const { account, accountControl } = result;

            processLoginRules(account, accountControl,
                function loginSuccess() {
                    addUserToSession(account, req, next);
                },
                function callback(error, result) {
                    if (error) {
                        res.status(UtilHttpStatus.BAD_REQUEST).send(error.message);
                    } else {
                        res.send(account);
                    }
                }
            );
        })(req, res, next);
};

exports.publicLogin = asyncMiddleware(async (req, res, next) => {
    var result = {};
    let publicAccount;
    let controlDoc;

    // don't log public account in if user exist in the session
    if (req.user) {
        result.status = 'ok';
        result.user = req.user;
        res.json(result);
        return;
    }

    try {
        publicAccount = await accounts.findOneByPublicURL(req.body.context);
        controlDoc = await accounts.getAccountControlDocument(publicAccount.documentId);
    } catch (err) {
        let account = await accounts.findOne(req.body.context);
        controlDoc = await accounts.getAccountControlDocument(account.documentId);
        publicAccount = controlDoc && controlDoc.publicAccount === true ? account : null;
    }

    if (publicAccount) {
        processLoginRules(publicAccount, controlDoc,
            function loginSuccess() {
                addUserToSession(publicAccount, req, next);
            },
            function callback(error) {
                if (error) {
                    res.status(UtilHttpStatus.BAD_REQUEST).send(error.message);
                } else {
                    result.status = 'ok';
                    result.user = publicAccount;
                    res.json(result);
                }
            }
        );
    } else {
        result.status = 'err';
        result.message = 'Account is not public!';
        res.json(result);
    }
});

const handleSocialLogin = async (error, user, req, res, next) => {
  if (error) {
    res.redirect("/login?errorMessage=" + error.message);
  } else if (!user) {
    res.redirect("/login?errorMessage=Login failed. Please contact the administrator.");
  } else {
    try {
        const account = await accounts.findOne(user.documentId);
        const accountControl = await accounts.getAccountControlDocument(user.documentId);

        processLoginRules(account, accountControl,
            function loginSuccess() {
                addUserToSession(user, req, next);
            },
            function callback(error, result) {
                if (error) {
                    res.redirect("/login?errorMessage=" + error.message);
                } else {
                    res.redirect("/" + sharedUrlRoutes.serverRoutes.appDefault);
                }
            }
        );
    }  catch (error) {
        res.redirect("/login?errorMessage=" + error.message);
    };
  }
};

exports.loginSaml = function(req, res, next) {
    var fullDomainUrl = UtilHost.getFullDomainURL(req);

    authentication.passport.authenticate('saml', {
        issuer: fullDomainUrl,
        path: sharedUrlRoutes.serverRoutes.loginSaml,
        failureRedirect: '/login?errorMessage=Unknown account.'
            +'Please try again or create a new 0account.',
        failureFlash: true
    }, function (error, user) {
        handleSocialLogin(error, user, req, res, next);
    })(req, res, next);
};

exports.loginFacebook = authentication.authProvider('facebook');

exports.loginFacebookCallback = (req, res, next) => {
  authentication.passport.authenticate('facebook',
    { failureRedirect: '/login' },
    (error, user) => {
      handleSocialLogin(error, user, req, res, next);
    })(req, res, next);
};

exports.loginGoogle = authentication.authProvider('google');

exports.loginGoogleCallback = (req, res, next) => {
  authentication.passport.authenticate('google',
    { failureRedirect: '/login' }, (error, user) => {
      handleSocialLogin(error, user, req, res, next);
  })(req, res, next);
};

exports.logout = async function(req, res) {
    const account = await accounts.findOne(req.user.documentId);
    const accountControlDocument = await accounts.getAccountControlDocument(req.user.documentId);

    processLogoutRules(account, accountControlDocument,
        function logoutSuccess() {
            req.logout();
        },
        function callback(error, result) {
            if (error) {
                res.status(UtilHttpStatus.BAD_REQUEST).send(error.message);
            } else {
                res.status(UtilHttpStatus.OK).send();
            }
        });
};

exports.logoutAllDevices = async function(req, res) {
    if (!socket) {
        socket = require('../socket/SocketServer');
    }
    
    const userId = req.user ? req.user.documentId : null;
    if (userId){
        redisAccountDataService.removeAccountSessions(userId);
    }

    const account = await accounts.findOne(userId);
    const accountControlDocument = await accounts.getAccountControlDocument(userId);

    processLogoutRules(account, accountControlDocument,
        function logoutSuccess() {
            req.logout();
        },
        function callback(error, result) {
            if (error) {
                res.status(UtilHttpStatus.BAD_REQUEST).send(error.message);
            } else {
                socket.emit(SharedConstants.WEB_SOCKET_MESSAGE_SIGN_OUT_ALL_DEVICE, userId);
                res.status(UtilHttpStatus.OK).send();
            }
        });
};

//doesn't check if confirm password matches. it must be checked by client
exports.testPassword = function(req, res){

    var pass = req.body.pass;

    if(!pass){
        res.sendStatus(UtilHttpStatus.BAD_REQUEST);
        return;
    }

    var fails = PasswordChecker.evaluatePassword(pass, config.get("passwordCreationRules"));
    if(fails.length > 0){
        res.json({
            passwordFailures : fails
        });
    }else{
        res.sendStatus(UtilHttpStatus.OK);
    }

};

exports.getLoggedUserAccountGroupConfigDocument = function(req, res) {

  var userId = req.user ? req.user.documentId : null;

  accounts.getAccountGroupConfigDocument(userId)
    .then(function(configDocument){
      res.send(configDocument);
    })
    .catch(function(err){
      res.status(UtilHttpStatus.INTERNAL_SERVER_ERROR).send(err.message ? err.message : err);
    });

};
