'use strict';

const knex = require('../lib/knex');
const config = require('../lib/config');
const { enforce, castToInteger } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const entitySettings = require('../lib/entity-settings');
const interoperableErrors = require('../../shared/interoperable-errors');
const log = require('../lib/log');
const {getGlobalNamespaceId} = require('../../shared/namespaces');
const {getAdminId} = require('../../shared/users');

// TODO: This would really benefit from some permission cache connected to rebuildPermissions
// A bit of the problem is that the cache would have to expunged as the result of other processes modifying entites/permissions


async function listByEntityDTAjax(context, entityTypeId, entityId, params) {
    return await knex.transaction(async (tx) => {
        const entityType = entitySettings.getEntityType(entityTypeId);
        await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');

        return await dtHelpers.ajaxListTx(
            tx,
            params,
            builder => builder
                .from(entityType.sharesTable)
                .innerJoin('users', entityType.sharesTable + '.user', 'users.id')
                .innerJoin('generated_role_names', {
                    'generated_role_names.role': entityType.sharesTable + '.role',
                    'generated_role_names.entity_type': knex.raw('?', [entityTypeId])
                })
                .where(`${entityType.sharesTable}.entity`, entityId),
            ['users.username', 'users.name', 'generated_role_names.name', 'users.id', entityType.sharesTable + '.auto']
        );
    });
}

async function listByUserDTAjax(context, entityTypeId, userId, params) {
    return await knex.transaction(async (tx) => {
        const user = await tx('users').where('id', userId).first();
        if (!user) {
            shares.throwPermissionDenied();
        }

        await enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers');

        const entityType = entitySettings.getEntityType(entityTypeId);

        return await dtHelpers.ajaxListWithPermissionsTx(
            tx,
            context,
            [{entityTypeId}],
            params,
            builder => builder
                .from(entityType.sharesTable)
                .innerJoin(entityType.entitiesTable, entityType.sharesTable + '.entity', entityType.entitiesTable + '.id')
                .innerJoin('generated_role_names', 'generated_role_names.role', entityType.sharesTable + '.role')
                .where('generated_role_names.entity_type', entityTypeId)
                .where(entityType.sharesTable + '.user', userId),
            [entityType.entitiesTable + '.name', 'generated_role_names.name', entityType.entitiesTable + '.id', entityType.sharesTable + '.auto']
        );
    });
}

async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) {
    return await knex.transaction(async (tx) => {
        const entityType = entitySettings.getEntityType(entityTypeId);

        await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');

        return await dtHelpers.ajaxListTx(
            tx,
            params,
            builder => builder
                .from('users')
                .whereNotExists(function () {
                    return this
                        .select('*')
                        .from(entityType.sharesTable)
                        .whereRaw(`users.id = ${entityType.sharesTable}.user`)
                        .andWhere(`${entityType.sharesTable}.entity`, entityId);
                }),
            ['users.id', 'users.username', 'users.name']
        );
    });
}

async function listRolesDTAjax(entityTypeId, params) {
    return await dtHelpers.ajaxList(
        params,
        builder => builder
            .from('generated_role_names')
            .where({entity_type: entityTypeId}),
        ['role', 'name', 'description']
    );
}

async function assign(context, entityTypeId, entityId, userId, role) {
    const entityType = entitySettings.getEntityType(entityTypeId);

    await knex.transaction(async tx => {
        await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');

        enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');

        const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
        const entity = await tx(entityType.entitiesTable).where('id', entityId).select(['id', ...extraColumns]).first();
        enforce(entity, 'Invalid entity id');

        if (entityType.dependentPermissions) {
            enforce(!entityType.dependentPermissions.getParent(entity), 'Cannot share/unshare a dependent entity');
        }

        const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('role').first();

        if (entry) {
            if (!role) {
                await tx(entityType.sharesTable).where({user: userId, entity: entityId}).del();
            } else if (entry.role !== role) {
                await tx(entityType.sharesTable).where({user: userId, entity: entityId}).update('role', role);
            }
        } else {
            await tx(entityType.sharesTable).insert({
                user: userId,
                entity: entityId,
                role
            });
        }

        await tx(entityType.permissionsTable).where({user: userId, entity: entityId}).del();
        if (entityTypeId === 'namespace') {
            await rebuildPermissionsTx(tx, {userId});
        } else if (role) {
            await rebuildPermissionsTx(tx, { entityTypeId, entityId, userId });
        }
    });
}

