import type { BaseMovieProvider, Models } from "@nontara/server";
import { createLogger, ExtensionError } from "@nontara/server";
import type { ConsolaInstance } from "consola";
import { MediaSize, type TMDB } from "tmdb-ts";
import { createTMDBClient } from "../shared/client";
import type { TMDBProviderConfig } from "../shared/types";

export class TMDBMovieProvider implements BaseMovieProvider {
	private logger: ConsolaInstance;
	private client: TMDB | null = null;

	constructor(_config?: TMDBProviderConfig) {
		this.logger = createLogger("TMDBMovieProvider");
	}

	/**
	 * Mount/initialize the provider
	 */
	async mount(): Promise<void> {
		this.logger.debug("Mounting movie provider");
	}

	/**
	 * Dispose/cleanup the provider
	 */
	async dispose(): Promise<void> {
		this.logger.debug("Disposing movie provider");
		this.client = null;
	}

	/**
	 * Initialize TMDB client
	 * Called internally before making API requests
	 */
	private async ensureClient(): Promise<TMDB> {
		if (!this.client) {
			this.client = await createTMDBClient();
			if (!this.client) {
				throw new ExtensionError("TMDB client not available");
			}
		}
		return this.client;
	}

	async search(
		query: Models.MetadataSearchQuery,
	): Promise<Models.MetadataSearchResult> {
		try {
			const client = await this.ensureClient();
			const response = await client.search.movies({
				query: query.title,
				year: query.year ? Number.parseInt(query.year, 10) : undefined,
			});

			const results: Models.MetadataSearchResultItem[] = [];

			if (response.results) {
				const sortedResults = [...response.results].sort(
					(a, b) => (b.popularity || 0) - (a.popularity || 0),
				);

				for (const movie of sortedResults) {
					results.push({
						id: movie.id,
						title: movie.title,
						originalTitle: movie.original_title,
						posterPath: movie.poster_path ?? null,
						releaseDate: movie.release_date,
						originalLanguage: movie.original_language || "",
					});
				}
			}

			return { results };
		} catch (error) {
			this.logger.error("Error searching movies:", error);
			throw new ExtensionError(`TMDB movie search failed: ${error}`);
		}
	}

