import config from 'config';
import { CronJob } from 'cron';

import { HttpError } from './errors.js';
import { knexOwner as knex } from './knex.js';
import initLogger from './logger.js';
import { indexApi } from './manticore.js';
import slugify from './utils/slugify.js';

const logger = initLogger();

const lastViewRefresh = {
	actors: 0,
	stashes: 0,
};

export function curateStash(stash, assets = {}) {
	if (!stash) {
		return null;
	}

	const curatedStash = {
		id: stash.id,
		name: stash.name,
		slug: stash.slug,
		isPrimary: stash.primary,
		isPublic: stash.public,
		isArchived: stash.archived,
		createdAt: stash.created_at,
		stashedScenes: stash.stashed_scenes ?? null,
		stashedMovies: stash.stashed_movies ?? null,
		stashedActors: stash.stashed_actors ?? null,
		lastStashAt: stash.last_stash_at,
		user: assets.user
			? {
					id: assets.user.id,
					username: assets.user.username,
					avatar: `/media/avatars/${assets.user.id}_${assets.user.username}.png`,
					createdAt: assets.user.created_at,
				}
			: null,
		comment: stash.comment,
		meta: stash.meta,
	};

	return curatedStash;
}

function curateStashed(stashed) {
	if (!stashed) {
		return null;
	}

	const curatedStashed = {
		id: stashed.id,
		stashId: stashed.stash_id,
		actorId: stashed.actor_id,
		createdAt: stashed.created_at,
	};

	return curatedStashed;
}

function curateStashEntry(stash, user) {
	const curatedStashEntry = {
		user_id: user?.id || undefined,
		name: stash.name || undefined,
		slug: slugify(stash.name) || undefined,
		public: stash.isPublic ?? undefined,
		archived: stash.isArchived ?? undefined,
		comment: stash.comment,
		meta: stash.meta,
	};

	return curatedStashEntry;
}

function verifyStashAccess(stash, sessionUser) {
	if (!stash || (!stash.public && stash.user_id !== sessionUser?.id)) {
		throw new HttpError('This stash does not exist, or you are not allowed access.', 404);
	}
}

export async function fetchStashById(stashIdOrSlug, sessionUser) {
	const stash = await knex('stashes')
		.where((builder) => {
			if (typeof stashIdOrSlug === 'number') {
				builder.where('id', stashIdOrSlug);
			}
			else {
				builder
					.where('slug', stashIdOrSlug)
					.where('user_id', sessionUser.id);
			}
		})
		.first();

	verifyStashAccess(stash, sessionUser);

	return curateStash(stash);
}

export async function fetchStashByUsernameAndSlug(usernameOrId, stashSlug, sessionUser) {
	const user = await knex('users')
		.where((builder) => {
			if (typeof usernameOrId === 'number') {
				builder.where('id', usernameOrId);
			}
			else {
				builder.where('username', usernameOrId);
			}
		})
		.first();

	if (!user) {
		throw new HttpError('This user does not exist.', 404);
	}

	const stash = await knex('stashes')
		.select('stashes.*', 'stashes_meta.*')
		.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
		.where('slug', stashSlug)
		.where('user_id', user.id)
		.first();

	verifyStashAccess(stash, sessionUser);

	return curateStash(stash, { user });
}

export async function fetchUserStashes(usernameOrId, reqUser) {
	if (!usernameOrId) {
		throw new HttpError(`No username or user ID provided`, 400);
	}

	const userId = typeof usernameOrId === 'number'
		? usernameOrId
		: await knex('users')
				.where('username', usernameOrId)
				.first()
				.then((user) => user?.id);

	if (!userId) {
		throw new HttpError(`Could not find user '${usernameOrId}'`, 404);
	}

	const stashes = await knex('stashes')
		.select('stashes.*', 'stashes_meta.*')
		.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
		.leftJoin('users', 'users.id', 'stashes.user_id')
		.where('user_id', userId)
		.modify((builder) => {
			if (userId !== reqUser?.id) {
				builder.where('public', true);
			}
		})
		.orderBy([
			{ column: 'stashes.primary', order: 'desc' },
			{ column: 'stashes_meta.last_stash_at', order: 'desc' },
			{ column: 'stashes.name', order: 'asc' },
		]);

	return stashes.map((stash) => curateStash(stash, { user: reqUser }));
}