async function rebuildPermissionsTx(tx, restriction) {
    restriction = restriction || {};

    const namespaceEntityType = entitySettings.getEntityType('namespace');

    // Collect entity types we care about
    let restrictedEntityTypes;
    if (restriction.entityTypeId) {
        const entityType = entitySettings.getEntityType(restriction.entityTypeId);
        restrictedEntityTypes = {
            [restriction.entityTypeId]: entityType
        };
    } else {
        restrictedEntityTypes = entitySettings.getEntityTypesWithPermissions();
    }


    // To prevent users locking out themselves, we consider user with id 1 to be the admin and always assign it
    // the admin role. The admin role is a global role that has admin===true
    // If this behavior is not desired, it is enough to delete the user with id 1.
    const adminUser = await tx('users').where('id', getAdminId()).first();
    if (adminUser) {
        let adminRole;
        for (const role in config.roles.global) {
            if (config.roles.global[role].admin) {
                adminRole = role;
                break;
            }
        }

        if (adminRole) {
            await tx('users').update('role', adminRole).where('id', getAdminId());
        }
    }

    // Reset root, own and shared namespaces shares as per the user roles
    const usersAutoSharesQry = tx('users')
        .select(['users.id', 'users.role', 'users.namespace']);
    if (restriction.userId) {
        usersAutoSharesQry.where('users.id', restriction.userId);
    }
    const usersAutoShares = await usersAutoSharesQry;

    for (const user of usersAutoShares) {
        const roleConf = config.roles.global[user.role];

        if (roleConf) {
            const desiredRoles = new Map();

            if (roleConf.sharedNamespaces) {
                for (const shrKey in roleConf.sharedNamespaces) {
                    const shrRole = roleConf.sharedNamespaces[shrKey];
                    const shrNsId = castToInteger(shrKey);

                    desiredRoles.set(shrNsId, shrRole);
                }
            }

            if (roleConf.ownNamespaceRole) {
                desiredRoles.set(user.namespace, roleConf.ownNamespaceRole);
            }

            if (roleConf.rootNamespaceRole) {
                desiredRoles.set(getGlobalNamespaceId(), roleConf.rootNamespaceRole);
            }

            for (const [nsId, role] of desiredRoles.entries()) {
                await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: nsId }).del();
                await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: nsId, role: role, auto: true });
            }
        }
    }


    // Build the map of all namespaces
    // nsMap is a map of namespaces - each of the following shape:
    // .id - id of the namespace
    // .namespace - id of the parent or null if no parent
    // .userPermissions - Map userId -> [entityTypeId] -> array of permissions
    // .transitiveUserPermissions - the same as above, but taking into account transitive permission obtained from namespace parents

    const namespaces = await tx('namespaces').select(['id', 'namespace']);

    const nsMap = new Map();
    for (const namespace of namespaces) {
        namespace.userPermissions = new Map();
        nsMap.set(namespace.id, namespace);
    }

    // This populates .userPermissions
    const nsSharesQuery = tx(namespaceEntityType.sharesTable).select(['entity', 'user', 'role']);
    if (restriction.userId) {
        nsSharesQuery.where('user', restriction.userId);
    }

    const nsShares = await nsSharesQuery;
    for (const nsShare of nsShares) {
        const ns = nsMap.get(nsShare.entity);

        const userPerms = {};
        ns.userPermissions.set(nsShare.user, userPerms);

        for (const entityTypeId in restrictedEntityTypes) {
            if (config.roles.namespace[nsShare.role] &&
                config.roles.namespace[nsShare.role].children &&
                config.roles.namespace[nsShare.role].children[entityTypeId]) {

                userPerms[entityTypeId] = new Set(config.roles.namespace[nsShare.role].children[entityTypeId]);

            } else {
                userPerms[entityTypeId] = new Set();
            }
        }
    }

    // This computes .transitiveUserPermissions
    for (const ns of nsMap.values()) {
        ns.transitiveUserPermissions = new Map();

        for (const userPermsPair of ns.userPermissions) {
            const userPerms = {};
            ns.transitiveUserPermissions.set(userPermsPair[0], userPerms);

            for (const entityTypeId in restrictedEntityTypes) {
                userPerms[entityTypeId] = new Set(userPermsPair[1][entityTypeId]);
            }
        }

        let parentId = ns.namespace;
        while (parentId) {
            const parent = nsMap.get(parentId);

            for (const userPermsPair of parent.userPermissions) {
                const user = userPermsPair[0];

                if (ns.transitiveUserPermissions.has(user)) {
                    const userPerms = ns.transitiveUserPermissions.get(user);

                    for (const entityTypeId in restrictedEntityTypes) {
                        for (const perm of userPermsPair[1][entityTypeId]) {
                            userPerms[entityTypeId].add(perm);
                        }
                    }
                } else {
                    const userPerms = {};
                    ns.transitiveUserPermissions.set(user, userPerms);

                    for (const entityTypeId in restrictedEntityTypes) {
                        userPerms[entityTypeId] = new Set(userPermsPair[1][entityTypeId]);
                    }
                }
            }

            parentId = parent.namespace;
        }
    }


    // This reads direct shares from DB, joins each with the permissions from namespaces and stores the permissions into DB
    for (const entityTypeId in restrictedEntityTypes) {
        const entityType = restrictedEntityTypes[entityTypeId];

        const expungeQuery = tx(entityType.permissionsTable).del();
        if (restriction.entityId) {
            expungeQuery.where('entity', restriction.entityId);
        }
        if (restriction.userId) {
            expungeQuery.where('user', restriction.userId);
        }
        await expungeQuery;

        const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
        const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace', ...extraColumns]);


        const notToBeInserted = new Set();
        if (restriction.entityId) {
            if (restriction.parentId) {
                notToBeInserted.add(restriction.parentId);
                entitiesQuery.whereIn('id', [restriction.entityId, restriction.parentId]);
            } else {
                entitiesQuery.where('id', restriction.entityId);
            }
        }
        const entities = await entitiesQuery;

        const parentEntities = new Map();
        let nonChildEntities;
        if (entityType.dependentPermissions) {
            nonChildEntities = [];

            for (const entity of entities) {
                const parent = entityType.dependentPermissions.getParent(entity);

                if (parent) {
                    let childEntities;
                    if (parentEntities.has(parent)) {
                        childEntities = parentEntities.get(parent);
                    } else {
                        childEntities = [];
                        parentEntities.set(parent, childEntities);
                    }

                    childEntities.push(entity.id);
                } else {
                    nonChildEntities.push(entity);
                }
            }
        } else {
            nonChildEntities = entities;
        }


        for (const entity of nonChildEntities) {
            const permsPerUser = new Map();

            if (entity.namespace) { // The root namespace has not parent namespace, thus the test
                const transitiveUserPermissions = nsMap.get(entity.namespace).transitiveUserPermissions;
                for (const transitivePermsPair of transitiveUserPermissions.entries()) {
                    permsPerUser.set(transitivePermsPair[0], new Set(transitivePermsPair[1][entityTypeId]));
                }
            }

            const directSharesQuery = tx(entityType.sharesTable).select(['user', 'role']).where('entity', entity.id);
            if (restriction.userId) {
                directSharesQuery.andWhere('user', restriction.userId);
            }
            const directShares = await directSharesQuery;

            for (const share of directShares) {
                let userPerms;
                if (permsPerUser.has(share.user)) {
                    userPerms = permsPerUser.get(share.user);
                } else {
                    userPerms = new Set();
                    permsPerUser.set(share.user, userPerms);
                }

                if (config.roles[entityTypeId][share.role] &&
                    config.roles[entityTypeId][share.role].permissions) {

                    for (const perm of config.roles[entityTypeId][share.role].permissions) {
                        userPerms.add(perm);
                    }
                }
            }

            if (!notToBeInserted.has(entity.id)) {
                for (const userPermsPair of permsPerUser.entries()) {
                    const data = [];

                    for (const operation of userPermsPair[1]) {
                        data.push({user: userPermsPair[0], entity: entity.id, operation});
                    }

                    if (data.length > 0) {
                        await tx(entityType.permissionsTable).insert(data);
                    }
                }
            }

            if (parentEntities.has(entity.id)) {
                const childEntities = parentEntities.get(entity.id);

                for (const childId of childEntities) {
                    for (const userPermsPair of permsPerUser.entries()) {
                        const data = [];

                        for (const operation of userPermsPair[1]) {
                            if (operation !== 'share') {
                                data.push({user: userPermsPair[0], entity: childId, operation});
                            }
                        }

                        if (data.length > 0) {
                            await tx(entityType.permissionsTable).insert(data);
                        }
                    }
                }
            }
        }
    }
}

