Source: coreModules/coreFunctions.js

const luxon = require('luxon')
const crypto = require('crypto')
const { encode } = require('punycode')

const DateTime = luxon.DateTime

const db = require('./dbConnection')

/**
 * Convert a decimal number to a binary array, optionally padded to a certain length
 * Generated by github copilot
 * @param {*} N the denary value to be converted
 * @param {*} length 
 * @returns {Array} array of binary digits as strings
 */
function decimalToBinary(N, length = 0) {
    const bin = (N >>> 0).toString(2)
    const padded = length ? bin.padStart(length, '0') : bin
    return padded.split('').reverse()
}

// console.log(encodePermissionBitmask(["MANAGE_USERS", "MANAGE_EVENTS"]))
// console.log(decodePermissionBitmask(12))

/**
 * Encode an array of permission names into a bitmask integer
 * @param {Array} permissions 
 * @returns {Integer} value  of permission bitmask
 */
function encodePermissionBitmask(permissions) {
    let bitmask = ""
    let permData = [...global.permData]

    permData.reverse().forEach(permission => {
        if (permissions.includes(permission.permissionName)) {
            bitmask += "1"
        } else {
            bitmask += "0"
        }
    })

    return parseInt(bitmask, 2)
}

/**
 * Decode a permission bitmask integer into an object of permission names and boolean values
 * @param {Integer} bitmask 
 * @returns {Object} object of permission names and boolean values
 */
function decodePermissionBitmask(bitmask) {
    const permsBinary = decimalToBinary(bitmask)
    let userPerms = {}

    // Split up the binary string and reverse it to match permission order
    // Then iterate through each bit, referencing to the global permissions data and assign permission names accordingly into an object to be returned
    permsBinary.forEach((bit, index) => {
        let permission = global.permDataMapped[index]

        if (bit == "1") {
            userPerms[permission.permissionName] = true
        } else {
            userPerms[permission.permissionName] = false
        }
    })

    return userPerms
}

/**
 * Back end logic to check if a user's provided permissions meet the requirements specified. Contains the logic for both "AND" and "OR" operations as well as universal access for admins.
 * Will accept either an Integer bitmask or a decoded permissions object.
 * @param {Array} requiredPerms 
 * @param {Object} userPerms Can also be an integer value
 * @param {String} logicType "AND" or "OR"
 * @returns {Boolean}
 */
function hasPermissions(requiredPerms, userPerms, logicType = "AND") {

    if (typeof userPerms === 'object') {
        decodedUserPerms = userPerms
    } else {
        let decodedUserPerms = decodePermissionBitmask(userPerms)
    }

    if (decodedUserPerms["ADMINISTRATOR"] === true) {
        return true
    } else {
        if (logicType === "OR") {
            // If the logic type is set to OR then only one permission from the requiredPerms array needs to be true
            let accessGranted = false

            requiredPerms.forEach(perm => {
                if (decodedUserPerms[perm] === true) {
                    accessGranted = true
                }
            })

            return accessGranted
        } else {
            // If the logic type is set to AND (Default option) then all permissions from the requiredPerms array need to be true
            let accessGranted = true

            requiredPerms.forEach(perm => {
                if (decodedUserPerms[perm] !== true) {
                    accessGranted = false
                }
            })

            return accessGranted
        }
    }
}

/**
 * Hashes a password using the scrypt algorithm with a random salt, returning the combined salt and hash for storage
 * @param {String} password 
 * @returns {String} Salt and Hash
 */
function hashPassword(password) {
    // Generate random salt for the user
    const salt = crypto.randomBytes(16).toString('hex')

    // Hash the password with the salt
    const hash = crypto.scryptSync(password, salt, 64).toString('hex')

    // Return the combined result for storage
    return (`${salt}:${hash}`)
}

/**
 * Verifies a password by hashing it with the same salt and comparing it to the stored hash
 * @param {String} password 
 * @param {String} storedPassword 
 * @returns {Boolean}
 */
function verifyPassword(password, storedPassword) {
    // Split the stored hashed password into salt and hash, trim any white space to remove chance of errors
    const parts = String(storedPassword).trim().split(':')
    const [salt, hash] = parts.map(p => p.trim())

    // Hash the password with the salt
    const hashedPassword = crypto.scryptSync(password, salt, 64)
    const originalPassword = Buffer.from(hash, 'hex')

    if (hashedPassword.length !== originalPassword.length) return false

    // Compare the hashed password with the stored hash and return true or false if they match or not
    return crypto.timingSafeEqual(hashedPassword, originalPassword)
}

// Filter out columns that are in the exception list
function filterColumns(array, exceptionList) {
    let filteredColumns = array.map(user => {
        let filteredColumn = {}
        Object.keys(user).forEach(key => {
            if (!exceptionList.includes(key)) {
                filteredColumn[key] = user[key]
            }
        })
        return filteredColumn;
    })

    return filteredColumns
}

/**
 * Takes a userID and returns the user data from the database corresponding to that userID.
 * Returns first value as a boolean, false indicating failure, true indicating success.
 * The second value is either the the error data if it failed or the requested data if it succeeded.
 * Filters out so only selected data will be returned, password hash is not included.
 * @param {string} userID 
 * @param {function} cb 
 */
function getUserData(userID, cb) {
    if (userID) {
        db.query('SELECT * FROM users WHERE userID = ?', [userID], (err, results) => {
            if (err) {
                cb(false, err)
            } else {
                if (results.length === 0) {
                    cb(false, 'No user found with that ID')
                } else {
                    
                    let userData = {
                        userID: results[0].userID,
                        username: results[0].username,
                        perms: decodePermissionBitmask(results[0].perms),
                        permsValue: results[0].perms,
                        email: results[0].email,
                        DOB: results[0].DOB,
                        firstName: results[0].firstName,
                        lastName: results[0].lastName,
                        timezone: results[0].timezone,
                        accountCreated: DateTime.fromSeconds(results[0].accountCreated)
                    }

                    cb(true, userData)
                }
            }
        })
    } else {
        // Just returns a blank user profile for when there is no one logged in
        cb
        (
            true, 
            {
                userID: '',
                username: '',
                perms: decodePermissionBitmask(0),
                permsValue: 0,
                email: '',
                DOB: '',
                firstName: '',
                lastName: '',
                timezone: '',
                accountCreated: DateTime.utc()
            }
        )
    }
}

module.exports = {
    encodePermissionBitmask,
    decodePermissionBitmask,
    hasPermissions,
    filterColumns,
    hashPassword,
    verifyPassword,
    getUserData
}