'use strict';

const escapeRegexp = require('escape-string-regexp');

const knex = require('./knex');
const { indexApi } = require('./manticore');
const bulkInsert = require('./utils/bulk-insert');
const { HttpError } = require('./errors');

async function addAlert(alert, sessionUser) {
	if (!sessionUser) {
		throw new HttpError('You are not authenthicated', 401);
	}

	if ((!alert.actors || alert.actors.length === 0) && (!alert.tags || alert.tags.length === 0) && (!alert.entities || alert.entities.length === 0) && (!alert.matches || alert.matches.length === 0)) {
		throw new HttpError('Alert must contain at least one actor, tag or entity', 400);
	}

	if (alert.matches?.some((match) => !match.property || !match.expression)) {
		throw new HttpError('Match must define a property and an expression', 400);
	}

	const [{ id: alertId }] = await knex('alerts')
		.insert({
			user_id: sessionUser.id,
			notify: alert.notify,
			email: alert.email,
			all: alert.all,
		})
		.returning('id');

	await Promise.all([
		alert.actors?.length > 0 && bulkInsert('alerts_actors', alert.actors.map((actorId) => ({
				alert_id: alertId,
				actor_id: actorId,
			})), false),
		alert.tags?.length > 0 && bulkInsert('alerts_tags', alert.tags.map((tagId) => ({
				alert_id: alertId,
				tag_id: tagId,
			})), false),
		alert.matches?.length > 0 && bulkInsert('alerts_matches', alert.matches.map((match) => ({
				alert_id: alertId,
				property: match.property,
				expression: /\/.*\//.test(match.expression)
					? match.expression.slice(1, -1)
					: escapeRegexp(match.expression),
			})), false),
		alert.stashes?.length > 0 && bulkInsert('alerts_stashes', alert.stashes.map((stashId) => ({
				alert_id: alertId,
				stash_id: stashId,
			})), false),
		alert.entities && bulkInsert('alerts_entities', alert.entities.map((entityId) => ({
			alert_id: alertId,
			entity_id: entityId,
		})).slice(0, alert.all ? 1 : Infinity), false), // one scene can never match multiple entities in AND mode
	]);

	return alertId;
}

async function removeAlert(alertId) {
	await knex('alerts').where('id', alertId).delete();
}