async function rebuildPermissions(restriction) {
    await knex.transaction(async tx => {
        await rebuildPermissionsTx(tx, restriction);
    });
}

async function regenerateRoleNamesTable() {
    await knex.transaction(async tx => {
        await tx('generated_role_names').del();

        const entityTypeIds = ['global', ...Object.keys(entitySettings.getEntityTypesWithPermissions())];

        for (const entityTypeId of entityTypeIds) {
            const roles = config.roles[entityTypeId];

            for (const role in roles) {
                await tx('generated_role_names').insert({
                    entity_type: entityTypeId,
                    role,
                    name: roles[role].name,
                    description: roles[role].description,
                });
            }
        }
    });
}


function throwPermissionDenied() {
    throw new interoperableErrors.PermissionDeniedError('Permission denied');
}

async function removeDefaultShares(tx, user) {
    const namespaceEntityType = entitySettings.getEntityType('namespace');

    const roleConf = config.roles.global[user.role];

    if (roleConf) {
        const desiredRole = roleConf.rootNamespaceRole;

        if (roleConf.ownNamespaceRole) {
            await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: user.namespace }).del();
        }

        if (roleConf.rootNamespaceRole) {
            await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: getGlobalNamespaceId() }).del();
        }
    }
}

function checkGlobalPermission(context, requiredOperations) {
    if (!context.user) {
        return false;
    }

    if (typeof requiredOperations === 'string') {
        requiredOperations = [ requiredOperations ];
    }

    if (context.user.restrictedAccessHandler) {
        const originalRequiredOperations = requiredOperations;
        const allowedPerms = context.user.restrictedAccessHandler.globalPermissions;
        if (allowedPerms) {
            requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
        } else {
            requiredOperations = [];
        }
        log.verbose('check global permissions with restrictedAccessHandler --  requiredOperations: [' + originalRequiredOperations + '] -> [' + requiredOperations + ']');
    }

    if (requiredOperations.length === 0) {
        return false;
    }

    if (context.user.admin) { // This handles the getAdminContext() case
        return true;
    }

    const roleSpec = config.roles.global[context.user.role];
    let success = false;
    if (roleSpec) {
        for (const requiredOperation of requiredOperations) {
            if (roleSpec.permissions.includes(requiredOperation)) {
                success = true;
                break;
            }
        }
    }

    return success;
}

function enforceGlobalPermission(context, requiredOperations) {
    if (!checkGlobalPermission(context, requiredOperations)) {
        throwPermissionDenied();
    }
}