export async function fetchDomainStashes(domain, itemId, sessionUser) {
	const stashes = await knex(`stashes_${domain}s`)
		.select('stashes.*')
		.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`)
		.where({
			[`${domain}_id`]: itemId,
			user_id: sessionUser.id,
		});

	return stashes.map((stash) => curateStash(stash));
}

function verifyStashName(stash) {
	if (!stash.name) {
		throw new HttpError('Stash name required', 400);
	}

	if (stash.name.length < config.stashes.nameLength[0]) {
		throw new HttpError('Stash name is too short', 400);
	}

	if (stash.name.length > config.stashes.nameLength[1]) {
		throw new HttpError('Stash name is too long', 400);
	}

	if (!config.stashes.namePattern.test(stash.name)) {
		throw new HttpError('Stash name contains invalid characters', 400);
	}
}

export async function refreshView(domain = 'stashes') {
	// throttle view refreshes
	if (new Date() - lastViewRefresh[domain] > config.stashes.viewRefreshCooldowns[domain] * 60000) {
		lastViewRefresh[domain] = new Date();

		logger.verbose(`Refreshing ${domain} view`);

		await knex.schema.refreshMaterializedView(`${domain}_meta`);
		await knex.schema.refreshMaterializedView('stashes_meta');

		return true;
	}

	logger.debug(`Skipping ${domain} view refresh`);

	return false;
}

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

	if (!newStash) {
		throw new HttpError('Missing new stash', 400);
	}

	verifyStashName(newStash);

	try {
		const [stash] = await knex('stashes')
			.insert(curateStashEntry(newStash, sessionUser))
			.returning('*');

		const curatedStash = curateStash(stash, { user: sessionUser });

		return curatedStash;
	}
	catch (error) {
		if (error.routine === '_bt_check_unique') {
			throw new HttpError('Stash name should be unique', 409);
		}

		throw error;
	}
}

export async function updateStash(stashIdOrSlug, updatedStash, sessionUser) {
	if (!sessionUser) {
		throw new HttpError('You are not authenthicated', 401);
	}

	if (!stashIdOrSlug) {
		throw new HttpError('Missing stash ID or slug', 400);
	}

	if (!updatedStash) {
		throw new HttpError('Missing updated stash', 400);
	}

	if (updatedStash.name) {
		verifyStashName(updatedStash);
	}

	try {
		const [stash] = await knex('stashes')
			.where((builder) => {
				if (typeof stashIdOrSlug === 'number') {
					builder.where('id', stashIdOrSlug);
				}
				else {
					builder.where('slug', stashIdOrSlug);
				}
			})
			.where('user_id', sessionUser.id)
			.update(curateStashEntry(updatedStash))
			.returning('*');

		if (!stash) {
			throw new HttpError('You are not authorized to modify this stash', 403);
		}

		return curateStash(stash);
	}
	catch (error) {
		if (error.routine === '_bt_check_unique') {
			throw new HttpError('Stash name should be unique', 409);
		}

		throw error;
	}
}

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

	if (!stashId) {
		throw new HttpError('Missing stash ID', 400);
	}

	const stash = await fetchStashById(stashId, sessionUser);

	if (!stash) {
		throw new HttpError(`Could not find stash '${stashId}'`, 404);
	}

	const [removed] = await knex('stashes')
		.where({
			id: stash.id,
			user_id: sessionUser.id,
			primary: false,
		})
		.delete()
		.returning('*');

	if (!removed) {
		throw new HttpError('This stash could not be removed', 409);
	}

	return curateStash(removed);
}

export async function stashActor(actorId, stashId, sessionUser) {
	if (!actorId) {
		throw new HttpError('Missing actor ID', 400);
	}

	if (!stashId) {
		throw new HttpError('Missing stash ID', 400);
	}

	const stash = await fetchStashById(stashId, sessionUser);

	if (!stash) {
		throw new HttpError(`Could not find stash '${stashId}'`, 404);
	}

	try {
		const [stashed] = await knex('stashes_actors')
			.insert({
				stash_id: stash.id,
				actor_id: actorId,
			})
			.returning('*');

		await indexApi.replace({
			table: 'actors_stashed',
			id: stashed.id,
			doc: {
				actor_id: actorId,
				user_id: sessionUser.id,
				stash_id: stash.id,
				created_at: Math.round(stashed.created_at.getTime() / 1000),
			},
		});

		logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed actor ${actorId} to stash ${stash.id} (${stash.name})`);

		refreshView('actors');

		// return fetchDomainStashes('actor', actorId, sessionUser);
		return curateStashed(stashed);
	}
	catch (error) {
		if (error.routine === '_bt_check_unique') {
			throw new HttpError(`Actor ${actorId} is already stashed in '${stash.name}'`, 409);
		}

		throw error;
	}
}