async function notify(scenes) {
	const sceneIds = scenes.map((scene) => scene.id);

	const [
		releasesActors,
		releasesTags,
		rawAlerts,
		alertsActors,
		alertsTags,
		alertsEntities,
		alertsMatches,
		alertsStashes,
	] = await Promise.all([
		knex('releases_actors').whereIn('release_id', sceneIds),
		knex('releases_tags').whereIn('release_id', sceneIds),
		knex('alerts'),
		knex('alerts_actors'),
		knex('alerts_tags'),
		knex('alerts_entities'),
		knex('alerts_matches'),
		knex('alerts_stashes'),
	]);

	const actorIdsByReleaseId = releasesActors.reduce((acc, releaseActor) => {
		if (!acc[releaseActor.release_id]) {
			acc[releaseActor.release_id] = [];
		}

		acc[releaseActor.release_id].push(releaseActor.actor_id);

		return acc;
	}, {});

	const tagIdsByReleaseId = releasesTags.reduce((acc, releaseTag) => {
		if (!acc[releaseTag.release_id]) {
			acc[releaseTag.release_id] = [];
		}

		acc[releaseTag.release_id].push(releaseTag.tag_id);

		return acc;
	}, {});

	const alertsActorsByAlertId = alertsActors.reduce((acc, alertActor) => { if (!acc[alertActor.alert_id]) { acc[alertActor.alert_id] = []; } acc[alertActor.alert_id].push(alertActor.actor_id); return acc; }, {});
	const alertsTagsByAlertId = alertsTags.reduce((acc, alertTag) => { if (!acc[alertTag.alert_id]) { acc[alertTag.alert_id] = []; } acc[alertTag.alert_id].push(alertTag.tag_id); return acc; }, {});
	const alertsEntitiesByAlertId = alertsEntities.reduce((acc, alertEntity) => { if (!acc[alertEntity.alert_id]) { acc[alertEntity.alert_id] = []; } acc[alertEntity.alert_id].push(alertEntity.entity_id); return acc; }, {});
	const alertsStashesByAlertId = alertsStashes.reduce((acc, alertStash) => { if (!acc[alertStash.alert_id]) { acc[alertStash.alert_id] = []; } acc[alertStash.alert_id].push(alertStash.stash_id); return acc; }, {});

	const alertsMatchesByAlertId = alertsMatches.reduce((acc, alertMatch) => {
		if (!acc[alertMatch.alert_id]) {
			acc[alertMatch.alert_id] = [];
		}

		acc[alertMatch.alert_id].push({
			property: alertMatch.property,
			expression: new RegExp(alertMatch.expression, 'ui'),
		});

		return acc;
	}, {});

	const alerts = rawAlerts.map((alert) => ({
		id: alert.id,
		userId: alert.user_id,
		notify: alert.notify,
		email: alert.email,
		all: alert.all,
		allActors: alert.all_actors,
		allEntities: alert.all_entities,
		allTags: alert.all_tags,
		allMatches: alert.all_matches,
		actors: alertsActorsByAlertId[alert.id] || [],
		tags: alertsTagsByAlertId[alert.id] || [],
		entities: alertsEntitiesByAlertId[alert.id] || [],
		matches: alertsMatchesByAlertId[alert.id] || [],
		stashes: alertsStashesByAlertId[alert.id] || [],
	}));

	const curatedScenes = scenes.map((scene) => ({
		id: scene.id,
		title: scene.title,
		description: scene.description,
		actorIds: actorIdsByReleaseId[scene.id] || [],
		tagIds: tagIdsByReleaseId[scene.id] || [],
		entityId: scene.entity.id,
		parentEntityId: scene.entity.parent?.id,
	}));

	const triggers = alerts.flatMap((alert) => {
		const alertScenes = curatedScenes.filter((scene) => {
			if (alert.all) {
				if (alert.actors.length > 0 && !alert.actors[alert.allActors ? 'every' : 'some']((actorId) => scene.actorIds.includes(actorId))) {
					return false;
				}

				if (alert.tags.length > 0 && !alert.tags[alert.allTags ? 'every' : 'some']((tagId) => scene.tagIds.includes(tagId))) {
					return false;
				}

				// multiple entities can only be matched in OR mode
				if (alert.entities.length > 0 && !alert.entities.some((alertEntityId) => alertEntityId === scene.entityId || alertEntityId === scene.parentEntityId)) {
					return false;
				}

				if (alert.matches.length > 0 && !alert.matches[alert.allMatches ? 'every' : 'some']((match) => match.expression.test(scene[match.property]))) {
					return false;
				}

				return true;
			}

			if (alert.matches.length > 0 && alert.actors[alert.allActors ? 'every' : 'some']((actorId) => scene.actorIds.includes(actorId))) {
				return true;
			}

			if (alert.tags.length > 0 && alert.tags[alert.allTags ? 'every' : 'some']((tagId) => scene.tagIds.includes(tagId))) {
				return true;
			}

			// multiple entities can only be matched in OR mode
			if (alert.entities.length > 0 && alert.entities.some((alertEntityId) => alertEntityId === scene.entityId || alertEntityId === scene.parentEntityId)) {
				return true;
			}

			if (alert.matches.length > 0 && alert.matches[alert.allMatches ? 'every' : 'some']((match) => match.expression.test(scene[match.property]))) {
				return true;
			}

			return false;
		});

		return alertScenes.map((scene) => ({
			sceneId: scene.id,
			alert,
		}));
	});

	const notifications = Object.values(Object.fromEntries(triggers // prevent multiple notifications for the same scene
		.filter((trigger) => trigger.alert.notify)
		.map((trigger) => [`${trigger.alert.userId}:${trigger.sceneId}`, trigger])))
		.map((trigger) => ({
			user_id: trigger.alert.userId,
			alert_id: trigger.alert.id,
			scene_id: trigger.sceneId,
		}));

	const uniqueStashes = Object.values(Object.fromEntries(triggers.flatMap((trigger) => trigger.alert.stashes.map((stashId) => ({
		stashId,
		sceneId: trigger.sceneId,
		userId: trigger.alert.userId,
	}))).map((stash) => [`${stash.stashId}:${stash.sceneId}`, stash])));

	const stashEntries = uniqueStashes.map((stash) => ({
		scene_id: stash.sceneId,
		stash_id: stash.stashId,
	}));

	const [stashed] = await Promise.all([
		bulkInsert('stashes_scenes', stashEntries, false),
		bulkInsert('notifications', notifications, false),
	]);

	// we need created_at from the databased, but user_id is not returned. it's easier to query it than to try and merge it with the input data
	const stashedEntries = await knex('stashes_scenes')
		.select('stashes_scenes.*', 'stashes.user_id')
		.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
		.whereIn('stashes_scenes.id', stashed.map((stash) => stash.id));

	const docs = stashedEntries.map((stash) => ({
		replace: {
			index: 'scenes_stashed',
			id: stash.id,
			doc: {
				scene_id: stash.scene_id,
				user_id: stash.user_id,
				stash_id: stash.stash_id,
				created_at: Math.round(stash.created_at.getTime() / 1000),
			},
		},
	}));

	if (docs.length > 0) {
		await indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
	}

	return triggers;
}

async function updateNotification(notificationId, notification, sessionUser) {
	await knex('notifications')
		.where('user_id', sessionUser.id)
		.where('id', notificationId)
		.update({
			seen: notification.seen,
		});
}

async function updateNotifications(notification, sessionUser) {
	await knex('notifications')
		.where('user_id', sessionUser.id)
		.update({
			seen: notification.seen,
		});
}

module.exports = {
	addAlert,
	removeAlert,
	notify,
	updateNotification,
	updateNotifications,
};