import config from 'config';
import { differenceInYears } from 'date-fns';
import { unit } from 'mathjs';

import knex from './knex.js';
import { searchApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchCountriesByAlpha2 } from './countries.js';

export function curateActor(actor, context = {}) {
	return {
		id: actor.id,
		slug: actor.slug,
		name: actor.name,
		gender: actor.gender,
		age: actor.age,
		dateOfBirth: actor.date_of_birth,
		ageFromBirth: actor.date_of_birth && differenceInYears(Date.now(), actor.date_of_birth),
		ageThen: context.sceneDate && actor.date_of_birth && differenceInYears(context.sceneDate, actor.date_of_birth),
		bust: actor.bust,
		cup: actor.cup,
		waist: actor.waist,
		hip: actor.hip,
		naturalBoobs: actor.naturalBoobs,
		height: actor.height && {
			metric: actor.height,
			imperial: unit(actor.height, 'cm').splitUnit(['ft', 'in']).map((value) => Math.round(value.toNumber())),
		},
		weight: actor.weight && {
			metric: actor.weight,
			imperial: Math.round(unit(actor.weight, 'kg').toNumeric('lbs')),
		},
		eyes: actor.eyes,
		hairColor: actor.hairColor,
		hasTattoos: actor.has_tattoos,
		tattoos: actor.tattoos,
		hasPiercings: actor.has_piercings,
		piercings: actor.piercings,
		origin: actor.birth_country_alpha2 && {
			country: actor.birth_country_alpha2 && {
				alpha2: actor.birth_country_alpha2,
				name: actor.birth_country_name,
			},
		},
		residence: actor.residence_country_alpha2 && {
			country: actor.residence_country_alpha2 && {
				alpha2: actor.residence_country_alpha2,
				name: actor.residence_country_name,
			},
		},
		avatar: actor.avatar && {
			id: actor.avatar.id,
			path: actor.avatar.path,
			thumbnail: actor.avatar.thumbnail,
			lazy: actor.avatar.lazy,
			isS3: actor.avatar.is_s3,
		},
		createdAt: actor.created_at,
		updatedAt: actor.updated_at,
		likes: actor.stashed,
		...context.append?.[actor.id],
	};
}

export function sortActorsByGender(actors) {
	if (!actors) {
		return actors;
	}

	const alphaActors = actors.sort((actorA, actorB) => actorA.name.localeCompare(actorB.name, 'en'));
	const genderActors = ['transsexual', 'female', 'male', undefined, null].flatMap((gender) => alphaActors.filter((actor) => actor.gender === gender));

	return genderActors;
}

export async function fetchActorsById(actorIds, options = {}) {
	const [actors] = await Promise.all([
		knex('actors')
			.select(
				'actors.*',
				'actors_meta.*',
				'birth_countries.alpha2 as birth_country_alpha2',
				knex.raw('COALESCE(birth_countries.alias, birth_countries.name) as birth_country_name'),
				'residence_countries.alpha2 as residence_country_alpha2',
				knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'),
			)
			.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id')
			.leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors.birth_country_alpha2')
			.leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors.residence_country_alpha2')
			.whereIn('actors.id', actorIds)
			.modify((builder) => {
				if (options.order) {
					builder.orderBy(...options.order);
				}
			}),
	]);

	if (options.order) {
		return actors.map((actorEntry) => curateActor(actorEntry, { append: options.append }));
	}

	const curatedActors = actorIds.map((actorId) => {
		const actor = actors.find((actorEntry) => actorEntry.id === actorId);

		if (!actor) {
			console.warn(`Can't match actor ${actorId}`);
			return null;
		}

		return curateActor(actor, { append: options.append });
	}).filter(Boolean);

	return curatedActors;
}

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

	return {
		page: options?.page || 1,
		limit: options?.limit || 30,
		requireAvatar: options?.requireAvatar || false,
		order: [options.order?.[0] || 'name', options.order?.[1] || 'asc'],
	};
}

function buildQuery(filters) {
	const query = {
		bool: {
			must: [],
		},
	};

	const expressions = {
		age: 'if(date_of_birth, floor((now() - date_of_birth) / 31556952), 0)',
	};

	if (filters.query) {
		query.bool.must.push({
			match: {
				name: filters.query,
			},
		});
	}

	['gender', 'country'].forEach((attribute) => {
		if (filters[attribute]) {
			query.bool.must.push({
				equals: {
					[attribute]: filters[attribute],
				},
			});
		}
	});

	['age', 'height', 'weight'].forEach((attribute) => {
		if (filters[attribute]) {
			query.bool.must.push({
				range: {
					[attribute]: {
						gte: filters[attribute][0],
						lte: filters[attribute][1],
					},
				},
			});
		}
	});

	if (filters.dateOfBirth && filters.dobType === 'dateOfBirth') {
		query.bool.must.push({
			equals: {
				date_of_birth: Math.floor(filters.dateOfBirth.getTime() / 1000),
			},
		});
	}

	if (filters.dateOfBirth && filters.dobType === 'birthday') {
		expressions.month_of_birth = 'month(date_of_birth)';
		expressions.day_of_birth = 'day(date_of_birth)';

		const month = filters.dateOfBirth.getMonth() + 1;
		const day = filters.dateOfBirth.getDate();

		query.bool.must.push({
			bool: {
				must: [
					{
						equals: {
							month_of_birth: month,
						},
					},
					{
						equals: {
							day_of_birth: day,
						},
					},
				],
			},
		});
	}

	if (filters.cup) {
		expressions.cup_in_range = `regex(cup, '^[${filters.cup[0]}-${filters.cup[1]}]')`;

		query.bool.must.push({
			equals: {
				cup_in_range: 1,
			},
		});
	}

	if (typeof filters.naturalBoobs === 'boolean') {
		query.bool.must.push({
			equals: {
				natural_boobs: filters.naturalBoobs ? 2 : 1, // manticore boolean does not support null, so 0 = null, 1 = false (enhanced), 2 = true (natural)
			},
		});
	}

	if (filters.requireAvatar) {
		query.bool.must.push({
			equals: {
				has_avatar: 1,
			},
		});
	}

	return { query, expressions };
}

const sortMap = {
	likes: 'stashed',
	scenes: 'scenes',
	relevance: '_score',
};

function getSort(order) {
	if (order[0] === 'name') {
		return [{
			slug: order[1],
		}];
	}

	return [
		{
			[sortMap[order[0]]]: order[1],
		},
		{
			slug: 'asc', // sort by name where primary order is equal
		},
	];
}

export async function fetchActors(filters, rawOptions) {
	const options = curateOptions(rawOptions);
	const { query, expressions } = buildQuery(filters);

	const result = await searchApi.search({
		index: 'actors',
		query,
		expressions,
		limit: options.limit,
		offset: (options.page - 1) * options.limit,
		sort: getSort(options.order, filters),
		aggs: {
			countries: {
				terms: {
					field: 'country',
					size: 300,
				},
				sort: [{ country: { order: 'asc' } }],
			},
		},
		options: {
			max_matches: config.database.manticore.maxMatches,
			max_query_time: config.database.manticore.maxQueryTime,
		},
	});

	const actorIds = result.hits.hits.map((hit) => Number(hit._id));
	const [actors, countries] = await Promise.all([
		fetchActorsById(actorIds),
		fetchCountriesByAlpha2(result.aggregations.countries.buckets.map((bucket) => bucket.key)),
	]);

	return {
		actors,
		countries,
		total: result.hits.total,
		limit: options.limit,
	};
}