async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
    if (!context.user) {
        return false;
    }

    const entityType = entitySettings.getEntityType(entityTypeId);

    if (typeof requiredOperations === 'string') {
        requiredOperations = [ requiredOperations ];
    }

    requiredOperations = filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, requiredOperations, 'checkPermissions');

    if (requiredOperations.length === 0) {
        return false;
    }

    if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
        const existsQuery = tx(entityType.entitiesTable);

        if (entityId) {
            existsQuery.where('id', entityId);
        }

        const exists = await existsQuery.first();

        return !!exists;

    } else {
        const permsQuery = tx(entityType.permissionsTable)
            .where('user', context.user.id)
            .whereIn('operation', requiredOperations);

        if (entityId) {
            permsQuery.andWhere('entity', entityId);
        }

        const perms = await permsQuery.first();

        return !!perms;
    }
}

async function checkEntityPermission(context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        return false;
    }

    return await knex.transaction(async tx => {
        return await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
    });
}

async function checkEntityPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        return false;
    }

    return await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
}

async function checkTypePermission(context, entityTypeId, requiredOperations) {
    return await knex.transaction(async tx => {
        return await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
    });
}

async function enforceEntityPermission(context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        throwPermissionDenied();
    }
    await knex.transaction(async tx => {
        const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
        if (!result) {
            log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
            throwPermissionDenied();
        }
    });
}

async function enforceEntityPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        throwPermissionDenied();
    }
    const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
    if (!result) {
        log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
        throwPermissionDenied();
    }
}

async function enforceTypePermission(context, entityTypeId, requiredOperations) {
    await knex.transaction(async tx => {
        const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
        if (!result) {
            log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
            throwPermissionDenied();
        }
    });
}

async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperations) {
    const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
    if (!result) {
        log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
        throwPermissionDenied();
    }
}

function getGlobalPermissions(context) {
    if (!context.user) {
        return [];
    }

    enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');

    return (config.roles.global[context.user.role] || {}).permissions || [];
}

async function getPermissionsTx(tx, context, entityTypeId, entityId) {
    if (!context.user) {
        return [];
    }

    enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');

    const entityType = entitySettings.getEntityType(entityTypeId);

    const rows = await tx(entityType.permissionsTable)
        .select('operation')
        .where('entity', entityId)
        .where('user', context.user.id);

    const operations = rows.map(x => x.operation);
    return filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, operations, 'getPermissions');
}

// If entityId is null, it means that we require that restrictedAccessHandler does not differentiate based on entityId. This is used in ajaxListWithPermissionsTx.
function filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg) {
    if (context.user.restrictedAccessHandler) {
        const originalOperations = permissions;
        if (context.user.restrictedAccessHandler.permissions) {
            const entityPerms = context.user.restrictedAccessHandler.permissions[entityTypeId];

            if (!entityPerms) {
                permissions = [];
            } else if (entityPerms === true) {
                // no change to operations
            } else if (entityPerms instanceof Set) {
                permissions = permissions.filter(perm => entityPerms.has(perm));
            } else {
                if (entityId) {
                    const allowedPerms = entityPerms[entityId];
                    if (allowedPerms) {
                        permissions = permissions.filter(perm => allowedPerms.has(perm));
                    } else {
                        const allowedPerms = entityPerms['default'];
                        if (allowedPerms) {
                            permissions = permissions.filter(perm => allowedPerms.has(perm));
                        } else {
                            permissions = [];
                        }
                    }
                } else {
                    const allowedPerms = entityPerms['default'];
                    if (allowedPerms) {
                        permissions = permissions.filter(perm => allowedPerms.has(perm));
                    } else {
                        permissions = [];
                    }
                }
            }
        } else {
            permissions = [];
        }
        log.verbose(operationMsg + ' with restrictedAccessHandler --  entityTypeId: ' + entityTypeId + '  entityId: ' + entityId + '  operations: [' + originalOperations + '] -> [' + permissions + ']');
    }

    return permissions;
}

function isAccessibleByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg) {
    return filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg).length > 0;
}


module.exports.listByEntityDTAjax = listByEntityDTAjax;
module.exports.listByUserDTAjax = listByUserDTAjax;
module.exports.listUnassignedUsersDTAjax = listUnassignedUsersDTAjax;
module.exports.listRolesDTAjax = listRolesDTAjax;
module.exports.assign = assign;
module.exports.rebuildPermissionsTx = rebuildPermissionsTx;
module.exports.rebuildPermissions = rebuildPermissions;
module.exports.removeDefaultShares = removeDefaultShares;
module.exports.enforceEntityPermission = enforceEntityPermission;
module.exports.enforceEntityPermissionTx = enforceEntityPermissionTx;
module.exports.enforceTypePermission = enforceTypePermission;
module.exports.enforceTypePermissionTx = enforceTypePermissionTx;
module.exports.checkEntityPermissionTx = checkEntityPermissionTx;
module.exports.checkEntityPermission = checkEntityPermission;
module.exports.checkTypePermission = checkTypePermission;
module.exports.enforceGlobalPermission = enforceGlobalPermission;
module.exports.checkGlobalPermission = checkGlobalPermission;
module.exports.throwPermissionDenied = throwPermissionDenied;
module.exports.regenerateRoleNamesTable = regenerateRoleNamesTable;
module.exports.getGlobalPermissions = getGlobalPermissions;
module.exports.getPermissionsTx = getPermissionsTx;
module.exports.filterPermissionsByRestrictedAccessHandler = filterPermissionsByRestrictedAccessHandler;
module.exports.isAccessibleByRestrictedAccessHandler = isAccessibleByRestrictedAccessHandler;
'use strict';