	async resolve(
		info: Models.MovieInfo,
	): Promise<Models.ResolvedMovieMetadataResult> {
		const client = await this.ensureClient();

		// Get all pages of search results
		const allMovies = [];
		let currentPage = 1;
		let totalPages = 1;

		do {
			const search = await client.search.movies({
				query: info.title ?? info.fileName,
				year: info.year,
				page: currentPage,
			});

			if (!search.results || search.results.length === 0) {
				break;
			}

			allMovies.push(...search.results);
			totalPages = search.total_pages || 1;
			currentPage++;
		} while (currentPage <= totalPages);

		if (allMovies.length === 0) {
			return null;
		}

		// Filter by original_language matching info.language
		const languageMatches = allMovies.filter(
			(movie) => movie.original_language === info.language,
		);

		// If no language matches, use all results
		const candidateMovies =
			languageMatches.length > 0 ? languageMatches : allMovies;

		// Find the movie with best combined score (title similarity + runtime closeness)
		let bestMatch = candidateMovies[0];
		let bestScore = Number.MAX_SAFE_INTEGER;

		const normalizeTitle = (title: string) =>
			title
				.toLowerCase()
				.replace(/[^\w\s]/g, "")
				.trim();

		const searchTitle = normalizeTitle(info.title ?? info.fileName);

		for (const movie of candidateMovies) {
			// Get detailed movie info to access runtime
			const details = await client.movies.details(movie.id);

			// Calculate title similarity score (lower is better)
			const movieTitle = normalizeTitle(movie.title);
			const originalTitle = normalizeTitle(movie.original_title || movie.title);

			const titleDistance1 = Math.abs(searchTitle.length - movieTitle.length);
			const titleDistance2 = Math.abs(
				searchTitle.length - originalTitle.length,
			);
			const titleScore = Math.min(titleDistance1, titleDistance2);

			// Check for exact title matches (bonus)
			const exactMatch =
				searchTitle === movieTitle || searchTitle === originalTitle;
			const titleMatchScore = exactMatch ? 0 : titleScore;

			// Calculate runtime difference (normalize to 0-100 scale)
			let runtimeScore = 50; // default if no runtime available
			if (details.runtime && info.runtimeSeconds) {
				const runtimeDifference = Math.abs(
					details.runtime * 60 - info.runtimeSeconds,
				);
				runtimeScore = Math.min(runtimeDifference, 100);
			}

			// Combined score (title weight: 70%, runtime weight: 30%)
			const combinedScore = titleMatchScore * 0.7 + runtimeScore * 0.3;

			if (combinedScore < bestScore) {
				bestScore = combinedScore;
				bestMatch = movie;
			}
		}

		// Get detailed info for the best match
		if (!bestMatch) {
			return null;
		}

		const movieDetails = await client.movies.details(bestMatch.id, [
			"translations",
		]);

		// Check if there's a translation matching the primary metadata language
		const matchingTranslation = movieDetails.translations?.translations?.find(
			(translation) =>
				translation.iso_639_1 === info.metadataLanguage.primary &&
				translation.data.title.length > 0,
		);

		// If a matching translation is found, re-query the movie details with that language
		if (matchingTranslation) {
			this.logger.debug(
				`Found primary language translations for file name ${info.fileName}`,
			);
			const localizedDetails = await client.movies.details(
				bestMatch.id,
				undefined,
				info.metadataLanguage.primary,
			);

			const providerIds: Models.ProviderId[] = [
				{ provider: "TMDB", providerId: String(bestMatch.id) },
			];
			if (movieDetails.imdb_id) {
				providerIds.push({
					provider: "IMDB",
					providerId: movieDetails.imdb_id,
				});
			}

			return {
				title: localizedDetails.title,
				originalTitle: localizedDetails.original_title,
				year: localizedDetails.release_date
					? new Date(movieDetails.release_date).getFullYear()
					: info.year,
				description:
					localizedDetails.overview.length > 0
						? localizedDetails.overview
						: movieDetails.overview,
				releaseDate: localizedDetails.release_date,
				genres: localizedDetails.genres?.map((g) => g.name) || [],
				countries:
					localizedDetails.production_countries?.map((c) => c.name) || [],
				posterUrl: localizedDetails.poster_path
					? `https://image.tmdb.org/t/p/w500${localizedDetails.poster_path}`
					: null,
				backdropUrl: localizedDetails.backdrop_path
					? `https://image.tmdb.org/t/p/w1280${localizedDetails.backdrop_path}`
					: null,
				rating: localizedDetails.vote_average,
				language: localizedDetails.original_language,
				providerIds,
			};
		}

		// Audio language matching metadata primary language
		if (info.language === info.metadataLanguage.primary) {
			const localizedDetails = await client.movies.details(
				bestMatch.id,
				[],
				info.metadataLanguage.primary,
			);

			const providerIds: Models.ProviderId[] = [
				{ provider: "TMDB", providerId: String(bestMatch.id) },
			];
			if (movieDetails.imdb_id) {
				providerIds.push({
					provider: "IMDB",
					providerId: movieDetails.imdb_id,
				});
			}

			return {
				title: localizedDetails.title,
				originalTitle: localizedDetails.original_title,
				year: localizedDetails.release_date
					? new Date(movieDetails.release_date).getFullYear()
					: info.year,
				description:
					localizedDetails.overview.length > 0
						? localizedDetails.overview
						: movieDetails.overview,
				releaseDate: localizedDetails.release_date,
				genres: movieDetails.genres?.map((g) => g.name) || [],
				countries:
					localizedDetails.production_countries?.map((c) => c.name) || [],
				posterUrl: localizedDetails.poster_path
					? `https://image.tmdb.org/t/p/w500${localizedDetails.poster_path}`
					: null,
				backdropUrl: localizedDetails.backdrop_path
					? `https://image.tmdb.org/t/p/w1280${localizedDetails.backdrop_path}`
					: null,
				rating: localizedDetails.vote_average,
				language: localizedDetails.original_language,
				homepage: localizedDetails.homepage,
				originalLanguage: localizedDetails.original_language,
				popularity: localizedDetails.popularity,
				tagline: localizedDetails.tagline ?? movieDetails.tagline,
				providerIds,
			};
		}

		// If no matching translation is found, use the default English language
		this.logger.debug(
			`Using fallback metadata language for file ${info.fileName}`,
		);

		const providerIds: Models.ProviderId[] = [
			{ provider: "TMDB", providerId: String(bestMatch.id) },
		];
		if (movieDetails.imdb_id) {
			providerIds.push({
				provider: "IMDB",
				providerId: movieDetails.imdb_id,
			});
		}

		return {
			title:
				movieDetails.original_language === info.metadataLanguage.primary
					? movieDetails.original_title
					: movieDetails.title,
			originalTitle: movieDetails.original_title,
			year: movieDetails.release_date
				? new Date(movieDetails.release_date).getFullYear()
				: info.year,
			description: movieDetails.overview,
			releaseDate: movieDetails.release_date,
			genres: movieDetails.genres?.map((g) => g.name) || [],
			countries: movieDetails.production_countries?.map((c) => c.name) || [],
			posterUrl: movieDetails.poster_path
				? `https://image.tmdb.org/t/p/${MediaSize.W500}${movieDetails.poster_path}`
				: null,
			backdropUrl: movieDetails.backdrop_path
				? `https://image.tmdb.org/t/p/${MediaSize.W1280}${movieDetails.backdrop_path}`
				: null,
			rating: movieDetails.vote_average,
			language: movieDetails.original_language,
			homepage: movieDetails.homepage,
			originalLanguage: movieDetails.original_language,
			popularity: movieDetails.popularity,
			tagline: movieDetails.tagline,
			providerIds,
		};
	}

