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

export class TMDBSeriesProvider implements BaseSeriesProvider {
	private logger: ConsolaInstance;
	private client: TMDB | null = null;

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

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

	/**
	 * Dispose/cleanup the provider
	 */
	async dispose(): Promise<void> {
		this.logger.debug("Disposing series 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.tvShows({
				query: query.title,
				first_air_date_year: query.year
					? Number.parseInt(query.year, 10)
					: undefined,
			});

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

			if (response.results) {
				for (const tvShow of response.results) {
					results.push({
						id: tvShow.id,
						name: tvShow.name,
						originalName: tvShow.original_name,
						overview: tvShow.overview || "",
						posterPath: tvShow.poster_path,
						backdropPath: tvShow.backdrop_path,
						firstAirDate: tvShow.first_air_date,
						voteAverage: tvShow.vote_average || 0,
						voteCount: tvShow.vote_count || 0,
						popularity: tvShow.popularity || 0,
						adult: tvShow.adult || false,
						originalLanguage: tvShow.original_language || "",
						genreIds: tvShow.genre_ids || [],
						mediaType: "tv",
						originCountry: tvShow.origin_country || [],
					});
				}
			}

			// Sort by popularity (descending)
			results.sort((a, b) => b.popularity - a.popularity);

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

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

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

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

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

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

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

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

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

			// Find the show with best title match
			let bestMatch = candidateShows[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 show of candidateShows) {
				// Calculate title similarity score (lower is better)
				const showName = normalizeTitle(show.name);
				const originalName = normalizeTitle(show.original_name || show.name);

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

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

				if (titleMatchScore < bestScore) {
					bestScore = titleMatchScore;
					bestMatch = show;
				}
			}

			if (!bestMatch) {
				return null;
			}

			// Get detailed show info with translations
			const showDetails = await client.tvShows.details(bestMatch.id, [
				"translations",
			]);

			// Get season details
			const seasonDetails = await client.tvSeasons.details({
				tvShowID: bestMatch.id,
				seasonNumber: info.season,
			});

			// Get episode details
			const episodeDetails = await client.tvEpisode.details({
				tvShowID: bestMatch.id,
				seasonNumber: info.season,
				episodeNumber: info.episode,
			});

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

			// Build provider IDs
			const providerIds: Models.ProviderId[] = [
				{ provider: "TMDB", providerId: String(bestMatch.id) },
			];
			const externalIds = await client.tvShows.externalIds(bestMatch.id);
			if (externalIds.imdb_id) {
				providerIds.push({
					provider: "IMDB",
					providerId: externalIds.imdb_id,
				});
			}
			if (externalIds.tvdb_id) {
				providerIds.push({
					provider: "TVDB",
					providerId: String(externalIds.tvdb_id),
				});
			}

