/* jslint node: true */
/* eslint-disable no-console */
'use strict';

const {
    printUsageAndSetExitCode,
    getAnswers,
    ExitCodes,
    argv,
    initConfigAndDatabases,
} = require('./oputil_common.js');
const getHelpFor = require('./oputil_help.js').getHelpFor;
const Errors = require('../enig_error.js').Errors;
const UserProps = require('../user_property.js');

const async = require('async');
const _ = require('lodash');
const moment = require('moment');
const fs = require('fs-extra');
const Table = require('easy-table');

exports.handleUserCommand = handleUserCommand;

function initAndGetUser(userName, cb) {
    async.waterfall(
        [
            function init(callback) {
                initConfigAndDatabases(callback);
            },
            function getUserObject(callback) {
                const User = require('../../core/user.js');
                User.getUserIdAndName(userName, (err, userId) => {
                    if (err) {
                        //  try user ID if number was supplied
                        if (_.isNumber(userName)) {
                            return User.getUser(parseInt(userName), callback);
                        }
                        return callback(err);
                    }
                    return User.getUser(userId, callback);
                });
            },
        ],
        (err, user) => {
            return cb(err, user);
        }
    );
}

function setAccountStatus(user, status) {
    if (argv._.length < 3) {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    const AccountStatus = require('../../core/user.js').AccountStatus;

    status = {
        activate: AccountStatus.active,
        deactivate: AccountStatus.inactive,
        disable: AccountStatus.disabled,
        lock: AccountStatus.locked,
    }[status];

    const statusDesc = _.invert(AccountStatus)[status];

    async.series(
        [
            callback => {
                return user.persistProperty(UserProps.AccountStatus, status, callback);
            },
            callback => {
                if (AccountStatus.active !== status) {
                    return callback(null);
                }

                return user.unlockAccount(callback);
            },
        ],
        err => {
            if (err) {
                process.exitCode = ExitCodes.ERROR;
                console.error(err.message);
            } else {
                console.info(`User status set to ${statusDesc}`);
            }
        }
    );
}

function setUserPassword(user) {
    if (argv._.length < 4) {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    async.waterfall(
        [
            function validate(callback) {
                //	:TODO: prompt if no password provided (more secure, no history, etc.)
                const password = argv._[argv._.length - 1];
                if (0 === password.length) {
                    return callback(Errors.Invalid('Invalid password'));
                }
                return callback(null, password);
            },
            function set(password, callback) {
                user.setNewAuthCredentials(password, err => {
                    if (err) {
                        process.exitCode = ExitCodes.BAD_ARGS;
                    }
                    return callback(err);
                });
            },
        ],
        err => {
            if (err) {
                console.error(err.message);
            } else {
                console.info('New password set');
            }
        }
    );
}

function removeUserRecordsFromDbAndTable(dbName, tableName, userId, col, cb) {
    const db = require('../../core/database.js').dbs[dbName];
    db.run(
        `DELETE FROM ${tableName}
        WHERE ${col} = ?;`,
        [userId],
        err => {
            return cb(err);
        }
    );
}

function removeUser(user) {
    async.series(
        [
            callback => {
                if (user.isRoot()) {
                    return callback(Errors.Invalid('Cannot delete root/SysOp user!'));
                }

                return callback(null);
            },
            callback => {
                if (false === argv.prompt) {
                    return callback(null);
                }

                console.info('About to permanently delete the following user:');
                console.info(`Username : ${user.username}`);
                console.info(
                    `Real name: ${user.properties[UserProps.RealName] || 'N/A'}`
                );
                console.info(`User ID  : ${user.userId}`);
                console.info('WARNING: This cannot be undone!');
                getAnswers(
                    [
                        {
                            name: 'proceed',
                            message: `Proceed in deleting ${user.username}?`,
                            type: 'confirm',
                        },
                    ],
                    answers => {
                        if (answers.proceed) {
                            return callback(null);
                        }
                        return callback(Errors.General('User canceled'));
                    }
                );
            },
            callback => {
                //  op has confirmed they are wanting ready to proceed (or passed --no-prompt)
                const DeleteFrom = {
                    message: ['user_message_area_last_read'],
                    system: ['user_event_log'],
                    user: ['user_group_member', 'user'],
                    file: ['file_user_rating'],
                };

                async.eachSeries(
                    Object.keys(DeleteFrom),
                    (dbName, nextDbName) => {
                        const tables = DeleteFrom[dbName];
                        async.eachSeries(
                            tables,
                            (tableName, nextTableName) => {
                                const col =
                                    'user' === dbName && 'user' === tableName
                                        ? 'id'
                                        : 'user_id';
                                removeUserRecordsFromDbAndTable(
                                    dbName,
                                    tableName,
                                    user.userId,
                                    col,
                                    err => {
                                        return nextTableName(err);
                                    }
                                );
                            },
                            err => {
                                return nextDbName(err);
                            }
                        );
                    },
                    err => {
                        return callback(err);
                    }
                );
            },
            callback => {
                //
                //  Clean up *private* messages *to* this user
                //
                const Message = require('../../core/message.js');
                const MsgDb = require('../../core/database.js').dbs.message;

                const filter = {
                    resultType: 'id',
                    privateTagUserId: user.userId,
                };
                Message.findMessages(filter, (err, ids) => {
                    if (err) {
                        return callback(err);
                    }

                    async.eachSeries(
                        ids,
                        (messageId, nextMessageId) => {
                            MsgDb.run(
                                `DELETE FROM message
                            WHERE message_id = ?;`,
                                [messageId],
                                err => {
                                    return nextMessageId(err);
                                }
                            );
                        },
                        err => {
                            return callback(err);
                        }
                    );
                });
            },
        ],
        err => {
            if (err) {
                return console.error(err.reason ? err.reason : err.message);
            }

            console.info('User has been deleted.');
        }
    );
}

function renameUser(user) {
    if (argv._.length < 3) {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    const newUserName = argv._[argv._.length - 1];

    async.series(
        [
            callback => {
                const {
                    validateUserNameAvail,
                } = require('../../core/system_view_validate.js');
                return validateUserNameAvail(newUserName, callback);
            },
            callback => {
                const userDb = require('../../core/database.js').dbs.user;
                userDb.run(
                    `UPDATE user
                    SET user_name = ?
                    WHERE id = ?;`,
                    [newUserName, user.userId],
                    err => {
                        return callback(err);
                    }
                );
            },
        ],
        err => {
            if (err) {
                return console.error(err.reason ? err.reason : err.message);
            }
            return console.info(`User "${user.username}" renamed to "${newUserName}"`);
        }
    );
}

function modUserGroups(user) {
    if (argv._.length < 3) {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); //	remove any quotes - necessary to allow "-foo"
    let action = groupName[0]; //	+ or -

    if ('-' === action || '+' === action || '~' === action) {
        groupName = groupName.substr(1);
    }

    action = action || '+';

    if (0 === groupName.length) {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    //
    //	Groups are currently arbitrary, so do a slight validation
    //
    if (!/[A-Za-z0-9]+/.test(groupName)) {
        process.exitCode = ExitCodes.BAD_ARGS;
        return console.error('Bad group name');
    }

    function done(err) {
        if (err) {
            process.exitCode = ExitCodes.BAD_ARGS;
            console.error(err.message);
        } else {
            console.info('User groups modified');
        }
    }

    const UserGroup = require('../../core/user_group.js');
    if ('-' === action || '~' === action) {
        UserGroup.removeUserFromGroup(user.userId, groupName, done);
    } else {
        UserGroup.addUserToGroup(user.userId, groupName, done);
    }
}

function showUserInfo(user) {
    const User = require('../../core/user.js');

    const statusDesc = () => {
        const status = user.properties[UserProps.AccountStatus];
        return _.invert(User.AccountStatus)[status] || 'N/A';
    };

    const created = () => {
        const ac = user.properties[UserProps.AccountCreated];
        return ac ? moment(ac).format() : 'N/A';
    };

    const lastLogin = () => {
        const ll = user.properties[UserProps.LastLoginTs];
        return ll ? moment(ll).format() : 'N/A';
    };

    const propOrNA = p => {
        return user.properties[p] || 'N/A';
    };

    const currentTheme = () => {
        return user.properties[UserProps.ThemeId];
    };

    const stdInfo = `User information:
Username     : ${user.username}${user.isRoot() ? ' (root/SysOp)' : ''}
Real name    : ${propOrNA(UserProps.RealName)}
ID           : ${user.userId}
Status       : ${statusDesc()}
Groups       : ${user.groups.join(', ')}
Theme ID     : ${currentTheme()}
Created      : ${created()}
Last login   : ${lastLogin()}
Login count  : ${propOrNA(UserProps.LoginCount)}
Email        : ${propOrNA(UserProps.EmailAddress)}
Location     : ${propOrNA(UserProps.Location)}
Affiliations : ${propOrNA(UserProps.Affiliations)}`;
    let secInfo = '';
    if (argv.security) {
        const otp = user.getProperty(UserProps.AuthFactor2OTP);
        if (otp) {
            const backupCodesOrNa = () => {
                try {
                    return JSON.parse(
                        user.getProperty(UserProps.AuthFactor2OTPBackupCodes)
                    ).join(', ');
                } catch (e) {
                    return 'N/A';
                }
            };
            secInfo = `\n2FA OTP      : ${otp}
OTP secret   : ${user.getProperty(UserProps.AuthFactor2OTPSecret) || 'N/A'}
OTP Backup   : ${backupCodesOrNa()}`;
        }
    }

    console.info(`${stdInfo}${secInfo}`);
}

function twoFactorAuthOTP(user) {
    if (argv._.length < 4) {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    const {
        OTPTypes,
        prepareOTP,
        createBackupCodes,
    } = require('../../core/user_2fa_otp.js');

    let otpType = argv._[argv._.length - 1];

    //  shortcut for removal
    if ('disable' === otpType) {
        const props = [
            UserProps.AuthFactor2OTP,
            UserProps.AuthFactor2OTPSecret,
            UserProps.AuthFactor2OTPBackupCodes,
        ];
        return user.removeProperties(props, err => {
            if (err) {
                console.error(err.message);
            } else {
                console.info(`2FA OTP disabled for ${user.username}`);
            }
        });
    }

    async.waterfall(
        [
            function validate(callback) {
                //  :TODO: Prompt for if not supplied
                //  allow aliases for OTP types
                otpType =
                    {
                        google: OTPTypes.GoogleAuthenticator,
                        hotp: OTPTypes.RFC4266_HOTP,
                        totp: OTPTypes.RFC6238_TOTP,
                    }[otpType] || otpType;
                otpType = _.find(OTPTypes, t => {
                    return t.toLowerCase() === otpType.toLowerCase();
                });
                if (!otpType) {
                    return callback(Errors.Invalid('Invalid OTP type'));
                }
                return callback(null, otpType);
            },
            function prepare(otpType, callback) {
                const otpOpts = {
                    username: user.username,
                    qrType: argv['qr-type'] || 'ascii',
                };
                prepareOTP(otpType, otpOpts, (err, otpInfo) => {
                    return callback(
                        err,
                        Object.assign(otpInfo, {
                            otpType,
                            backupCodes: createBackupCodes(),
                        })
                    );
                });
            },
            function storeOrDisplayQR(otpInfo, callback) {
                if (!argv.out || !otpInfo.qr) {
                    return callback(null, otpInfo);
                }

                fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => {
                    return callback(err, otpInfo);
                });
            },
            function persist(otpInfo, callback) {
                const props = {
                    [UserProps.AuthFactor2OTP]: otpInfo.otpType,
                    [UserProps.AuthFactor2OTPSecret]: otpInfo.secret,
                    [UserProps.AuthFactor2OTPBackupCodes]: JSON.stringify(
                        otpInfo.backupCodes
                    ),
                };
                user.persistProperties(props, err => {
                    return callback(err, otpInfo);
                });
            },
        ],
        (err, otpInfo) => {
            if (err) {
                console.error(err.message);
            } else {
                console.info(`OTP enabled for  : ${user.username}`);
                console.info(`Secret           : ${otpInfo.secret}`);
                console.info(`Backup codes     : ${otpInfo.backupCodes.join(', ')}`);

                if (otpInfo.qr) {
                    if (!argv.out) {
                        console.info('--- Begin QR ---');
                        console.info(otpInfo.qr);
                        console.info('--- End QR ---');
                    } else {
                        console.info(`QR code saved to ${argv.out}`);
                    }
                }
            }
        }
    );
}

function listUsers() {
    //  oputil user list [disabled|inactive|active|locked|all]
    //  :TODO: --created-since SPEC and --last-called SPEC
    //  --created-since SPEC
    //  SPEC can be TIMESTAMP or e.g. "-1hour" or "-90days"
    let listWhat;
    if (argv._.length > 2) {
        listWhat = argv._[argv._.length - 1];
    } else {
        listWhat = 'all';
    }

    const sortBy = (argv.sort || 'id').toLowerCase();

    const User = require('../../core/user');
    if (!['all'].concat(Object.keys(User.AccountStatus)).includes(listWhat)) {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    async.waterfall(
        [
            callback => {
                const UserProps = require('../../core/user_property');

                const userListOpts = {
                    properties: [
                        UserProps.RealName,
                        UserProps.AccountStatus,
                        UserProps.AccountCreated,
                        UserProps.LastLoginTs,
                        UserProps.LoginCount,
                    ],
                };

                User.getUserList(userListOpts, (err, userList) => {
                    if (err) {
                        return callback(err);
                    }

                    if ('all' === listWhat) {
                        return callback(null, userList);
                    }

                    const accountStatusFilter = User.AccountStatus[listWhat].toString();

                    return callback(
                        null,
                        userList.filter(user => {
                            return user[UserProps.AccountStatus] === accountStatusFilter;
                        })
                    );
                });
            },
            (userList, callback) => {
                // default sort: by ID
                const sortById = (left, right) => {
                    return left.userId - right.userId;
                };

                const sortByLogin = prop => (left, right) => {
                    return parseInt(right[prop]) - parseInt(left[prop]);
                };

                const sortByString = prop => (left, right) => {
                    return left[prop].localeCompare(right[prop], {
                        sensitivity: false,
                        numeric: true,
                    });
                };

                const sortByTimestamp = prop => (left, right) => {
                    return moment(right[prop]) - moment(left[prop]);
                };

                let sorter;
                switch (sortBy) {
                    case 'username':
                        sorter = sortByString('userName');
                        break;
                    case 'realname':
                        sorter = sortByString(UserProps.RealName);
                        break;
                    case 'status':
                        sorter = sortByString(UserProps.AccountStatus);
                        break;
                    case 'created':
                        sorter = sortByTimestamp(UserProps.AccountCreated);
                        break;
                    case 'lastlogin':
                        sorter = sortByTimestamp(UserProps.LastLoginTs);
                        break;
                    case 'logins':
                        sorter = sortByLogin(UserProps.LoginCount);
                        break;

                    case 'id':
                    default:
                        sorter = sortById;
                        break;
                }

                userList.sort(sorter);

                const StatusNames = _.invert(User.AccountStatus);

                const propOrNA = (user, prop) => {
                    return user[prop] || 'N/A';
                };

                const timestampOrNA = (user, prop, format) => {
                    let ts = user[prop];
                    return ts ? moment(ts).format(format) : 'N/A';
                };

                const makeAccountStatus = status => {
                    return StatusNames[status] || 'N/A';
                };

                const table = new Table();
                userList.forEach(user => {
                    table.cell('ID', user.userId);
                    table.cell('Username', user.userName);
                    table.cell('Real Name', user[UserProps.RealName]);
                    table.cell(
                        'Status',
                        makeAccountStatus(user[UserProps.AccountStatus])
                    );
                    table.cell(
                        'Created',
                        timestampOrNA(user, UserProps.AccountCreated, 'YYYY-MM-DD')
                    );
                    table.cell(
                        'Last Login',
                        timestampOrNA(user, UserProps.LastLoginTs, 'YYYY-MM-DD HH::mm')
                    );
                    table.cell('Logins', propOrNA(user, UserProps.LoginCount));

                    table.newRow();
                });

                console.info(table.toString());

                return callback(null);
            },
        ],
        err => {
            if (err) {
                return console.error(err.reason ? err.reason : err.message);
            }
        }
    );
}

function handleUserCommand() {
    function errUsage() {
        return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
    }

    if (true === argv.help) {
        return errUsage();
    }

    const action = argv._[1];
    const userRequired = !['list'].includes(action);

    let userName;
    if (userRequired) {
        const usernameIdx = [
            'pw',
            'pass',
            'passwd',
            'password',
            'group',
            'mv',
            'rename',
            '2fa-otp',
            'otp',
        ].includes(action)
            ? argv._.length - 2
            : argv._.length - 1;
        userName = argv._[usernameIdx];
    }

    if (!userName && userRequired) {
        return errUsage();
    }

    initAndGetUser(userName, (err, user) => {
        if (userName && err) {
            process.exitCode = ExitCodes.ERROR;
            return console.error(err.message);
        }

        return (
            {
                pw: setUserPassword,
                passwd: setUserPassword,
                password: setUserPassword,

                rm: removeUser,
                remove: removeUser,
                del: removeUser,
                delete: removeUser,

                mv: renameUser,
                rename: renameUser,

                activate: setAccountStatus,
                deactivate: setAccountStatus,
                disable: setAccountStatus,
                lock: setAccountStatus,

                group: modUserGroups,

                info: showUserInfo,

                '2fa-otp': twoFactorAuthOTP,
                otp: twoFactorAuthOTP,
                list: listUsers,
            }[action] || errUsage
        )(user, action);
    });
}