	/**
	 * Resolve credits (cast & crew) for a movie
	 * Credits handling integrated in movie provider
	 */
	async resolveCredits(info: {
		movieId: string | number;
	}): Promise<Models.ResolvedTitleCreditsResult> {
		try {
			const client = await this.ensureClient();

			const tmdbId =
				typeof info.movieId === "string"
					? Number.parseInt(info.movieId, 10)
					: info.movieId;

			if (Number.isNaN(tmdbId)) {
				throw new ExtensionError(
					`Invalid TMDB movie ID format: ${info.movieId}`,
				);
			}

			const credits = await client.movies.credits(tmdbId);

			const cast: Models.Cast[] = (credits.cast || []).map((c) => ({
				adult: c.adult || false,
				gender: c.gender || 0,
				id: c.id,
				knownForDepartment: c.known_for_department || "",
				name: c.name,
				originalName: c.original_name || c.name,
				popularity: c.popularity || 0,
				profilePath: c.profile_path
					? `https://image.tmdb.org/t/p/w185${c.profile_path}`
					: null,
				castId: c.cast_id || 0,
				character: c.character || "",
				creditId: c.credit_id || "",
				order: c.order || 0,
			}));

			const crew: Models.Crew[] = (credits.crew || []).map((c) => ({
				adult: c.adult || false,
				gender: c.gender || 0,
				id: c.id,
				knownForDepartment: c.known_for_department || "",
				name: c.name,
				originalName: c.original_name || c.name,
				popularity: c.popularity || 0,
				profilePath: c.profile_path
					? `https://image.tmdb.org/t/p/w185${c.profile_path}`
					: null,
				creditId: c.credit_id || "",
				department: c.department || "",
				job: c.job || "",
			}));

			return { cast, crew };
		} catch (error) {
			this.logger.error("Error resolving movie credits:", error);
			throw new ExtensionError(
				`TMDB movie credits resolution failed: ${error}`,
			);
		}
	}
}