			// If a matching translation is found, re-query with that language
			if (matchingTranslation) {
				this.logger.debug(
					`Found primary language translations for file name ${info.fileName}`,
				);

				const localizedShowDetails = await client.tvShows.details(
					bestMatch.id,
					undefined,
					info.metadataLanguage.primary,
				);

				const localizedSeasonDetails = await client.tvSeasons.details(
					{
						tvShowID: bestMatch.id,
						seasonNumber: info.season,
					},
					undefined,
					{
						language: info.metadataLanguage.primary as AvailableLanguage,
					},
				);

				const localizedEpisodeDetails = await client.tvEpisode.details(
					{
						tvShowID: bestMatch.id,
						seasonNumber: info.season,
						episodeNumber: info.episode,
					},
					undefined,
					{
						language: info.metadataLanguage.primary as AvailableLanguage,
					},
				);

				return {
					id: String(bestMatch.id),
					title: localizedShowDetails.name,
					originalTitle: localizedShowDetails.original_name,
					year: localizedShowDetails.first_air_date
						? new Date(localizedShowDetails.first_air_date).getFullYear()
						: (info.year ?? undefined),
					description:
						localizedShowDetails.overview.length > 0
							? localizedShowDetails.overview
							: showDetails.overview,
					tagline: localizedShowDetails.tagline,
					status: localizedShowDetails.status,
					genres: localizedShowDetails.genres?.map((g) => g.name) || [],
					countries:
						localizedShowDetails.production_countries?.map((c) => c.name) || [],
					posterUrl: localizedShowDetails.poster_path
						? `https://image.tmdb.org/t/p/w500${localizedShowDetails.poster_path}`
						: null,
					backdropUrl: localizedShowDetails.backdrop_path
						? `https://image.tmdb.org/t/p/w1280${localizedShowDetails.backdrop_path}`
						: null,
					rating: localizedShowDetails.vote_average,
					popularity: localizedShowDetails.popularity,
					language: localizedShowDetails.original_language,
					originalLanguage: localizedShowDetails.original_language,
					homepage: localizedShowDetails.homepage,
					firstAirDate: localizedShowDetails.first_air_date,
					season: {
						number: info.season,
						name: localizedSeasonDetails.name,
					},
					episode: {
						number: info.episode,
						title: localizedEpisodeDetails.name,
						description: localizedEpisodeDetails.overview,
						backgroundUrl: localizedEpisodeDetails.still_path
							? `https://image.tmdb.org/t/p/w1280${localizedEpisodeDetails.still_path}`
							: null,
					},
					airDate: localizedEpisodeDetails.air_date,
					runtimeSeconds: localizedEpisodeDetails.runtime
						? localizedEpisodeDetails.runtime * 60
						: undefined,
					providerIds,
				};
			}

			// Audio language matching metadata primary language
			if (info.language === info.metadataLanguage.primary) {
				const localizedShowDetails = await client.tvShows.details(
					bestMatch.id,
					undefined,
					info.metadataLanguage.primary,
				);

				const localizedSeasonDetails = await client.tvSeasons.details(
					{
						tvShowID: bestMatch.id,
						seasonNumber: info.season,
					},
					undefined,
					{
						language: info.metadataLanguage.primary as AvailableLanguage,
					},
				);

				const localizedEpisodeDetails = await client.tvEpisode.details(
					{
						tvShowID: bestMatch.id,
						seasonNumber: info.season,
						episodeNumber: info.episode,
					},
					undefined,
					{
						language: info.metadataLanguage.primary as AvailableLanguage,
					},
				);

				return {
					id: String(bestMatch.id),
					title: localizedShowDetails.name,
					originalTitle: localizedShowDetails.original_name,
					year: localizedShowDetails.first_air_date
						? new Date(localizedShowDetails.first_air_date).getFullYear()
						: (info.year ?? undefined),
					description:
						localizedShowDetails.overview.length > 0
							? localizedShowDetails.overview
							: showDetails.overview,
					tagline: localizedShowDetails.tagline,
					status: localizedShowDetails.status,
					genres: localizedShowDetails.genres?.map((g) => g.name) || [],
					countries:
						localizedShowDetails.production_countries?.map((c) => c.name) || [],
					posterUrl: localizedShowDetails.poster_path
						? `https://image.tmdb.org/t/p/w500${localizedShowDetails.poster_path}`
						: null,
					backdropUrl: localizedShowDetails.backdrop_path
						? `https://image.tmdb.org/t/p/w1280${localizedShowDetails.backdrop_path}`
						: null,
					rating: localizedShowDetails.vote_average,
					popularity: localizedShowDetails.popularity,
					language: localizedShowDetails.original_language,
					originalLanguage: localizedShowDetails.original_language,
					homepage: localizedShowDetails.homepage,
					firstAirDate: localizedShowDetails.first_air_date,
					season: {
						number: info.season,
						name: localizedSeasonDetails.name,
					},
					episode: {
						number: info.episode,
						title: localizedEpisodeDetails.name,
						description: localizedEpisodeDetails.overview,
						backgroundUrl: localizedEpisodeDetails.still_path
							? `https://image.tmdb.org/t/p/w1280${localizedEpisodeDetails.still_path}`
							: null,
					},
					airDate: localizedEpisodeDetails.air_date,
					runtimeSeconds: localizedEpisodeDetails.runtime
						? localizedEpisodeDetails.runtime * 60
						: undefined,
					providerIds,
				};
			}

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

