'use strict';

const _ = require('lodash');
const path = require('path');
const nconf = require('nconf');
const { rimraf } = require('rimraf');

const db = require('../database');
const posts = require('../posts');
const flags = require('../flags');
const topics = require('../topics');
const groups = require('../groups');
const messaging = require('../messaging');
const plugins = require('../plugins');
const activitypub = require('../activitypub');
const batch = require('../batch');
const utils = require('../utils');

module.exports = function (User) {
	const deletesInProgress = {};

	User.delete = async (callerUid, uid) => {
		await User.deleteContent(callerUid, uid);
		return await User.deleteAccount(uid);
	};

	User.deleteContent = async function (callerUid, uid) {
		if (utils.isNumber(uid) && parseInt(uid, 10) <= 0) {
			throw new Error('[[error:invalid-uid]]');
		}
		if (deletesInProgress[uid]) {
			throw new Error('[[error:already-deleting]]');
		}
		deletesInProgress[uid] = 'user.delete';
		async function deletePostsTopics() {
			await deletePosts(callerUid, uid);
			await deleteTopics(callerUid, uid);
		}
		await Promise.all([
			deletePostsTopics(),
			deleteUploads(callerUid, uid),
			deleteQueued(uid),
		]);
		delete deletesInProgress[uid];
	};

	async function deletePosts(callerUid, uid) {
		await batch.processSortedSet(`uid:${uid}:posts`, async (pids) => {
			await posts.purge(pids, callerUid);
			await db.sortedSetRemove(`uid:${uid}:posts`, pids);
		}, { alwaysStartAt: 0, batch: 500 });
	}

	async function deleteTopics(callerUid, uid) {
		await batch.processSortedSet(`uid:${uid}:topics`, async (tids) => {
			await topics.purge(tids, callerUid);
			await db.sortedSetRemove(`uid:${uid}:topics`, tids);
		}, { alwaysStartAt: 0, batch: 100 });
	}

	async function deleteUploads(callerUid, uid) {
		await batch.processSortedSet(`uid:${uid}:uploads`, async (uploads) => {
			await User.deleteUpload(callerUid, uid, uploads);
		}, { alwaysStartAt: 0, batch: 500 });
	}

	async function deleteQueued(uid) {
		let deleteIds = [];
		await batch.processSortedSet('post:queue', async (ids) => {
			const data = await db.getObjects(ids.map(id => `post:queue:${id}`));
			const userQueuedIds = data.filter(d => String(d.uid) === String(uid)).map(d => d.id);
			deleteIds = deleteIds.concat(userQueuedIds);
		}, { batch: 500 });

		for (const id of deleteIds) {
			// eslint-disable-next-line no-await-in-loop
			await posts.removeFromQueue(id);
		}
	}

	async function removeFromSortedSets(uid) {
		await db.sortedSetsRemove([
			'users:joindate',
			'users:postcount',
			'users:reputation',
			'users:banned',
			'users:banned:expire',
			'users:flags',
			'users:online',
			'digest:day:uids',
			'digest:week:uids',
			'digest:biweek:uids',
			'digest:month:uids',
		], uid);
	}

	User.deleteAccount = async function (uid) {
		if (deletesInProgress[uid] === 'user.deleteAccount') {
			throw new Error('[[error:already-deleting]]');
		}
		deletesInProgress[uid] = 'user.deleteAccount';
		const isLocal = utils.isNumber(uid);
		await removeFromSortedSets(uid);
		const userData = await db.getObject(isLocal ? `user:${uid}` : `userRemote:${uid}`);

		if (!userData || !userData.username) {
			delete deletesInProgress[uid];
			throw new Error('[[error:no-user]]');
		}

		await plugins.hooks.fire('static:user.delete', { uid: uid, userData: userData });
		await Promise.all([
			deleteVotes(uid),
			deleteChats(uid),
			User.auth.revokeAllSessions(uid),
		]);

		const keys = [
			`uid:${uid}:notifications:read`,
			`uid:${uid}:notifications:unread`,
			`uid:${uid}:bookmarks`,
			`uid:${uid}:tids_read`,
			`uid:${uid}:tids_unread`,
			`uid:${uid}:blocked_uids`,
			`user:${uid}:settings`,
			`user:${uid}:usernames`,
			`user:${uid}:emails`,
			`uid:${uid}:topics`, `uid:${uid}:posts`,
			`uid:${uid}:chats`, `uid:${uid}:chats:unread`,
			`uid:${uid}:chat:rooms`,
			`uid:${uid}:chat:rooms:unread`,
			`uid:${uid}:chat:rooms:read`,
			`uid:${uid}:upvote`, `uid:${uid}:downvote`,
			`uid:${uid}:flag:pids`,
			`uid:${uid}:sessions`,
			`uid:${uid}:shares`,
			`uid:${uid}:profile:pictures`,
			`invitation:uid:${uid}`,
		];

		const bulkRemove = [
			['username:uid', userData.username],
			['username:sorted', `${userData.username.toLowerCase()}:${uid}`],
			['userslug:uid', userData.userslug],
			['fullname:uid', userData.fullname],
		];
		if (userData.email) {
			bulkRemove.push(['email:uid', userData.email.toLowerCase()]);
			bulkRemove.push(['email:sorted', `${userData.email.toLowerCase()}:${uid}`]);
		}

		if (userData.fullname) {
			bulkRemove.push(['fullname:sorted', `${userData.fullname.toLowerCase()}:${uid}`]);
		}

		await Promise.all([
			db.sortedSetRemoveBulk(bulkRemove),
			isLocal ? db.decrObjectField('global', 'userCount') : null,
			db.deleteAll(keys),
			db.setRemove('invitation:uids', uid),
			deleteUserIps(uid),
			deleteUserFromFollowers(uid),
			deleteUserFromFollowedTopics(uid),
			deleteUserFromIgnoredTopics(uid),
			deleteUserFromFollowedTags(uid),
			deleteImages(uid),
			groups.leaveAllGroups(uid),
			flags.resolveFlag('user', uid, uid),
			User.reset.cleanByUid(uid),
			User.email.expireValidation(uid),
			!isLocal ? activitypub.actors.remove(uid) : null,
		]);
		await db.deleteAll([
			`followers:${uid}`, `following:${uid}`,
			`uid:${uid}:followed_tags`, `uid:${uid}:followed_tids`,
			`uid:${uid}:ignored_tids`,
			`${isLocal ? 'user' : 'userRemote'}:${uid}`,
		]);
		delete deletesInProgress[uid];
		return userData;
	};

	async function deleteUserFromFollowedTopics(uid) {
		const tids = await db.getSortedSetRange(`uid:${uid}:followed_tids`, 0, -1);
		await db.setsRemove(tids.map(tid => `tid:${tid}:followers`), uid);
	}

	async function deleteUserFromIgnoredTopics(uid) {
		const tids = await db.getSortedSetRange(`uid:${uid}:ignored_tids`, 0, -1);
		await db.setsRemove(tids.map(tid => `tid:${tid}:ignorers`), uid);
	}

	async function deleteUserFromFollowedTags(uid) {
		const tags = await db.getSortedSetRange(`uid:${uid}:followed_tags`, 0, -1);
		await db.sortedSetsRemove(tags.map(tag => `tag:${tag}:followers`), uid);
	}

	async function deleteVotes(uid) {
		const upvoteDownvotePids = await db.getSortedSetRange([
			`uid:${uid}:upvote`, `uid:${uid}:downvote`,
		], 0, -1);
		const pids = _.uniq(upvoteDownvotePids).filter(Boolean);
		for (const pid of pids) {
			// eslint-disable-next-line no-await-in-loop
			await posts.unvote(pid, uid);
		}
	}

	async function deleteChats(uid) {
		const roomIds = await db.getSortedSetRange([
			`uid:${uid}:chat:rooms`, `chat:rooms:public`,
		], 0, -1);
		await messaging.leaveRooms(uid, roomIds);
	}

	async function deleteUserIps(uid) {
		const ips = await db.getSortedSetRange(`uid:${uid}:ip`, 0, -1);
		await Promise.all([
			db.sortedSetsRemove(ips.map(ip => `ip:${ip}:uid`), uid),
			db.delete(`uid:${uid}:ip`),
		]);
	}

	async function deleteUserFromFollowers(uid) {
		const [followers, following] = await Promise.all([
			db.getSortedSetRange(`followers:${uid}`, 0, -1),
			db.getSortedSetRange(`following:${uid}`, 0, -1),
		]);

		async function updateCount(uids, name, fieldName) {
			await batch.processArray(uids, async (uids) => {
				const counts = await db.sortedSetsCard(uids.map(uid => name + uid));
				const bulkSet = counts.map(
					(count, index) => ([`user:${uids[index]}`, { [fieldName]: count || 0 }])
				);
				await db.setObjectBulk(bulkSet);
			}, {
				batch: 500,
			});
		}

		const followingSets = followers.map(uid => `following:${uid}`);
		const followerSets = following.map(uid => `followers:${uid}`);

		await db.sortedSetsRemove(followerSets.concat(followingSets), uid);
		await Promise.all([
			updateCount(following, 'followers:', 'followerCount'),
			updateCount(followers, 'following:', 'followingCount'),
		]);
	}

	async function deleteImages(uid) {
		if (utils.isNumber(uid)) {
			const folder = path.join(nconf.get('upload_path'), 'profile', `uid-${uid}`);
			await rimraf(folder);
		}
	}
};
