
'use strict';

const _ = require('lodash');
const validator = require('validator');

const db = require('../database');
const groups = require('../groups');
const user = require('../user');
const categories = require('../categories');
const plugins = require('../plugins');
const translator = require('../translator');
const utils = require('../utils');

const helpers = module.exports;

const uidToSystemGroup = {
	0: 'guests',
	'-1': 'spiders',
	'-2': 'fediverse',
};

helpers.isUsersAllowedTo = async function (privilege, uids, cid) {
	// Remote categories inherit world pseudo-category privileges
	if (!utils.isNumber(cid)) {
		cid = -1;
	}

	let allowed;
	const masked = await db.isSetMember(`cid:${cid}:privilegeMask`, privilege);
	if (!masked) {
		const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([
			groups.isMembers(uids, `cid:${cid}:privileges:${privilege}`),
			groups.isMembersOfGroupList(uids, `cid:${cid}:privileges:groups:${privilege}`),
		]);
		allowed = uids.map((uid, index) => hasUserPrivilege[index] || hasGroupPrivilege[index]);
	} else {
		allowed = uids.map(() => false);
	}

	const result = await plugins.hooks.fire('filter:privileges:isUsersAllowedTo', { allowed, privilege, uids, cid });
	return result.allowed;
};

helpers.isAllowedTo = async function (privilege, uidOrGroupName, cid) {
	const _cid = cid; // original passed-in cid needed for privilege mask checks

	// Remote categories (non-numeric) inherit world privileges
	if (Array.isArray(cid)) {
		cid = cid.map(cid => (utils.isNumber(cid) ? cid : -1));
	} else {
		cid = utils.isNumber(cid) ? cid : -1;
	}

	let allowed;
	if (Array.isArray(privilege) && !Array.isArray(cid)) {
		const mask = await db.isSetMembers(`cid:${_cid}:privilegeMask`, privilege);
		allowed = await isAllowedToPrivileges(privilege, uidOrGroupName, cid);
		allowed = allowed.map((allowed, idx) => mask[idx] ? false : allowed);
	} else if (Array.isArray(cid) && !Array.isArray(privilege)) {
		const mask = await db.isMemberOfSets(_cid.map(cid => `cid:${cid}:privilegeMask`), privilege);
		allowed = await isAllowedToCids(privilege, uidOrGroupName, cid);
		allowed = allowed.map((allowed, idx) => mask[idx] ? false : allowed);
	}

	if (allowed) {
		({ allowed } = await plugins.hooks.fire('filter:privileges:isAllowedTo', { allowed: allowed, privilege: privilege, uid: uidOrGroupName, cid: cid }));
		return allowed;
	}
	throw new Error('[[error:invalid-data]]');
};

async function isAllowedToCids(privilege, uidOrGroupName, cids) {
	if (!privilege) {
		return cids.map(() => false);
	}

	const groupKeys = cids.map(cid => `cid:${cid}:privileges:groups:${privilege}`);

	// Group handling
	if (isNaN(parseInt(uidOrGroupName, 10)) && (uidOrGroupName || '').length) {
		return await checkIfAllowedGroup(uidOrGroupName, groupKeys);
	}

	// User handling
	if (parseInt(uidOrGroupName, 10) <= 0) {
		return await isSystemGroupAllowedToCids(privilege, uidOrGroupName, cids);
	}

	const userKeys = cids.map(cid => `cid:${cid}:privileges:${privilege}`);
	return await checkIfAllowedUser(uidOrGroupName, userKeys, groupKeys);
}

async function isAllowedToPrivileges(privileges, uidOrGroupName, cid) {
	const groupKeys = privileges.map(privilege => `cid:${cid}:privileges:groups:${privilege}`);
	// Group handling
	if (isNaN(parseInt(uidOrGroupName, 10)) && (uidOrGroupName || '').length) {
		return await checkIfAllowedGroup(uidOrGroupName, groupKeys);
	}

	// User handling
	if (parseInt(uidOrGroupName, 10) <= 0) {
		return await isSystemGroupAllowedToPrivileges(privileges, uidOrGroupName, cid);
	}

	const userKeys = privileges.map(privilege => `cid:${cid}:privileges:${privilege}`);
	return await checkIfAllowedUser(uidOrGroupName, userKeys, groupKeys);
}

async function checkIfAllowedUser(uid, userKeys, groupKeys) {
	const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([
		groups.isMemberOfGroups(uid, userKeys),
		groups.isMemberOfGroupsList(uid, groupKeys),
	]);
	return userKeys.map((key, index) => hasUserPrivilege[index] || hasGroupPrivilege[index]);
}

async function checkIfAllowedGroup(groupName, groupKeys) {
	const sets = await Promise.all([
		groups.isMemberOfGroups(groupName, groupKeys),
		groups.isMemberOfGroups('registered-users', groupKeys),
	]);
	return groupKeys.map((key, index) => sets[0][index] || sets[1][index]);
}

async function isSystemGroupAllowedToCids(privilege, uid, cids) {
	const groupKeys = cids.map(cid => `cid:${cid}:privileges:groups:${privilege}`);
	return await groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys);
}