			return {
				id: String(bestMatch.id),
				title:
					showDetails.original_language === info.metadataLanguage.primary
						? showDetails.original_name
						: showDetails.name,
				originalTitle: showDetails.original_name,
				year: showDetails.first_air_date
					? new Date(showDetails.first_air_date).getFullYear()
					: (info.year ?? undefined),
				description: showDetails.overview,
				tagline: showDetails.tagline,
				status: showDetails.status,
				genres: showDetails.genres?.map((g) => g.name) || [],
				countries: showDetails.production_countries?.map((c) => c.name) || [],
				posterUrl: showDetails.poster_path
					? `https://image.tmdb.org/t/p/w500${showDetails.poster_path}`
					: null,
				backdropUrl: showDetails.backdrop_path
					? `https://image.tmdb.org/t/p/w1280${showDetails.backdrop_path}`
					: null,
				rating: showDetails.vote_average,
				popularity: showDetails.popularity,
				language: showDetails.original_language,
				originalLanguage: showDetails.original_language,
				homepage: showDetails.homepage,
				firstAirDate: showDetails.first_air_date,
				season: {
					number: info.season,
					name: seasonDetails.name,
				},
				episode: {
					number: info.episode,
					title: episodeDetails.name,
					description: episodeDetails.overview,
					backgroundUrl: episodeDetails.still_path
						? `https://image.tmdb.org/t/p/w1280${episodeDetails.still_path}`
						: null,
				},
				airDate: episodeDetails.air_date,
				runtimeSeconds: episodeDetails.runtime
					? episodeDetails.runtime * 60
					: undefined,
				providerIds,
			};
		} catch (error) {
			this.logger.error("Error resolving TV series:", error);
			throw new ExtensionError(`TMDB series resolution failed: ${error}`);
		}
	}

	/**
	 * Resolve credits (cast & crew) for a TV series (series-level)
	 * This fetches the main cast and crew for the entire series
	 */
	async resolveCredits(
		query: Models.SeriesCreditsQuery,
	): Promise<Models.ResolvedTitleCreditsResult> {
		try {
			const client = await this.ensureClient();

			const tmdbId = Number.parseInt(query.providerId, 10);

			if (Number.isNaN(tmdbId)) {
				throw new ExtensionError(
					`Invalid TMDB series ID format: ${query.providerId}`,
				);
			}

			// Fetch TV show level credits
			const credits = await client.tvShows.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 TV show credits:", error);
			throw new ExtensionError(
				`TMDB TV show credits resolution failed: ${error}`,
			);
		}
	}

	/**
	 * Resolve credits (cast & crew) for a TV series episode
	 */
	async resolveCreditsForEpisode(
		query: Models.SeriesEpisodeCreditsQuery,
	): Promise<Models.ResolvedTitleCreditsResult> {
		try {
			const client = await this.ensureClient();

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

			if (Number.isNaN(tmdbId)) {
				throw new ExtensionError(
					`Invalid TMDB series ID format: ${query.seriesId}`,
				);
			}

			// Fetch episode-specific credits
			const credits = await client.tvEpisode.credits({
				tvShowID: tmdbId,
				seasonNumber: query.seasonNumber,
				episodeNumber: query.episodeNumber,
			});

			// Map regular cast members
			const regularCast: 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,
			}));

			// Map guest stars to cast format
			const guestStars: Models.Cast[] = (credits.guest_stars || []).map(
				(g) => ({
					adult: g.adult || false,
					gender: g.gender || 0,
					id: g.id,
					knownForDepartment: g.known_for_department || "",
					name: g.name,
					originalName: g.original_name || g.name,
					popularity: g.popularity || 0,
					profilePath: g.profile_path
						? `https://image.tmdb.org/t/p/w185${g.profile_path}`
						: null,
					castId: 0, // Guest stars don't have cast_id
					character: g.character || "",
					creditId: g.credit_id || "",
					order: g.order || 0,
				}),
			);

			// Combine regular cast and guest stars
			const cast: Models.Cast[] = [...regularCast, ...guestStars];

			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 series credits:", error);
			throw new ExtensionError(
				`TMDB series credits resolution failed: ${error}`,
			);
		}
	}
}