const knex = require('../lib/knex');
const config = require('../lib/config');
const { enforce, castToInteger } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const entitySettings = require('../lib/entity-settings');
const interoperableErrors = require('../../shared/interoperable-errors');
const log = require('../lib/log');
const {getGlobalNamespaceId} = require('../../shared/namespaces');
const {getAdminId} = require('../../shared/users');

// TODO: This would really benefit from some permission cache connected to rebuildPermissions
// A bit of the problem is that the cache would have to expunged as the result of other processes modifying entites/permissions


async function listByEntityDTAjax(context, entityTypeId, entityId, params) {
    return await knex.transaction(async (tx) => {
        const entityType = entitySettings.getEntityType(entityTypeId);
        await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');

        return await dtHelpers.ajaxListTx(
            tx,
            params,
            builder => builder
                .from(entityType.sharesTable)
                .innerJoin('users', entityType.sharesTable + '.user', 'users.id')
                .innerJoin('generated_role_names', {
                    'generated_role_names.role': entityType.sharesTable + '.role',
                    'generated_role_names.entity_type': knex.raw('?', [entityTypeId])
                })
                .where(`${entityType.sharesTable}.entity`, entityId),
            ['users.username', 'users.name', 'generated_role_names.name', 'users.id', entityType.sharesTable + '.auto']
        );
    });
}

async function listByUserDTAjax(context, entityTypeId, userId, params) {
    return await knex.transaction(async (tx) => {
        const user = await tx('users').where('id', userId).first();
        if (!user) {
            shares.throwPermissionDenied();
        }

        await enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers');

        const entityType = entitySettings.getEntityType(entityTypeId);

        return await dtHelpers.ajaxListWithPermissionsTx(
            tx,
            context,
            [{entityTypeId}],
            params,
            builder => builder
                .from(entityType.sharesTable)
                .innerJoin(entityType.entitiesTable, entityType.sharesTable + '.entity', entityType.entitiesTable + '.id')
                .innerJoin('generated_role_names', 'generated_role_names.role', entityType.sharesTable + '.role')
                .where('generated_role_names.entity_type', entityTypeId)
                .where(entityType.sharesTable + '.user', userId),
            [entityType.entitiesTable + '.name', 'generated_role_names.name', entityType.entitiesTable + '.id', entityType.sharesTable + '.auto']
        );
    });
}

async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) {
    return await knex.transaction(async (tx) => {
        const entityType = entitySettings.getEntityType(entityTypeId);

        await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');

        return await dtHelpers.ajaxListTx(
            tx,
            params,
            builder => builder
                .from('users')
                .whereNotExists(function () {
                    return this
                        .select('*')
                        .from(entityType.sharesTable)
                        .whereRaw(`users.id = ${entityType.sharesTable}.user`)
                        .andWhere(`${entityType.sharesTable}.entity`, entityId);
                }),
            ['users.id', 'users.username', 'users.name']
        );
    });
}

async function listRolesDTAjax(entityTypeId, params) {
    return await dtHelpers.ajaxList(
        params,
        builder => builder
            .from('generated_role_names')
            .where({entity_type: entityTypeId}),
        ['role', 'name', 'description']
    );
}

async function assign(context, entityTypeId, entityId, userId, role) {
    const entityType = entitySettings.getEntityType(entityTypeId);

    await knex.transaction(async tx => {
        await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');

        enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');

        const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
        const entity = await tx(entityType.entitiesTable).where('id', entityId).select(['id', ...extraColumns]).first();
        enforce(entity, 'Invalid entity id');

        if (entityType.dependentPermissions) {
            enforce(!entityType.dependentPermissions.getParent(entity), 'Cannot share/unshare a dependent entity');
        }

        const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('role').first();

        if (entry) {
            if (!role) {
                await tx(entityType.sharesTable).where({user: userId, entity: entityId}).del();
            } else if (entry.role !== role) {
                await tx(entityType.sharesTable).where({user: userId, entity: entityId}).update('role', role);
            }
        } else {
            await tx(entityType.sharesTable).insert({
                user: userId,
                entity: entityId,
                role
            });
        }

        await tx(entityType.permissionsTable).where({user: userId, entity: entityId}).del();
        if (entityTypeId === 'namespace') {
            await rebuildPermissionsTx(tx, {userId});
        } else if (role) {
            await rebuildPermissionsTx(tx, { entityTypeId, entityId, userId });
        }
    });
}

