import { inArray } from "drizzle-orm";
import { database } from "@/db";
import { credits, mediaItems, movies, people, titles } from "@/db/schema";
import type { LibraryType } from "@/db/schema/enums";
import type {
	MetadataLanguage,
	MovieInfo,
	ResolvedMovieMetadata,
	ResolvedTitleCredits,
} from "@/models";
import { MovieNamings } from "@/namings/movies";
import type { MetadataService } from "../../metadata";
import {
	BaseLibraryScanner,
	type MediaInspectionResult,
	type ScanFileOptions,
} from "./BaseLibraryScanner";

interface ParsedMovieInfo {
	title: string;
	year: number;
}

export class MovieLibraryScanner extends BaseLibraryScanner<
	ParsedMovieInfo,
	ResolvedMovieMetadata
> {
	private readonly movieNamings = new MovieNamings();

	constructor(metadataService: MetadataService) {
		super("MovieLibraryScanner", metadataService);
	}

	public getLibraryType(): LibraryType {
		return "MOVIE";
	}

	protected parseFilename(filename: string): ParsedMovieInfo | null {
		return this.movieNamings.parseFilename(filename);
	}

	protected formatParsedInfo(parsedInfo: ParsedMovieInfo): string {
		return `"${parsedInfo.title}" (${parsedInfo.year})`;
	}

	protected async resolveMetadata(
		parsedInfo: ParsedMovieInfo,
		mediaInspection: MediaInspectionResult,
		metadataLanguage: MetadataLanguage,
		filename: string,
	): Promise<ResolvedMovieMetadata | null> {
		const { durationSeconds, detectedLanguage } = mediaInspection;

		const info: MovieInfo = {
			fileName: filename,
			runtimeSeconds: durationSeconds,
			title: parsedInfo.title,
			year: parsedInfo.year,
			language: detectedLanguage || "en",
			metadataLanguage,
		};

		try {
			// Use metadata queue for rate-limited API calls
			return await this.metadataQueue.add(async () => {
				return await this.metadataService.resolveMovie(info);
			});
		} catch (error) {
			this.logger.debug(
				`Metadata resolution failed for "${parsedInfo.title}" (${parsedInfo.year}) [${filename}]: ${error}`,
			);
			return null;
		}
	}

	protected async insertToDatabase(
		parsedInfo: ParsedMovieInfo,
		metadata: ResolvedMovieMetadata | null,
		mediaInspection: MediaInspectionResult,
		options: ScanFileOptions,
	): Promise<boolean> {
		const resolvedMetadata = metadata;
		const { durationSeconds, mediaInfo } = mediaInspection;
		const { absolutePath, libraryName, libraryFolderId } = options;

		const finalMetadata: ResolvedMovieMetadata = resolvedMetadata || {
			title: parsedInfo.title,
			originalTitle: parsedInfo.title,
			year: parsedInfo.year,
			description: "",
			releaseDate: "",
			genres: [],
			countries: [],
			posterUrl: null,
			backdropUrl: null,
			rating: 0,
			language: mediaInspection.detectedLanguage || "en",
			providerIds: [],
		};

		if (!resolvedMetadata) {
			this.logger.warn(
				`[${libraryName}] No metadata found for "${parsedInfo.title}", using placeholder data`,
			);
		}

		try {
			let createdMovieId: string | null = null;

			// Use database queue to control concurrent database operations
			await this.databaseQueue.add(async () => {
				await database.transaction(async (tx) => {
					const insertedTitle = await tx
						.insert(titles)
						.values({
							libraryFolderId,
							type: "MOVIE" as const,
							title: finalMetadata.title,
							originalTitle: finalMetadata.originalTitle,
							tagline: finalMetadata.tagline || null,
							overview: finalMetadata.description,
							year: finalMetadata.year,
							releaseDate: finalMetadata.releaseDate,
							poster: finalMetadata.posterUrl,
							backdrop: finalMetadata.backdropUrl || null,
							rating: finalMetadata.rating || null,
							originalLanguage: finalMetadata.originalLanguage || null,
							homepage: finalMetadata.homepage || null,
						})
						.returning({ id: titles.id });

					const titleId = insertedTitle[0].id;

					const insertedMovie = await tx
						.insert(movies)
						.values({
							titleId,
							runtimeSeconds: durationSeconds,
						})
						.returning({ id: movies.id });

					const movieId = insertedMovie[0].id;

					const insertedMediaItem = await tx
						.insert(mediaItems)
						.values({
							parentType: "MOVIE" as const,
							movieId,
							episodeId: null,
							path: absolutePath,
							etag: null,
							sizeBytes: mediaInfo.format.size,
							container: mediaInfo.format.format_name || null,
						})
						.returning({ id: mediaItems.id });

					const mediaItemId = insertedMediaItem[0].id;

					if (finalMetadata.genres && finalMetadata.genres.length > 0) {
						await this.insertGenres(titleId, finalMetadata.genres, tx);
					}

					if (
						finalMetadata.providerIds &&
						finalMetadata.providerIds.length > 0
					) {
						await this.insertProviderIds(
							titleId,
							finalMetadata.providerIds,
							tx,
						);
					}

					if (mediaInfo.streams && mediaInfo.streams.length > 0) {
						await this.insertMediaStreams(mediaItemId, mediaInfo.streams, tx);
					}

					createdMovieId = movieId;
					this.logger.debug(`Successfully inserted movie ID ${movieId}`);
				});
			});

			// Fetch and insert credits using metadata queue
			if (createdMovieId && resolvedMetadata) {
				try {
					this.logger.debug("Fetching credits from metadata provider");

					const movieId = createdMovieId;

					// Use metadata queue for rate-limited API calls
					const creditsData = await this.metadataQueue.add(async () => {
						return await this.resolveCredits(movieId, resolvedMetadata);
					});

					if (creditsData) {
						await this.insertCredits(createdMovieId, creditsData);
					} else {
						this.logger.warn("Credits not found");
					}
				} catch (error) {
					this.logger.warn(`Failed to resolve credits: ${error}`);
				}
			}

			this.logger.success(
				`[${libraryName}] Added "${finalMetadata.title}" (${finalMetadata.year}) - ${Math.round(durationSeconds / 60)}min`,
			);
			return true;
		} catch (error) {
			this.logger.error(`[${libraryName}] Failed to insert movie: ${error}`);
			return false;
		}
	}

	private async resolveCredits(
		_movieId: string,
		metadata: ResolvedMovieMetadata,
	): Promise<ResolvedTitleCredits | null> {
		if (metadata.providerIds && metadata.providerIds.length > 0) {
			const provider = metadata.providerIds[0];

			try {
				return await this.metadataService.resolveTitleCredits({
					titleType: "MOVIE",
					providerId: provider.providerId,
				});
			} catch (error) {
				this.logger.debug(`Failed to resolve credits: ${error}`);
				return null;
			}
		}
		return null;
	}

	private async insertCredits(
		movieId: string,
		creditsData: ResolvedTitleCredits,
	): Promise<void> {
		await database.transaction(async (tx) => {
			// Collect all unique person names
			const allNames = [
				...creditsData.cast.map((c) => c.name),
				...creditsData.crew.map((c) => c.name),
			];

			if (allNames.length === 0) {
				return;
			}

			// Batch lookup existing people (1 query)
			const existingPeople = await tx
				.select({ id: people.id, name: people.name })
				.from(people)
				.where(inArray(people.name, allNames));

			const peopleMap = new Map(existingPeople.map((p) => [p.name, p.id]));

			// Determine which people need to be created
			const missingPeople = [
				...creditsData.cast
					.filter((c) => !peopleMap.has(c.name))
					.map((c) => ({ name: c.name, profilePicture: c.profilePath })),
				...creditsData.crew
					.filter((c) => !peopleMap.has(c.name))
					.map((c) => ({ name: c.name, profilePicture: c.profilePath })),
			];

			// Remove duplicates from missing people
			const uniqueMissingPeople = Array.from(
				new Map(missingPeople.map((p) => [p.name, p])).values(),
			);

			// Batch insert missing people (1 query)
			if (uniqueMissingPeople.length > 0) {
				const newPeople = await tx
					.insert(people)
					.values(uniqueMissingPeople)
					.returning({ id: people.id, name: people.name });

				for (const person of newPeople) {
					peopleMap.set(person.name, person.id);
				}
			}

			// Build all credits entries
			const allCredits = [
				...creditsData.cast
					.map((castMember) => {
						const personId = peopleMap.get(castMember.name);
						if (!personId) return null;
						return {
							personId,
							movieId,
							episodeId: null,
							seriesId: null,
							role: "CAST" as const,
							character: castMember.character,
							department: castMember.knownForDepartment,
							order: castMember.order,
							job: null,
						};
					})
					.filter(
						(credit): credit is NonNullable<typeof credit> => credit !== null,
					),
				...creditsData.crew
					.map((crewMember) => {
						const personId = peopleMap.get(crewMember.name);
						if (!personId) return null;
						return {
							personId,
							movieId,
							episodeId: null,
							seriesId: null,
							role: "CREW" as const,
							character: null,
							department: crewMember.department,
							order: null,
							job: crewMember.job,
						};
					})
					.filter(
						(credit): credit is NonNullable<typeof credit> => credit !== null,
					),
			];

			// Batch insert all credits (1 query)
			if (allCredits.length > 0) {
				await tx.insert(credits).values(allCredits).onConflictDoNothing();
			}

			this.logger.debug(
				`Inserted ${creditsData.cast.length} cast and ${creditsData.crew.length} crew`,
			);
		});
	}
}