export async function unstashActor(actorId, stashId, sessionUser) {
	if (!actorId) {
		throw new HttpError('Missing actor ID', 400);
	}

	if (!stashId) {
		throw new HttpError('Missing stash ID', 400);
	}

	const stash = await fetchStashById(stashId, sessionUser);

	if (!stash) {
		throw new HttpError(`Could not find stash '${stashId}'`, 404);
	}

	const [unstashed] = await knex
		.from('stashes_actors AS deletable')
		.where('deletable.actor_id', actorId)
		.where('deletable.stash_id', stash.id)
		.whereExists(knex('stashes_actors') // verify user owns this stash, complimentary to row-level security
			.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
			.where('stashes_actors.stash_id', knex.raw('deletable.stash_id'))
			.where('stashes.user_id', sessionUser.id))
		.delete()
		.returning('*');

	try {
		await indexApi.callDelete({
			table: 'actors_stashed',
			query: {
				bool: {
					must: [
						{ equals: { actor_id: actorId } },
						{ equals: { stash_id: stash.id } },
						{ equals: { user_id: sessionUser.id } },
					],
				},
			},
		});
	}
	catch (error) {
		console.log(error);
	}

	logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId} (${stash.name})`);

	refreshView('actors');

	// return fetchDomainStashes('actor', actorId, sessionUser);
	return curateStashed(unstashed);
}

export async function stashScene(sceneId, stashId, sessionUser) {
	if (!sceneId) {
		throw new HttpError('Missing scene ID', 400);
	}

	if (!stashId) {
		throw new HttpError('Missing stash ID', 400);
	}

	const stash = await fetchStashById(stashId, sessionUser);

	if (!stash) {
		throw new HttpError(`Could not find stash '${stashId}'`, 404);
	}

	try {
		const [stashed] = await knex('stashes_scenes')
			.insert({
				stash_id: stash.id,
				scene_id: sceneId,
			})
			.returning('*');

		await indexApi.replace({
			table: 'scenes_stashed',
			id: stashed.id,
			doc: {
				// ...doc.replace.doc,
				scene_id: sceneId,
				user_id: sessionUser.id,
				stash_id: stash.id,
				created_at: Math.round(stashed.created_at.getTime() / 1000),
			},
		});

		logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed scene ${sceneId} to stash ${stash.id} (${stash.name})`);

		refreshView('scenes');

		// return fetchDomainStashes('scene', sceneId, sessionUser);
		return curateStashed(stashed);
	}
	catch (error) {
		if (error.routine === '_bt_check_unique') {
			throw new HttpError(`Scene ${sceneId} is already stashed in '${stash.name}'`, 409);
		}

		throw error;
	}
}