async function rebuildPermissionsTx(tx, restriction) {
    restriction = restriction || {};

    const namespaceEntityType = entitySettings.getEntityType('namespace');

    // Collect entity types we care about
    let restrictedEntityTypes;
    if (restriction.entityTypeId) {
        const entityType = entitySettings.getEntityType(restriction.entityTypeId);
        restrictedEntityTypes = {
            [restriction.entityTypeId]: entityType
        };
    } else {
        restrictedEntityTypes = entitySettings.getEntityTypesWithPermissions();
    }


    // To prevent users locking out themselves, we consider user with id 1 to be the admin and always assign it
    // the admin role. The admin role is a global role that has admin===true
    // If this behavior is not desired, it is enough to delete the user with id 1.
    const adminUser = await tx('users').where('id', getAdminId()).first();
    if (adminUser) {
        let adminRole;
        for (const role in config.roles.global) {
            if (config.roles.global[role].admin) {
                adminRole = role;
                break;
            }
        }

        if (adminRole) {
            await tx('users').update('role', adminRole).where('id', getAdminId());
        }
    }

    // Reset root, own and shared namespaces shares as per the user roles
    const usersAutoSharesQry = tx('users')
        .select(['users.id', 'users.role', 'users.namespace']);
    if (restriction.userId) {
        usersAutoSharesQry.where('users.id', restriction.userId);
    }
    const usersAutoShares = await usersAutoSharesQry;

    for (const user of usersAutoShares) {
        const roleConf = config.roles.global[user.role];

        if (roleConf) {
            const desiredRoles = new Map();

            if (roleConf.sharedNamespaces) {
                for (const shrKey in roleConf.sharedNamespaces) {
                    const shrRole = roleConf.sharedNamespaces[shrKey];
                    const shrNsId = castToInteger(shrKey);

                    desiredRoles.set(shrNsId, shrRole);
                }
            }

            if (roleConf.ownNamespaceRole) {
                desiredRoles.set(user.namespace, roleConf.ownNamespaceRole);
            }

            if (roleConf.rootNamespaceRole) {
                desiredRoles.set(getGlobalNamespaceId(), roleConf.rootNamespaceRole);
            }

            for (const [nsId, role] of desiredRoles.entries()) {
                await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: nsId }).del();
                await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: nsId, role: role, auto: true });
            }
        }
    }


    // Build the map of all namespaces
    // nsMap is a map of namespaces - each of the following shape:
    // .id - id of the namespace
    // .namespace - id of the parent or null if no parent
    // .userPermissions - Map userId -> [entityTypeId] -> array of permissions
    // .transitiveUserPermissions - the same as above, but taking into account transitive permission obtained from namespace parents

    const namespaces = await tx('namespaces').select(['id', 'namespace']);

    const nsMap = new Map();
    for (const namespace of namespaces) {
        namespace.userPermissions = new Map();
        nsMap.set(namespace.id, namespace);
    }

    // This populates .userPermissions
    const nsSharesQuery = tx(namespaceEntityType.sharesTable).select(['entity', 'user', 'role']);
    if (restriction.userId) {
        nsSharesQuery.where('user', restriction.userId);
    }

    const nsShares = await nsSharesQuery;
    for (const nsShare of nsShares) {
        const ns = nsMap.get(nsShare.entity);

        const userPerms = {};
        ns.userPermissions.set(nsShare.user, userPerms);

        for (const entityTypeId in restrictedEntityTypes) {
            if (config.roles.namespace[nsShare.role] &&
                config.roles.namespace[nsShare.role].children &&
                config.roles.namespace[nsShare.role].children[entityTypeId]) {

                userPerms[entityTypeId] = new Set(config.roles.namespace[nsShare.role].children[entityTypeId]);

            } else {
                userPerms[entityTypeId] = new Set();
            }
        }
    }

    // This computes .transitiveUserPermissions
    for (const ns of nsMap.values()) {
        ns.transitiveUserPermissions = new Map();

        for (const userPermsPair of ns.userPermissions) {
            const userPerms = {};
            ns.transitiveUserPermissions.set(userPermsPair[0], userPerms);

            for (const entityTypeId in restrictedEntityTypes) {
                userPerms[entityTypeId] = new Set(userPermsPair[1][entityTypeId]);
            }
        }

        let parentId = ns.namespace;
        while (parentId) {
            const parent = nsMap.get(parentId);

            for (const userPermsPair of parent.userPermissions) {
                const user = userPermsPair[0];

                if (ns.transitiveUserPermissions.has(user)) {
                    const userPerms = ns.transitiveUserPermissions.get(user);

                    for (const entityTypeId in restrictedEntityTypes) {
                        for (const perm of userPermsPair[1][entityTypeId]) {
                            userPerms[entityTypeId].add(perm);
                        }
                    }
                } else {
                    const userPerms = {};
                    ns.transitiveUserPermissions.set(user, userPerms);

                    for (const entityTypeId in restrictedEntityTypes) {
                        userPerms[entityTypeId] = new Set(userPermsPair[1][entityTypeId]);
                    }
                }
            }

            parentId = parent.namespace;
        }
    }


    // This reads direct shares from DB, joins each with the permissions from namespaces and stores the permissions into DB
    for (const entityTypeId in restrictedEntityTypes) {
        const entityType = restrictedEntityTypes[entityTypeId];

        const expungeQuery = tx(entityType.permissionsTable).del();
        if (restriction.entityId) {
            expungeQuery.where('entity', restriction.entityId);
        }
        if (restriction.userId) {
            expungeQuery.where('user', restriction.userId);
        }
        await expungeQuery;

        const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
        const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace', ...extraColumns]);


        const notToBeInserted = new Set();
        if (restriction.entityId) {
            if (restriction.parentId) {
                notToBeInserted.add(restriction.parentId);
                entitiesQuery.whereIn('id', [restriction.entityId, restriction.parentId]);
            } else {
                entitiesQuery.where('id', restriction.entityId);
            }
        }
        const entities = await entitiesQuery;

        const parentEntities = new Map();
        let nonChildEntities;
        if (entityType.dependentPermissions) {
            nonChildEntities = [];

            for (const entity of entities) {
                const parent = entityType.dependentPermissions.getParent(entity);

                if (parent) {
                    let childEntities;
                    if (parentEntities.has(parent)) {
                        childEntities = parentEntities.get(parent);
                    } else {
                        childEntities = [];
                        parentEntities.set(parent, childEntities);
                    }

                    childEntities.push(entity.id);
                } else {
                    nonChildEntities.push(entity);
                }
            }
        } else {
            nonChildEntities = entities;
        }


        for (const entity of nonChildEntities) {
            const permsPerUser = new Map();

            if (entity.namespace) { // The root namespace has not parent namespace, thus the test
                const transitiveUserPermissions = nsMap.get(entity.namespace).transitiveUserPermissions;
                for (const transitivePermsPair of transitiveUserPermissions.entries()) {
                    permsPerUser.set(transitivePermsPair[0], new Set(transitivePermsPair[1][entityTypeId]));
                }
            }

            const directSharesQuery = tx(entityType.sharesTable).select(['user', 'role']).where('entity', entity.id);
            if (restriction.userId) {
                directSharesQuery.andWhere('user', restriction.userId);
            }
            const directShares = await directSharesQuery;

            for (const share of directShares) {
                let userPerms;
                if (permsPerUser.has(share.user)) {
                    userPerms = permsPerUser.get(share.user);
                } else {
                    userPerms = new Set();
                    permsPerUser.set(share.user, userPerms);
                }

                if (config.roles[entityTypeId][share.role] &&
                    config.roles[entityTypeId][share.role].permissions) {

                    for (const perm of config.roles[entityTypeId][share.role].permissions) {
                        userPerms.add(perm);
                    }
                }
            }

            if (!notToBeInserted.has(entity.id)) {
                for (const userPermsPair of permsPerUser.entries()) {
                    const data = [];

                    for (const operation of userPermsPair[1]) {
                        data.push({user: userPermsPair[0], entity: entity.id, operation});
                    }

                    if (data.length > 0) {
                        await tx(entityType.permissionsTable).insert(data);
                    }
                }
            }

            if (parentEntities.has(entity.id)) {
                const childEntities = parentEntities.get(entity.id);

                for (const childId of childEntities) {
                    for (const userPermsPair of permsPerUser.entries()) {
                        const data = [];

                        for (const operation of userPermsPair[1]) {
                            if (operation !== 'share') {
                                data.push({user: userPermsPair[0], entity: childId, operation});
                            }
                        }

                        if (data.length > 0) {
                            await tx(entityType.permissionsTable).insert(data);
                        }
                    }
                }
            }
        }
    }
}

