import process from 'node:process';
import config from 'config';

import escape from '../utils/escape-manticore.js';
import promiseProps from '../utils/promise-props.js';
import { curateActor, fetchActorsById, sortActorsByGender } from './actors.js';
import { censor } from './censor.js';
import { fetchEntitiesById } from './entities.js';
import { HttpError } from './errors.js';
import { knexQuery as knex, knexManticore, knexOwner } from './knex.js';
import { querySql } from './manticore.js';
import { curateMedia } from './media.js';
import { curateStash } from './stashes.js';
import { fetchTagsById } from './tags.js';

function curateMovie(rawMovie, assets, context = {}) {
	if (!rawMovie) {
		return null;
	}

	return {
		id: rawMovie.id,
		title: censor(rawMovie.title, context.restriction),
		slug: rawMovie.slug,
		url: rawMovie.url,
		date: rawMovie.date,
		datePrecision: rawMovie.date_precision,
		createdAt: rawMovie.created_at,
		effectiveDate: rawMovie.effective_date,
		description: censor(rawMovie.description, context.restriction),
		duration: rawMovie.duration,
		channel: {
			id: assets.channel.id,
			slug: assets.channel.slug,
			name: censor(assets.channel.name, context.restriction),
			type: assets.channel.type,
			isIndependent: assets.channel.independent,
			hasLogo: assets.channel.has_logo,
		},
		network: assets.channel.network_id
			? {
					id: assets.channel.network_id,
					slug: assets.channel.network_slug,
					name: censor(assets.channel.network_name, context.restriction),
					type: assets.channel.network_type,
					hasLogo: assets.channel.has_logo,
				}
			: null,
		actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, { sceneDate: rawMovie.effective_date }))),
		directors: assets.directors.map((director) => ({
			id: director.id,
			slug: director.slug,
			name: director.name,
		})),
		tags: assets.tags.map((tag) => ({
			id: tag.id,
			slug: tag.slug,
			name: censor(tag.name, context.restriction),
		})),
		// poster: curateMedia(assets.poster),
		covers: assets.covers.map((cover) => curateMedia(cover, { type: 'cover' })),
		photos: assets.photos.map((photo) => curateMedia(photo, { type: 'photo' })),
		caps: assets.caps.map((cap) => curateMedia(cap, { type: 'cap' })),
		trailer: curateMedia(assets.trailer, { type: 'trailer' }),
		stashes: assets.stashes?.map((stash) => curateStash(stash)) || [],
		createdBatchId: rawMovie.created_batch_id,
		updatedBatchId: rawMovie.updated_batch_id,
	};
}