export async function unstashScene(sceneId, stashId, sessionUser) {
	if (!sceneId) {
		throw new HttpError('Missing scene ID', 400);
	}

	if (!stashId) {
		throw new HttpError('Missing stash ID', 400);
	}

	const stash = await fetchStashById(stashId, sessionUser);

	if (!stash) {
		throw new HttpError(`Could not find stash '${stashId}'`, 404);
	}

	const [unstashed] = await knex
		.from('stashes_scenes AS deletable')
		.where('deletable.scene_id', sceneId)
		.where('deletable.stash_id', stash.id)
		.whereExists(knex('stashes_scenes') // verify user owns this stash, complimentary to row-level security
			.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
			.where('stashes_scenes.stash_id', knex.raw('deletable.stash_id'))
			.where('stashes.user_id', sessionUser.id))
		.delete()
		.returning('*');

	await indexApi.callDelete({
		table: 'scenes_stashed',
		query: {
			bool: {
				must: [
					{ equals: { scene_id: sceneId } },
					{ equals: { stash_id: stash.id } },
					{ equals: { user_id: sessionUser.id } },
				],
			},
		},
	});

	logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stash.id} (${stash.name})`);

	refreshView('scenes');

	// return fetchDomainStashes('scene', sceneId, sessionUser);
	return curateStashed(unstashed);
}

export async function stashMovie(movieId, stashId, sessionUser) {
	if (!movieId) {
		throw new HttpError('Missing movie ID', 400);
	}

	if (!stashId) {
		throw new HttpError('Missing stash ID', 400);
	}

	const stash = await fetchStashById(stashId, sessionUser);

	if (!stash) {
		throw new HttpError(`Could not find stash '${stashId}'`, 404);
	}

	try {
		const [stashed] = await knex('stashes_movies')
			.insert({
				stash_id: stash.id,
				movie_id: movieId,
			})
			.returning('*');

		await indexApi.replace({
			table: 'movies_stashed',
			id: stashed.id,
			doc: {
				movie_id: movieId,
				user_id: sessionUser.id,
				stash_id: stash.id,
				created_at: Math.round(stashed.created_at.getTime() / 1000),
			},
		});

		logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed movie ${movieId} to stash ${stash.id} (${stash.name})`);

		refreshView('movies');

		// return fetchDomainStashes('movie', movieId, sessionUser);
		return curateStashed(stashed);
	}
	catch (error) {
		if (error.routine === '_bt_check_unique') {
			throw new HttpError(`Movie ${movieId} is already stashed in '${stash.name}'`, 409);
		}

		throw error;
	}
}

export async function unstashMovie(movieId, stashId, sessionUser) {
	if (!movieId) {
		throw new HttpError('Missing movie ID', 400);
	}

	if (!stashId) {
		throw new HttpError('Missing stash ID', 400);
	}

	const stash = await fetchStashById(stashId, sessionUser);

	if (!stash) {
		throw new HttpError(`Could not find stash '${stashId}'`, 404);
	}

	const [unstashed] = await knex
		.from('stashes_movies AS deletable')
		.where('deletable.movie_id', movieId)
		.where('deletable.stash_id', stash.id)
		.whereExists(knex('stashes_movies') // verify user owns this stash, complimentary to row-level security
			.leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id')
			.where('stashes_movies.stash_id', knex.raw('deletable.stash_id'))
			.where('stashes.user_id', sessionUser.id))
		.returning('*')
		.delete();

	await indexApi.callDelete({
		table: 'movies_stashed',
		query: {
			bool: {
				must: [
					{ equals: { movie_id: movieId } },
					{ equals: { stash_id: stash.id } },
					{ equals: { user_id: sessionUser.id } },
				],
			},
		},
	});

	logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed movie ${movieId} from stash ${stash.id} (${stash.name})`);

	refreshView('movies');

	// return fetchDomainStashes('movie', movieId, sessionUser);
	return curateStashed(unstashed);
}

export function initStashViewsCron() {
	CronJob.from({
		cronTime: config.stashes.viewRefreshCrontab,
		async onTick() {
			logger.verbose('Updating stash views');

			await refreshView('scenes');
			await refreshView('actors');
			await refreshView('movies');
			await refreshView('stashes');
		},
		start: true,
		runOnInit: true,
	});
}