async function isSystemGroupAllowedToPrivileges(privileges, uid, cid) {
	const groupKeys = privileges.map(privilege => `cid:${cid}:privileges:groups:${privilege}`);
	return await groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys);
}

helpers.getUserPrivileges = async function (cid, userPrivileges) {
	let memberSets = await groups.getMembersOfGroups(userPrivileges.map(privilege => `cid:${cid}:privileges:${privilege}`));
	memberSets = memberSets.map(set => set.map(uid => parseInt(uid, 10)));

	const members = _.uniq(_.flatten(memberSets));
	const memberData = await user.getUsersFields(members, ['picture', 'username', 'banned']);

	memberData.forEach((member) => {
		member.privileges = {};
		for (let x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) {
			member.privileges[userPrivileges[x]] = memberSets[x].includes(parseInt(member.uid, 10));
		}
		const types = {};
		for (const [key] of Object.entries(member.privileges)) {
			types[key] = getType(key);
		}
		member.types = types;
	});

	return memberData;
};

helpers.getGroupPrivileges = async function (cid, groupPrivileges) {
	const [memberSets, allGroupNames] = await Promise.all([
		groups.getMembersOfGroups(groupPrivileges.map(privilege => `cid:${cid}:privileges:${privilege}`)),
		groups.getAllGroupNames('groups:createtime'),
	]);

	const uniqueGroups = _.uniq(_.flatten(memberSets));

	let groupNames = allGroupNames.filter(groupName => !groupName.includes(':privileges:') && uniqueGroups.includes(groupName));

	groupNames = groups.ephemeralGroups.concat(groupNames);
	moveToFront(groupNames, groups.BANNED_USERS);
	moveToFront(groupNames, 'Global Moderators');
	moveToFront(groupNames, 'unverified-users');
	moveToFront(groupNames, 'verified-users');
	moveToFront(groupNames, 'registered-users');

	const adminIndex = groupNames.indexOf('administrators');
	if (adminIndex !== -1) {
		groupNames.splice(adminIndex, 1);
	}
	const groupData = await groups.getGroupsFields(groupNames, ['private', 'system']);
	const memberData = groupNames.map((member, index) => {
		const memberPrivs = {};

		for (let x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) {
			memberPrivs[groupPrivileges[x]] = memberSets[x].includes(member);
		}
		const types = {};
		for (const [key] of Object.entries(memberPrivs)) {
			types[key] = getType(key);
		}
		return {
			name: validator.escape(member),
			nameEscaped: translator.escape(validator.escape(member)),
			privileges: memberPrivs,
			types: types,
			isPrivate: groupData[index] && !!groupData[index].private,
			isSystem: groupData[index] && !!groupData[index].system,
		};
	});
	return memberData;
};


function getType(privilege) {
	privilege = privilege.replace(/^groups:/, '');
	const global = require('./global');
	const categories = require('./categories');
	return global.getType(privilege) || categories.getType(privilege) || 'other';
}

function moveToFront(groupNames, groupToMove) {
	const index = groupNames.indexOf(groupToMove);
	if (index !== -1) {
		groupNames.splice(0, 0, groupNames.splice(index, 1)[0]);
	} else {
		groupNames.unshift(groupToMove);
	}
}

helpers.giveOrRescind = async function (method, privileges, cids, members) {
	members = Array.isArray(members) ? members : [members];
	cids = Array.isArray(cids) ? cids : [cids];
	for (const member of members) {
		const groupKeys = [];
		cids.forEach((cid) => {
			privileges.forEach((privilege) => {
				groupKeys.push(`cid:${cid}:privileges:${privilege}`);
			});
		});
		/* eslint-disable no-await-in-loop */
		await method(groupKeys, member);
	}
};

helpers.userOrGroupPrivileges = async function (cid, uidOrGroup, privilegeList) {
	const groupNames = privilegeList.map(privilege => `cid:${cid}:privileges:${privilege}`);
	const isMembers = await groups.isMemberOfGroups(uidOrGroup, groupNames);
	return _.zipObject(privilegeList, isMembers);
};

helpers.getUidsWithPrivilege = async (cids, privilege) => {
	const disabled = (await categories.getCategoriesFields(cids, ['disabled'])).map(obj => obj.disabled);

	const groupNames = cids.reduce((memo, cid) => {
		memo.push(`cid:${cid}:privileges:${privilege}`);
		memo.push(`cid:${cid}:privileges:groups:${privilege}`);
		return memo;
	}, []);

	const memberSets = await groups.getMembersOfGroups(groupNames);
	// Every other set is actually a list of user groups, not uids, so convert those to members
	const sets = memberSets.reduce((memo, set, idx) => {
		if (idx % 2) {
			memo.groupNames.push(set);
		} else {
			memo.uids.push(set);
		}

		return memo;
	}, { groupNames: [], uids: [] });

	const uniqGroups = _.uniq(_.flatten(sets.groupNames));
	const groupUids = await groups.getMembersOfGroups(uniqGroups);
	const map = _.zipObject(uniqGroups, groupUids);
	const uidsByCid = cids.map((cid, index) => {
		if (disabled[index]) {
			return [];
		}

		return _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g]))));
	});
	return uidsByCid;
};

require('../promisify')(helpers);