export async function fetchMoviesById(movieIds, reqUser, context = {}) {
	const {
		movies,
		channels,
		actors,
		directors,
		tags,
		sceneTags,
		covers,
		photos,
		caps,
		trailers,
		stashes,
	} = await promiseProps({
		movies: knex('movies')
			.select('movies.*', knex.raw('cast(sum(releases.duration) as integer) as duration'))
			.leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id')
			.leftJoin('releases', 'releases.id', 'movies_scenes.scene_id')
			.whereIn('movies.id', movieIds)
			.groupBy('movies.id'),
		channels: knex('movies')
			.select('channels.*', 'networks.id as network_id', 'networks.slug as network_slug', 'networks.name as network_name', 'networks.type as network_type')
			.whereIn('movies.id', movieIds)
			.leftJoin('entities as channels', 'channels.id', 'movies.entity_id')
			.leftJoin('entities as networks', 'networks.id', 'channels.parent_id')
			.groupBy('channels.id', 'networks.id'),
		actors: knex('movies')
			.select(
				'actors.*',
				'actors_meta.*',
				'releases_actors.release_id',
				'movies.id as movie_id',
			)
			.distinctOn('actors.id', 'movies.id') // cannot distinct on JSON column avatar, must specify
			.whereIn('movies.id', movieIds)
			.whereNotNull('actors.id')
			.leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id')
			.leftJoin('releases_actors', 'releases_actors.release_id', 'movies_scenes.scene_id')
			.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
			.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id'),
		directors: knex('movies')
			.whereIn('movies.id', movieIds)
			.leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id')
			.leftJoin('releases_directors', 'releases_directors.release_id', 'movies_scenes.scene_id')
			.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
		tags: knex('movies_tags')
			.whereIn('movie_id', movieIds)
			.whereNotNull('tags.id')
			.leftJoin('tags', 'tags.id', 'movies_tags.tag_id'),
		sceneTags: knex('movies')
			.select('tags.id', 'tags.slug', 'tags.name', 'tags.priority', 'movies.id as movie_id')
			.distinct()
			.whereIn('movies.id', movieIds)
			.whereNotNull('tags.id')
			.leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id')
			.leftJoin('releases_tags', 'releases_tags.release_id', 'movies_scenes.scene_id')
			.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
			.orderBy('priority', 'desc'),
		covers: knex('movies_covers')
			.select('media.*', 'movies_covers.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
			.whereIn('movie_id', movieIds)
			.leftJoin('media', 'media.id', 'movies_covers.media_id')
			.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
			.orderBy('media.index')
			.groupBy('media.id', 'movies_covers.movie_id', 'sfw_media.id'),
		photos: context.restriction
			? []
			: knex.transaction(async (trx) => {
					if (reqUser) {
						await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
					}

					return trx('movies_scenes')
						.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
						.whereIn('movies_scenes.movie_id', movieIds)
						.whereNotNull('media.id')
						.leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_id')
						.leftJoin('media', 'media.id', 'releases_photos.media_id')
						.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
						.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
				}),
		caps: knex.transaction(async (trx) => {
			if (reqUser) {
				await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
			}

			return trx('movies_scenes')
				.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
				.whereIn('movies_scenes.movie_id', movieIds)
				.whereNotNull('media.id')
				.leftJoin('releases_caps', 'releases_caps.release_id', 'movies_scenes.scene_id')
				.leftJoin('media', 'media.id', 'releases_caps.media_id')
				.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
				.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
		}),
		trailers: knex('movies_trailers')
			.whereIn('movie_id', movieIds)
			.leftJoin('media', 'media.id', 'movies_trailers.media_id'),
		stashes: reqUser
			? knexOwner('stashes_movies')
					.leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id')
					.where('stashes.user_id', reqUser.id)
					.whereIn('stashes_movies.movie_id', movieIds)
			: [],
	});

	return movieIds.map((movieId) => {
		const movie = movies.find((movieEntry) => movieEntry.id === movieId);

		if (!movie) {
			console.warn('Cannot find movie', movieId);
			return null;
		}

		const movieChannel = channels.find((entity) => entity.id === movie.entity_id);
		const movieActors = actors.filter((actor) => actor.movie_id === movieId);
		const movieDirectors = directors.filter((director) => director.release_id === movieId);
		const movieTags = tags.filter((tag) => tag.movie_id === movieId);
		const movieSceneTags = sceneTags.filter((tag) => tag.movie_id === movieId);
		const movieCovers = covers.filter((cover) => cover.movie_id === movieId);
		const moviePhotos = photos.filter((photo) => photo.movie_id === movieId);
		const movieCaps = caps.filter((cap) => cap.movie_id === movieId);
		const movieTrailer = trailers.find((trailer) => trailer.movie_id === movieId);
		const movieStashes = stashes.filter((stash) => stash.movie_id === movieId);

		const combinedTags = Object.values(Object.fromEntries(movieTags.concat(movieSceneTags).map((tag) => [tag.slug, tag]))).toSorted((tagA, tagB) => tagB.priority - tagA.priority);

		return curateMovie(movie, {
			channel: movieChannel,
			actors: movieActors,
			directors: movieDirectors,
			tags: combinedTags,
			covers: movieCovers,
			photos: moviePhotos,
			caps: movieCaps,
			trailer: movieTrailer,
			stashes: movieStashes,
		}, context);
	}).filter(Boolean);
}

function curateOptions(options) {
	if (options?.limit > 100) {
		throw new HttpError('Limit must be <= 100', 400);
	}

	return {
		limit: options?.limit || 30,
		page: Number(options?.page) || 1,
		aggregate: options?.aggregate ?? true,
		aggregateYears: (options?.aggregate ?? true) && (options?.aggregateYears ?? true),
		aggregateActors: (options?.aggregate ?? true) && (options?.aggregateActors ?? true),
		aggregateTags: (options?.aggregate ?? true) && (options?.aggregateTags ?? true),
		aggregateChannels: (options?.aggregate ?? true) && (options?.aggregateChannels ?? true),
		dedupe: !!options?.dedupe,
	};
}

async function queryManticoreSql(filters, options) {
	const aggSize = config.database.manticore.maxAggregateSize;

	const sqlQuery = knexManticore.raw(`
		:query:
		OPTION
			field_weights=(
				title_filtered=7,
				actors=10,
				tags=9,
				meta=6,
				channel_name=2,
				channel_slug=3,
				network_name=1,
				network_slug=1
			),
			max_matches=:maxMatches:,
			max_query_time=:maxQueryTime:
		:yearsFacet:
		:actorsFacet:
		:tagsFacet:
		:channelsFacet:;
		show meta;
	`, {
		query: knexManticore(filters.stashId ? 'movies_stashed' : 'movies')
			.modify((builder) => {
				if (filters.stashId) {
					builder.select(knex.raw(`
						movies.id as id,
						movies.title as title,
						movies.actor_ids as actor_ids,
						movies.entity_ids as entity_ids,
						movies.tag_ids as tag_ids,
						movies.channel_id as channel_id,
						movies.network_id as network_id,
						movies.effective_date as effective_date,
						movies.stashed as stashed,
						movies.created_at,
						created_at as stashed_at,
						year(movies.effective_date) as effective_year,
						weight() as _score
					`));

					builder
						.innerJoin('movies', 'movies.id', 'movies_stashed.movie_id')
						.where('stash_id', filters.stashId);
				}
				else {
					builder.select(knex.raw(`
						*,
						year(movies.effective_date) as effective_year,
						weight() as _score
					`));
				}

				if (filters.query) {
					builder.whereRaw('match(\'@!title :query:\', movies)', { query: escape(filters.query) });
				}

				if (filters.years?.length > 0) {
					builder.whereIn('effective_year', filters.years);
				}

				filters.tagIds?.forEach((tagId) => {
					builder.where('any(tag_ids)', tagId);
				});

				filters.actorIds?.forEach((actorId) => {
					builder.where('any(actor_ids)', actorId);
				});

				if (filters.entityId) {
					builder.whereRaw('any(entity_ids) = ?', filters.entityId);
				}

				if (filters.requireCover) {
					builder.where('has_cover', true);
				}

				if (typeof filters.isShowcased === 'boolean') {
					builder.where('is_showcased', filters.isShowcased);
				}

				if (options.dedupe) {
					builder.where('movies.dupe_index', '<', 2);
				}

				if (!filters.scope || filters.scope === 'latest') {
					builder
						.where('effective_date', '<=', Math.round(Date.now() / 1000))
						.orderBy('movies.effective_date', 'desc'); // can't seem to use alias if it matches column-name? behavior not fully understand, but this works
				}
				else if (filters.scope === 'upcoming') {
					builder
						.where('effective_date', '>', Math.round(Date.now() / 1000))
						.orderBy('movies.effective_date', 'asc');
				}
				else if (filters.scope === 'new') {
					builder.orderBy([
						{ column: 'movies.created_at', order: 'desc' },
						{ column: 'movies.effective_date', order: 'asc' },
					]);
				}
				else if (filters.scope === 'likes') {
					builder.orderBy([
						{ column: 'movies.stashed', order: 'desc' },
						{ column: 'movies.effective_date', order: 'desc' },
					]);
				}
				else if (filters.scope === 'results') {
					builder.orderBy([
						{ column: '_score', order: 'desc' },
						{ column: 'movies.stashed', order: 'desc' },
						{ column: 'movies.effective_date', order: 'desc' },
					]);
				}
				else if (filters.scope === 'stashed' && filters.stashId) {
					builder.orderBy([
						{ column: 'stashed_at', order: 'desc' },
						{ column: 'movies.effective_date', order: 'desc' },
					]);
				}
				else {
					builder.orderBy('movies.effective_date', 'desc');
				}
			})
			.limit(options.limit)
			.offset((options.page - 1) * options.limit)
			.toString(),
		// option threads=1 fixes actors, but drastically slows down performance, wait for fix
		yearsFacet: options.aggregateYears ? knex.raw('facet effective_year as years order by effective_year desc limit ?', [aggSize]) : null,
		actorsFacet: options.aggregateActors ? knex.raw('facet movies.actor_ids order by count(*) desc limit ?', [aggSize]) : null,
		tagsFacet: options.aggregateTags ? knex.raw('facet movies.tag_ids order by count(*) desc limit ?', [aggSize]) : null,
		channelsFacet: options.aggregateChannels ? knex.raw('facet movies.channel_id order by count(*) desc limit ?', [aggSize]) : null,
		maxMatches: config.database.manticore.maxMatches,
		maxQueryTime: config.database.manticore.maxQueryTime,
	}).toString();

	// manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
	const curatedSqlQuery = filters.stashId
		? sqlQuery
		: sqlQuery.replace(/movies\./g, '');

	if (process.env.NODE_ENV === 'development') {
		console.log(curatedSqlQuery);
	}

	const results = await querySql(curatedSqlQuery);

	// console.log(results[0]);
	const years = results
		.find((result) => (result.columns[0].years || result.columns[0]['movies.years']) && result.columns[1]['count(*)'])
		?.data
		.map((row) => ({ key: row.years || row['movies.years'], doc_count: row['count(*)'] }))
		|| [];

	const actorIds = results
		.find((result) => (result.columns[0].actor_ids || result.columns[0]['movies.actor_ids']) && result.columns[1]['count(*)'])
		?.data
		.map((row) => ({ key: row.actor_ids || row['movies.actor_ids'], doc_count: row['count(*)'] }))
		|| [];

	const tagIds = results
		.find((result) => (result.columns[0].tag_ids || result.columns[0]['movies.tag_ids']) && result.columns[1]['count(*)'])
		?.data
		.map((row) => ({ key: row.tag_ids || row['movies.tag_ids'], doc_count: row['count(*)'] }))
		|| [];

	const channelIds = results
		.find((result) => (result.columns[0].channel_id || result.columns[0]['movies.channel_id']) && result.columns[1]['count(*)'])
		?.data
		.map((row) => ({ key: row.channel_id || row['movies.channel_id'], doc_count: row['count(*)'] }))
		|| [];

	const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found')?.Value) || 0;

	return {
		movies: results[0].data,
		total,
		aggregations: {
			years,
			actorIds,
			tagIds,
			channelIds,
		},
	};
}

function countAggregations(buckets) {
	if (!buckets) {
		return null;
	}

	return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
}

export async function fetchMovies(filters, rawOptions, reqUser, context) {
	const options = curateOptions(rawOptions);

	console.log(options);
	console.log(filters);

	const result = await queryManticoreSql(filters, options);

	const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));

	const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds);
	const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
	const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);

	const [aggActors, aggTags, aggChannels] = await Promise.all([
		options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { shadllow: true, order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
		options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(tags.name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
		options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
	]);

	const movieIds = result.movies.map((movie) => Number(movie.id));
	const movies = await fetchMoviesById(movieIds, reqUser, context);

	return {
		movies,
		aggYears,
		aggActors,
		aggTags,
		aggChannels,
		total: result.total,
		limit: options.limit,
	};
}
