import { error, json } from '@sveltejs/kit';
import {
	RECOMMENDATION_PROFILES,
	rankRecommendationResults,
	normalizeLookupItem
} from '../../../lib/collection-tools.js';
import { fetchEmby, fetchEmbyJson, normalizeEmbyGuid } from '../../../lib/server/emby-api.js';
import {
	fetchTmdbCredits,
	fetchTmdbDetails,
	fetchTmdbDiscover,
	fetchTmdbGenres,
	fetchTmdbKeywords,
	fetchTmdbRecommendations,
	fetchTmdbSimilar,
	searchTmdbByTitle
} from '../../../lib/server/tmdb-api.js';

const EMBY_PAGE_SIZE = 200;

function normalizeTitle(value) {
	return String(value || '')
		.toLowerCase()
		.replace(/[^a-z0-9]+/g, '');
}

function normalizeYear(value) {
	const year = Number(value || 0);
	return Number.isFinite(year) && year > 0 ? year : null;
}

function titleYearKey(name, year) {
	return `${normalizeTitle(name)}::${normalizeYear(year) || ''}`;
}

function normalizeCollectionName(name) {
	return String(name || '')
		.toLowerCase()
		.replace(/['’]/g, '')
		.replace(/[^a-z0-9]+/g, '');
}

function getSeedTmdbId(item) {
	const providerIds = item?.providerIds || {};
	return String(providerIds.Tmdb || providerIds.TMDB || providerIds.tmdb || '').trim();
}

function getTmdbMediaType(item) {
	return item?.type === 'Series' ? 'tv' : 'movie';
}

function summarizeItems(items, limit = 5) {
	return (items || []).slice(0, limit).map((item) => ({
		name: item?.name || item?.title || item?.Name || '',
		year: item?.year || item?.ProductionYear || null,
		id: item?.id || item?.Id || '',
		type: item?.type || item?.Type || ''
	}));
}

function filterEnglishCandidates(items) {
	return (items || []).filter((item) => !item?.originalLanguage || item.originalLanguage === 'en');
}

async function fetchAllUserItems(userId, params) {
	const allItems = [];
	let startIndex = 0;

	while (true) {
		const payload = await fetchEmbyJson(`/Users/${encodeURIComponent(userId)}/Items`, {
			params: {
				Recursive: true,
				GroupItemsIntoCollections: false,
				Limit: EMBY_PAGE_SIZE,
				StartIndex: startIndex,
				...params
			}
		});

		const pageItems = (payload.Items || payload || []).map(normalizeLookupItem);
		allItems.push(...pageItems);

		const total = Number(payload.TotalRecordCount || 0);
		if (!pageItems.length) break;
		if (total > 0 && allItems.length >= total) break;
		if (pageItems.length < EMBY_PAGE_SIZE) break;

		startIndex += pageItems.length;
	}

	return allItems;
}

async function fetchAllLibraryItems(userId) {
	return fetchAllUserItems(userId, {
		IncludeItemTypes: 'Movie,Series',
		Fields: 'Overview,Genres,CommunityRating,ProductionYear,ProviderIds'
	});
}

async function fetchAllPlayedItems(userId) {
	return fetchAllUserItems(userId, {
		Filters: 'IsPlayed',
		IncludeItemTypes: 'Movie,Series',
		SortBy: 'DatePlayed',
		SortOrder: 'Descending',
		Fields: 'Overview,Genres,CommunityRating,ProductionYear,ProviderIds,UserData'
	});
}

function uniqueById(items) {
	const seen = new Set();
	const unique = [];
	for (const item of items || []) {
		if (!item?.id || seen.has(item.id)) continue;
		seen.add(item.id);
		unique.push(item);
	}
	return unique;
}

function buildLibraryIndex(items) {
	const byId = new Map();
	const byTmdbId = new Map();
	const byTitleYear = new Map();
	const byTitle = new Map();

	for (const item of items || []) {
		if (!item?.id) continue;
		byId.set(item.id, item);

		const tmdbId = getSeedTmdbId(item);
		if (tmdbId && !byTmdbId.has(tmdbId)) {
			byTmdbId.set(tmdbId, item);
		}

		const titleKey = titleYearKey(item.name, item.year);
		if (normalizeTitle(item.name) && !byTitleYear.has(titleKey)) {
			byTitleYear.set(titleKey, item);
		}

		const normalizedName = normalizeTitle(item.name);
		if (normalizedName && !byTitle.has(normalizedName)) {
			byTitle.set(normalizedName, item);
		}
	}

	return { byId, byTmdbId, byTitleYear, byTitle };
}

async function fetchSeedItems(userId, ids, libraryIndex) {
	const fromLibrary = ids
		.map((id) => libraryIndex.byId.get(String(id)))
		.filter(Boolean);

	if (fromLibrary.length === ids.length) {
		return fromLibrary;
	}

	const payload = await fetchEmbyJson(`/Users/${encodeURIComponent(userId)}/Items`, {
		params: {
			Ids: ids.join(','),
			Limit: ids.length,
			Fields: 'Overview,Genres,CommunityRating,ProductionYear,ProviderIds',
			GroupItemsIntoCollections: false
		}
	});

	return uniqueById((payload.Items || payload || []).map(normalizeLookupItem));
}

async function findExistingCollection(userId, name) {
	const trimmedName = String(name || '').trim();
	if (!userId || !trimmedName) return null;

	const payload = await fetchEmbyJson(`/Users/${encodeURIComponent(userId)}/Items`, {
		params: {
			Recursive: true,
			SearchTerm: trimmedName,
			Limit: 20,
			SortBy: 'SortName',
			SortOrder: 'Ascending',
			Fields: 'Overview',
			IncludeItemTypes: 'BoxSet',
			GroupItemsIntoCollections: false
		}
	});

	const normalizedTarget = normalizeCollectionName(trimmedName);
	const exact = (payload.Items || payload || [])
		.map(normalizeLookupItem)
		.find((item) => item.type === 'BoxSet' && normalizeCollectionName(item.name) === normalizedTarget);

	return exact || null;
}

async function resolveTmdbMatch(item) {
	const directTmdbId = getSeedTmdbId(item);
	if (directTmdbId) {
		return {
			tmdbId: Number(directTmdbId),
			mediaType: getTmdbMediaType(item)
		};
	}

	const matches = await searchTmdbByTitle({
		mediaType: getTmdbMediaType(item),
		name: item.name,
		year: item.year
	});
	const best = matches[0];
	if (!best?.tmdbId) return null;

	return {
		tmdbId: best.tmdbId,
		mediaType: best.mediaType
	};
}

function resolveLocalCandidate(index, candidate) {
	const tmdbId = String(candidate?.tmdbId || '').trim();
	if (tmdbId && index.byTmdbId.has(tmdbId)) {
		return index.byTmdbId.get(tmdbId);
	}

	const exactTitleYear = index.byTitleYear.get(titleYearKey(candidate?.name, candidate?.year));
	if (exactTitleYear) {
		return exactTitleYear;
	}

	return index.byTitle.get(normalizeTitle(candidate?.name)) || null;
}

function addWeight(map, key, amount = 1) {
	if (key === null || key === undefined || key === '') return;
	map.set(key, (map.get(key) || 0) + amount);
}

function sortWeightedEntries(map, limit) {
	return [...map.entries()]
		.sort((a, b) => b[1] - a[1])
		.slice(0, limit);
}

function getGenreLookup(genres) {
	return new Map((genres || []).map((genre) => [normalizeTitle(genre.name), Number(genre.id)]));
}

function resolveGenreIdsFromLocalItems(items, genreLookup, weight, targetMap) {
	for (const item of items || []) {
		for (const genreName of item.genres || []) {
			const id = genreLookup.get(normalizeTitle(genreName));
			if (id) addWeight(targetMap, id, weight);
		}
	}
}

function normalizeKeywordIds(keywords) {
	return (keywords || []).map((keyword) => Number(keyword?.id || 0)).filter(Boolean);
}

function extractPeopleIds(credits, mediaType) {
	const ids = new Set();

	for (const person of (credits?.cast || []).slice(0, 5)) {
		if (person?.id) ids.add(Number(person.id));
	}

	if (mediaType === 'movie') {
		for (const crew of credits?.crew || []) {
			if (!crew?.id) continue;
			if (crew.job === 'Director' || crew.job === 'Writer' || crew.job === 'Screenplay') {
				ids.add(Number(crew.id));
			}
			if (ids.size >= 8) break;
		}
	}

	return [...ids];
}

function buildDiscoverQueries({ mediaType, genreIds, keywordIds, peopleIds }) {
	const base = {
		page: 1,
		sort_by: 'popularity.desc',
		'vote_count.gte': mediaType === 'tv' ? 10 : 25,
		with_original_language: 'en'
	};
	const queries = [];

	if (genreIds.length) {
		queries.push({
			label: `${mediaType}-genres`,
			params: {
				...base,
				with_genres: genreIds.slice(0, 4).join('|')
			}
		});
	}

	if (keywordIds.length) {
		queries.push({
			label: `${mediaType}-keywords`,
			params: {
				...base,
				with_keywords: keywordIds.slice(0, 6).join('|')
			}
		});
	}

	if (genreIds.length && keywordIds.length) {
		queries.push({
			label: `${mediaType}-genres-keywords`,
			params: {
				...base,
				with_genres: genreIds.slice(0, 3).join('|'),
				with_keywords: keywordIds.slice(0, 4).join('|')
			}
		});
	}

	if (mediaType === 'movie' && peopleIds.length) {
		queries.push({
			label: 'movie-people',
			params: {
				...base,
				with_people: peopleIds.slice(0, 5).join('|')
			}
		});
	}

	return queries;
}

function inspectCandidateSets(seedIds, resultSets) {
	const excluded = new Set((seedIds || []).map((id) => String(id)));
	const uniqueIncluded = new Map();
	const uniqueExcluded = new Map();

	for (const resultSet of resultSets || []) {
		const items = Array.isArray(resultSet) ? resultSet : (Array.isArray(resultSet?.items) ? resultSet.items : []);
		for (const item of items) {
			const normalized = normalizeLookupItem(item);
			if (!normalized.id) continue;
			if (excluded.has(normalized.id)) {
				if (!uniqueExcluded.has(normalized.id)) uniqueExcluded.set(normalized.id, normalized);
				continue;
			}
			if (!uniqueIncluded.has(normalized.id)) uniqueIncluded.set(normalized.id, normalized);
		}
	}

	return {
		uniqueCandidateCount: uniqueIncluded.size,
		excludedSeedCandidateCount: uniqueExcluded.size,
		sampleExcludedSeedCandidates: summarizeItems([...uniqueExcluded.values()])
	};
}

async function buildSeedContext(seed, libraryIndex) {
	const diagnostic = {
		seedId: seed.id,
		seedName: seed.name,
		seedType: seed.type,
		seedYear: seed.year,
		providerTmdbId: getSeedTmdbId(seed) || null,
		tmdbMatch: null,
		similarCount: 0,
		recommendationCount: 0,
		localSimilarCount: 0,
		localRecommendationCount: 0,
		error: null
	};

	try {
		const tmdbMatch = await resolveTmdbMatch(seed);
		if (!tmdbMatch?.tmdbId) {
			diagnostic.error = 'No TMDB match resolved for seed';
			return { seed, diagnostic, resultSets: [], details: null, keywords: [], peopleIds: [] };
		}

		diagnostic.tmdbMatch = tmdbMatch;

		const [details, keywords, credits, similar, recommendations] = await Promise.all([
			fetchTmdbDetails(tmdbMatch),
			fetchTmdbKeywords(tmdbMatch),
			fetchTmdbCredits(tmdbMatch),
			fetchTmdbSimilar({ ...tmdbMatch, page: 1 }),
			fetchTmdbRecommendations({ ...tmdbMatch, page: 1 })
		]);

		const englishSimilar = filterEnglishCandidates(similar);
		const englishRecommendations = filterEnglishCandidates(recommendations);
		const localSimilar = uniqueById(englishSimilar.map((candidate) => resolveLocalCandidate(libraryIndex, candidate)).filter(Boolean));
		const localRecommendations = uniqueById(
			englishRecommendations.map((candidate) => resolveLocalCandidate(libraryIndex, candidate)).filter(Boolean)
		);

		diagnostic.similarCount = englishSimilar.length;
		diagnostic.recommendationCount = englishRecommendations.length;
		diagnostic.localSimilarCount = localSimilar.length;
		diagnostic.localRecommendationCount = localRecommendations.length;

		return {
			seed,
			details,
			keywords,
			peopleIds: extractPeopleIds(credits, tmdbMatch.mediaType),
			diagnostic,
			resultSets: [
				{ items: localSimilar, sourceWeight: 5, label: 'tmdb-similar' },
				{ items: localRecommendations, sourceWeight: 6, label: 'tmdb-recommendations' }
			]
		};
	} catch (err) {
		diagnostic.error = err.message;
		return { seed, diagnostic, resultSets: [], details: null, keywords: [], peopleIds: [] };
	}
}

async function buildPreview(userId, seedIds, limit, profile) {
	const normalizedSeedIds = [...new Set(seedIds.map((id) => String(id).trim()).filter(Boolean))];
	const libraryItems = uniqueById(await fetchAllLibraryItems(userId));
	const playedItems = uniqueById(await fetchAllPlayedItems(userId));
	const excludedIds = [...new Set([...normalizedSeedIds, ...playedItems.map((item) => item.id).filter(Boolean)])];
	const libraryIndex = buildLibraryIndex(libraryItems);
	const seeds = await fetchSeedItems(userId, normalizedSeedIds, libraryIndex);
	const allowedTypes = [...new Set(seeds.map((item) => item.type).filter(Boolean))];
	const includeMovies = allowedTypes.includes('Movie');
	const includeSeries = allowedTypes.includes('Series');

	if (!seeds.length) {
		return {
			seeds: [],
			recommendations: [],
			diagnostics: {
				libraryItemCount: libraryItems.length,
				watchedItemCount: playedItems.length,
				seedCount: 0,
				embyCandidates: 0,
				tmdbEnabled: true,
				tmdbCandidates: 0,
				tmdbResolved: 0,
				uniqueCandidateCount: 0,
				excludedSeedCandidateCount: 0,
				sampleExcludedSeedCandidates: [],
				perSeed: [],
				discoverQueries: []
			}
		};
	}

	const seedContexts = await Promise.all(seeds.map((seed) => buildSeedContext(seed, libraryIndex)));
	const movieGenres = await fetchTmdbGenres('movie');
	const tvGenres = await fetchTmdbGenres('tv');
	const movieGenreLookup = getGenreLookup(movieGenres);
	const tvGenreLookup = getGenreLookup(tvGenres);

	const movieGenreWeights = new Map();
	const tvGenreWeights = new Map();
	const movieKeywordWeights = new Map();
	const tvKeywordWeights = new Map();
	const moviePeopleWeights = new Map();

	for (const context of seedContexts) {
		const mediaType = getTmdbMediaType(context.seed);
		const genreTarget = mediaType === 'tv' ? tvGenreWeights : movieGenreWeights;
		const keywordTarget = mediaType === 'tv' ? tvKeywordWeights : movieKeywordWeights;

		for (const genre of context.details?.genres || []) {
			addWeight(genreTarget, Number(genre.id), 5);
		}

		for (const keywordId of normalizeKeywordIds(context.keywords)) {
			addWeight(keywordTarget, keywordId, 4);
		}

		if (mediaType === 'movie') {
			for (const personId of context.peopleIds || []) {
				addWeight(moviePeopleWeights, personId, 3);
			}
		}
	}

	resolveGenreIdsFromLocalItems(seeds.filter((item) => getTmdbMediaType(item) === 'movie'), movieGenreLookup, 3, movieGenreWeights);
	resolveGenreIdsFromLocalItems(seeds.filter((item) => getTmdbMediaType(item) === 'tv'), tvGenreLookup, 3, tvGenreWeights);
	resolveGenreIdsFromLocalItems(playedItems.filter((item) => getTmdbMediaType(item) === 'movie'), movieGenreLookup, 1, movieGenreWeights);
	resolveGenreIdsFromLocalItems(playedItems.filter((item) => getTmdbMediaType(item) === 'tv'), tvGenreLookup, 1, tvGenreWeights);

	const discoverQueries = [
		...(includeMovies
			? buildDiscoverQueries({
				mediaType: 'movie',
				genreIds: sortWeightedEntries(movieGenreWeights, 6).map(([id]) => id),
				keywordIds: sortWeightedEntries(movieKeywordWeights, 8).map(([id]) => id),
				peopleIds: sortWeightedEntries(moviePeopleWeights, 6).map(([id]) => id)
			})
			: []),
		...(includeSeries
			? buildDiscoverQueries({
				mediaType: 'tv',
				genreIds: sortWeightedEntries(tvGenreWeights, 6).map(([id]) => id),
				keywordIds: sortWeightedEntries(tvKeywordWeights, 8).map(([id]) => id),
				peopleIds: []
			})
			: [])
	];

	const queryResults = await Promise.all(
		discoverQueries.map(async (query) => {
			try {
				const tmdbResults = filterEnglishCandidates(await fetchTmdbDiscover({
					mediaType: query.label.startsWith('tv') ? 'tv' : 'movie',
					params: query.params
				}));
				const localMatches = uniqueById(
					tmdbResults.map((candidate) => resolveLocalCandidate(libraryIndex, candidate)).filter(Boolean)
				);

				return {
					label: query.label,
					params: query.params,
					tmdbCount: tmdbResults.length,
					localCount: localMatches.length,
					localMatches,
					sourceWeight: 1
				};
			} catch (err) {
				return {
					label: query.label,
					params: query.params,
					tmdbCount: 0,
					localCount: 0,
					localMatches: [],
					error: err.message
				};
			}
		})
	);

	const resultSets = [
		...seedContexts.flatMap((context) => context.resultSets),
		...queryResults.map((query) => ({
			items: query.localMatches,
			sourceWeight: query.sourceWeight || 1,
			label: query.label
		}))
	].filter((resultSet) => (Array.isArray(resultSet) ? resultSet.length : resultSet.items.length) > 0);

	const candidateInspection = inspectCandidateSets(excludedIds, resultSets);
	const recommendations = rankRecommendationResults(normalizedSeedIds, resultSets, {
		limit,
		profile,
		seeds,
		excludeIds: excludedIds,
		allowedTypes
	});

	return {
		seeds,
		recommendations,
		diagnostics: {
			libraryItemCount: libraryItems.length,
			watchedItemCount: playedItems.length,
			seedCount: seeds.length,
			embyCandidates: seedContexts.reduce(
				(total, context) => total + context.diagnostic.localSimilarCount + context.diagnostic.localRecommendationCount,
				0
			),
			tmdbEnabled: true,
			tmdbCandidates:
				seedContexts.reduce(
					(total, context) => total + context.diagnostic.similarCount + context.diagnostic.recommendationCount,
					0
				) + queryResults.reduce((total, query) => total + query.tmdbCount, 0),
			tmdbResolved:
				seedContexts.reduce(
					(total, context) => total + context.diagnostic.localSimilarCount + context.diagnostic.localRecommendationCount,
					0
				) + queryResults.reduce((total, query) => total + query.localCount, 0),
			uniqueCandidateCount: candidateInspection.uniqueCandidateCount,
			excludedSeedCandidateCount: candidateInspection.excludedSeedCandidateCount,
			sampleExcludedSeedCandidates: candidateInspection.sampleExcludedSeedCandidates,
			perSeed: seedContexts.map((context) => context.diagnostic),
			discoverQueries: queryResults.map((query) => ({
				label: query.label,
				tmdbCount: query.tmdbCount,
				localCount: query.localCount,
				error: query.error || null,
				params: query.params
			}))
		}
	};
}

async function createOrUpdateCollection(userId, name, itemIds) {
	const existingCollection = await findExistingCollection(userId, name);

	if (existingCollection?.id) {
		await fetchEmby(`/Collections/${encodeURIComponent(existingCollection.id)}/Items`, {
			method: 'POST',
			params: {
				Ids: itemIds.join(',')
			}
		});

		return {
			id: existingCollection.id,
			name: existingCollection.name || name,
			updated: true
		};
	}

	const response = await fetchEmby('/Collections', {
		method: 'POST',
		params: {
			Name: name,
			Ids: itemIds.join(',')
		}
	});
	const created = await response.json();

	return {
		id: String(created?.Id || ''),
		name: created?.Name || name,
		updated: false
	};
}

export async function POST({ request }) {
	const body = await request.json();
	const mode = body?.mode === 'create' ? 'create' : 'preview';
	const userId = normalizeEmbyGuid(body?.userId);
	const seedIds = Array.isArray(body?.seedIds) ? body.seedIds : [];
	const limit = Math.min(Math.max(Number(body?.limit || 18), 1), 48);
	const includeSeeds = body?.includeSeeds !== false;
	const name = String(body?.name || '').trim();
	const profile = RECOMMENDATION_PROFILES[body?.profile] ? body.profile : 'balanced';

	if (!userId) {
		throw error(400, 'Missing userId');
	}

	if (seedIds.length === 0) {
		throw error(400, 'Select at least one seed item');
	}

	try {
		const preview = await buildPreview(userId, seedIds, limit, profile);
		const existingCollection = name ? await findExistingCollection(userId, name) : null;

		if (mode !== 'create') {
			return json({
				...preview,
				profile,
				collection: existingCollection
					? {
						id: existingCollection.id,
						name: existingCollection.name,
						updated: true
					}
					: null
			});
		}

		if (!name) {
			throw error(400, 'Collection name is required');
		}

		const collectionIds = [
			...(includeSeeds ? preview.seeds.map((item) => item.id) : []),
			...preview.recommendations.map((item) => item.id)
		];
		const uniqueIds = [...new Set(collectionIds.filter(Boolean))];

		if (uniqueIds.length === 0) {
			throw error(400, 'No items were available to add to the collection');
		}

		const collection = await createOrUpdateCollection(userId, name, uniqueIds);

		return json({
			seeds: preview.seeds,
			recommendations: preview.recommendations,
			profile,
			collection
		});
	} catch (err) {
		if (err?.status) throw err;
		if (String(err.message || '').includes('not configured')) {
			throw error(400, err.message);
		}
		throw error(502, `Could not build Emby collection: ${err.message}`);
	}
}