async function rebuildPermissions(restriction) {
    await knex.transaction(async tx => {
        await rebuildPermissionsTx(tx, restriction);
    });
}

async function regenerateRoleNamesTable() {
    await knex.transaction(async tx => {
        await tx('generated_role_names').del();

        const entityTypeIds = ['global', ...Object.keys(entitySettings.getEntityTypesWithPermissions())];

        for (const entityTypeId of entityTypeIds) {
            const roles = config.roles[entityTypeId];

            for (const role in roles) {
                await tx('generated_role_names').insert({
                    entity_type: entityTypeId,
                    role,
                    name: roles[role].name,
                    description: roles[role].description,
                });
            }
        }
    });
}


function throwPermissionDenied() {
    throw new interoperableErrors.PermissionDeniedError('Permission denied');
}

async function removeDefaultShares(tx, user) {
    const namespaceEntityType = entitySettings.getEntityType('namespace');

    const roleConf = config.roles.global[user.role];

    if (roleConf) {
        const desiredRole = roleConf.rootNamespaceRole;

        if (roleConf.ownNamespaceRole) {
            await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: user.namespace }).del();
        }

        if (roleConf.rootNamespaceRole) {
            await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: getGlobalNamespaceId() }).del();
        }
    }
}

function checkGlobalPermission(context, requiredOperations) {
    if (!context.user) {
        return false;
    }

    if (typeof requiredOperations === 'string') {
        requiredOperations = [ requiredOperations ];
    }

    if (context.user.restrictedAccessHandler) {
        const originalRequiredOperations = requiredOperations;
        const allowedPerms = context.user.restrictedAccessHandler.globalPermissions;
        if (allowedPerms) {
            requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
        } else {
            requiredOperations = [];
        }
        log.verbose('check global permissions with restrictedAccessHandler --  requiredOperations: [' + originalRequiredOperations + '] -> [' + requiredOperations + ']');
    }

    if (requiredOperations.length === 0) {
        return false;
    }

    if (context.user.admin) { // This handles the getAdminContext() case
        return true;
    }

    const roleSpec = config.roles.global[context.user.role];
    let success = false;
    if (roleSpec) {
        for (const requiredOperation of requiredOperations) {
            if (roleSpec.permissions.includes(requiredOperation)) {
                success = true;
                break;
            }
        }
    }

    return success;
}

function enforceGlobalPermission(context, requiredOperations) {
    if (!checkGlobalPermission(context, requiredOperations)) {
        throwPermissionDenied();
    }
}

async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
    if (!context.user) {
        return false;
    }

    const entityType = entitySettings.getEntityType(entityTypeId);

    if (typeof requiredOperations === 'string') {
        requiredOperations = [ requiredOperations ];
    }

    requiredOperations = filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, requiredOperations, 'checkPermissions');

    if (requiredOperations.length === 0) {
        return false;
    }

    if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
        const existsQuery = tx(entityType.entitiesTable);

        if (entityId) {
            existsQuery.where('id', entityId);
        }

        const exists = await existsQuery.first();

        return !!exists;

    } else {
        const permsQuery = tx(entityType.permissionsTable)
            .where('user', context.user.id)
            .whereIn('operation', requiredOperations);

        if (entityId) {
            permsQuery.andWhere('entity', entityId);
        }

        const perms = await permsQuery.first();

        return !!perms;
    }
}

async function checkEntityPermission(context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        return false;
    }

    return await knex.transaction(async tx => {
        return await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
    });
}

async function checkEntityPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        return false;
    }

    return await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
}

async function checkTypePermission(context, entityTypeId, requiredOperations) {
    return await knex.transaction(async tx => {
        return await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
    });
}

async function enforceEntityPermission(context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        throwPermissionDenied();
    }
    await knex.transaction(async tx => {
        const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
        if (!result) {
            log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
            throwPermissionDenied();
        }
    });
}

async function enforceEntityPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
    if (!entityId) {
        throwPermissionDenied();
    }
    const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
    if (!result) {
        log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
        throwPermissionDenied();
    }
}

async function enforceTypePermission(context, entityTypeId, requiredOperations) {
    await knex.transaction(async tx => {
        const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
        if (!result) {
            log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
            throwPermissionDenied();
        }
    });
}

async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperations) {
    const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
    if (!result) {
        log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
        throwPermissionDenied();
    }
}

function getGlobalPermissions(context) {
    if (!context.user) {
        return [];
    }

    enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');

    return (config.roles.global[context.user.role] || {}).permissions || [];
}

async function getPermissionsTx(tx, context, entityTypeId, entityId) {
    if (!context.user) {
        return [];
    }

    enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');

    const entityType = entitySettings.getEntityType(entityTypeId);

    const rows = await tx(entityType.permissionsTable)
        .select('operation')
        .where('entity', entityId)
        .where('user', context.user.id);

    const operations = rows.map(x => x.operation);
    return filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, operations, 'getPermissions');
}

// If entityId is null, it means that we require that restrictedAccessHandler does not differentiate based on entityId. This is used in ajaxListWithPermissionsTx.
function filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg) {
    if (context.user.restrictedAccessHandler) {
        const originalOperations = permissions;
        if (context.user.restrictedAccessHandler.permissions) {
            const entityPerms = context.user.restrictedAccessHandler.permissions[entityTypeId];

            if (!entityPerms) {
                permissions = [];
            } else if (entityPerms === true) {
                // no change to operations
            } else if (entityPerms instanceof Set) {
                permissions = permissions.filter(perm => entityPerms.has(perm));
            } else {
                if (entityId) {
                    const allowedPerms = entityPerms[entityId];
                    if (allowedPerms) {
                        permissions = permissions.filter(perm => allowedPerms.has(perm));
                    } else {
                        const allowedPerms = entityPerms['default'];
                        if (allowedPerms) {
                            permissions = permissions.filter(perm => allowedPerms.has(perm));
                        } else {
                            permissions = [];
                        }
                    }
                } else {
                    const allowedPerms = entityPerms['default'];
                    if (allowedPerms) {
                        permissions = permissions.filter(perm => allowedPerms.has(perm));
                    } else {
                        permissions = [];
                    }
                }
            }
        } else {
            permissions = [];
        }
        log.verbose(operationMsg + ' with restrictedAccessHandler --  entityTypeId: ' + entityTypeId + '  entityId: ' + entityId + '  operations: [' + originalOperations + '] -> [' + permissions + ']');
    }

    return permissions;
}

function isAccessibleByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg) {
    return filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg).length > 0;
}


module.exports.listByEntityDTAjax = listByEntityDTAjax;
module.exports.listByUserDTAjax = listByUserDTAjax;
module.exports.listUnassignedUsersDTAjax = listUnassignedUsersDTAjax;
module.exports.listRolesDTAjax = listRolesDTAjax;
module.exports.assign = assign;
module.exports.rebuildPermissionsTx = rebuildPermissionsTx;
module.exports.rebuildPermissions = rebuildPermissions;
module.exports.removeDefaultShares = removeDefaultShares;
module.exports.enforceEntityPermission = enforceEntityPermission;
module.exports.enforceEntityPermissionTx = enforceEntityPermissionTx;
module.exports.enforceTypePermission = enforceTypePermission;
module.exports.enforceTypePermissionTx = enforceTypePermissionTx;
module.exports.checkEntityPermissionTx = checkEntityPermissionTx;
module.exports.checkEntityPermission = checkEntityPermission;
module.exports.checkTypePermission = checkTypePermission;
module.exports.enforceGlobalPermission = enforceGlobalPermission;
module.exports.checkGlobalPermission = checkGlobalPermission;
module.exports.throwPermissionDenied = throwPermissionDenied;
module.exports.regenerateRoleNamesTable = regenerateRoleNamesTable;
module.exports.getGlobalPermissions = getGlobalPermissions;
module.exports.getPermissionsTx = getPermissionsTx;
module.exports.filterPermissionsByRestrictedAccessHandler = filterPermissionsByRestrictedAccessHandler;
module.exports.isAccessibleByRestrictedAccessHandler = isAccessibleByRestrictedAccessHandler;