import { findBy } from "@nontara/language-codes";
import type { ConsolaInstance } from "consola";
import { inArray } from "drizzle-orm";
import type ffmpeg from "fluent-ffmpeg";
import PQueue from "p-queue";
import type { database } from "@/db";
import { genres, mediaStreams, titleGenres, titleProviders } from "@/db/schema";
import type {
	LibraryType,
	MetadataProviderType,
	StreamCodecType,
} from "@/db/schema/enums";
import type { MetadataLanguage } from "@/models";
import { createLogger } from "@/utils/logger";
import type { MetadataService } from "../../metadata";

type DbTransaction = Parameters<Parameters<typeof database.transaction>[0]>[0];

export interface ScanFileOptions {
	filename: string;
	absolutePath: string;
	libraryName: string;
	metadataLanguage: MetadataLanguage;
	libraryFolderId: number;
}

export interface MediaInspectionResult {
	durationSeconds: number;
	detectedLanguage: string | null;
	mediaInfo: ffmpeg.FfprobeData;
}

export abstract class BaseLibraryScanner<
	TParsedInfo,
	TResolvedMetadata = unknown,
> {
	protected readonly logger: ConsolaInstance;
	protected metadataService: MetadataService;

	// Concurrent processing queues (accessible by child classes)
	protected readonly fileProcessingQueue: PQueue;
	protected readonly metadataQueue: PQueue;
	protected readonly databaseQueue: PQueue;

	// Progress tracking
	private scanStats = {
		total: 0,
		processed: 0,
		failed: 0,
	};

	constructor(
		protected readonly scannerName: string,
		metadataService: MetadataService,
	) {
		this.logger = createLogger(scannerName);
		this.metadataService = metadataService;

		// File processing queue: Process 5 files simultaneously
		this.fileProcessingQueue = new PQueue({
			concurrency: 5,
			timeout: 120000,
			autoStart: true,
		});

		// Metadata API queue: Rate-limited for TMDB (40 requests per 10 seconds)
		this.metadataQueue = new PQueue({
			concurrency: 10,
			interval: 10000,
			intervalCap: 40,
			timeout: 30000,
			autoStart: true,
		});

		// Database queue: Match connection pool size
		this.databaseQueue = new PQueue({
			concurrency: 10,
			timeout: 60000,
			autoStart: true,
		});

		// Queue event handlers
		this.fileProcessingQueue.on("active", () => {
			this.logger.debug(
				`Processing: ${this.fileProcessingQueue.pending}/${this.scanStats.total} files`,
			);
		});

		this.fileProcessingQueue.on("idle", () => {
			this.logger.success("File processing queue completed");
		});

		this.fileProcessingQueue.on("error", (error) => {
			this.logger.error(`File processing queue error: ${error}`);
		});
	}

	public abstract getLibraryType(): LibraryType;

	protected abstract parseFilename(
		filename: string,
	): TParsedInfo | Promise<TParsedInfo | null> | null;

	protected abstract resolveMetadata(
		parsedInfo: TParsedInfo,
		mediaInspection: MediaInspectionResult,
		metadataLanguage: MetadataLanguage,
		filename: string,
	): Promise<TResolvedMetadata | null>;

	protected abstract insertToDatabase(
		parsedInfo: TParsedInfo,
		metadata: TResolvedMetadata | null,
		mediaInspection: MediaInspectionResult,
		options: ScanFileOptions,
	): Promise<boolean>;

	protected abstract formatParsedInfo(parsedInfo: TParsedInfo): string;

	/**
	 * Reset scan statistics
	 */
	public resetStats(): void {
		this.scanStats = {
			total: 0,
			processed: 0,
			failed: 0,
		};
	}

	/**
	 * Increment total files counter
	 */
	public incrementTotal(): void {
		this.scanStats.total++;
	}

	/**
	 * Get queue statistics and progress
	 */
	public getQueueStats() {
		return {
			files: {
				total: this.scanStats.total,
				processed: this.scanStats.processed,
				failed: this.scanStats.failed,
				remaining:
					this.fileProcessingQueue.size + this.fileProcessingQueue.pending,
				percentage:
					this.scanStats.total > 0
						? Math.round(
								(this.scanStats.processed / this.scanStats.total) * 100,
							)
						: 0,
			},
			queues: {
				fileProcessing: {
					size: this.fileProcessingQueue.size,
					pending: this.fileProcessingQueue.pending,
					isPaused: this.fileProcessingQueue.isPaused,
				},
				metadata: {
					size: this.metadataQueue.size,
					pending: this.metadataQueue.pending,
					isPaused: this.metadataQueue.isPaused,
				},
				database: {
					size: this.databaseQueue.size,
					pending: this.databaseQueue.pending,
					isPaused: this.databaseQueue.isPaused,
				},
			},
		};
	}

	/**
	 * Pause all scanning queues
	 */
	public pause(): void {
		this.fileProcessingQueue.pause();
		this.metadataQueue.pause();
		this.databaseQueue.pause();
		this.logger.info("All scanning queues paused");
	}

	/**
	 * Resume all scanning queues
	 */
	public resume(): void {
		this.fileProcessingQueue.start();
		this.metadataQueue.start();
		this.databaseQueue.start();
		this.logger.info("All scanning queues resumed");
	}

	/**
	 * Wait for all queues to complete their current work
	 */
	public async waitForCompletion(): Promise<void> {
		await Promise.all([
			this.fileProcessingQueue.onIdle(),
			this.metadataQueue.onIdle(),
			this.databaseQueue.onIdle(),
		]);
	}

	public async scanFile(options: ScanFileOptions): Promise<boolean> {
		const { filename, absolutePath, libraryName } = options;

		this.logger.debug(`[${libraryName}] Processing file: ${filename}`);

		try {
			const parsedInfo: TParsedInfo | null = await this.parseFilename(filename);
			if (!parsedInfo) {
				this.logger.warn(`[${libraryName}] Cannot parse filename: ${filename}`);
				this.scanStats.failed++;
				return false;
			}

			this.logger.info(
				`[${libraryName}] Parsed: ${this.formatParsedInfo(parsedInfo)}`,
			);

			this.logger.debug("Inspecting media file with ffprobe");
			const mediaInfo = await this.inspectMediaFile(absolutePath);
			if (!mediaInfo) {
				this.logger.error(
					`[${libraryName}] Failed to inspect media file: ${filename}`,
				);
				this.scanStats.failed++;
				return false;
			}

			const detectedLanguage = this.detectLanguageFromStreams(
				mediaInfo.streams,
			);
			const durationSeconds = Math.round(mediaInfo.format.duration || 0);

			this.logger.debug(
				`Duration: ${Math.round(durationSeconds / 60)} minutes, Language: ${detectedLanguage || "unknown"}`,
			);

			const mediaInspection: MediaInspectionResult = {
				durationSeconds,
				detectedLanguage,
				mediaInfo,
			};

			this.logger.debug("Fetching metadata from providers");
			const metadata: TResolvedMetadata | null = await this.resolveMetadata(
				parsedInfo,
				mediaInspection,
				options.metadataLanguage,
				filename,
			);

			const result = await this.insertToDatabase(
				parsedInfo,
				metadata,
				mediaInspection,
				options,
			);

			if (result) {
				this.scanStats.processed++;
			} else {
				this.scanStats.failed++;
			}

			return result;
		} catch (error) {
			this.scanStats.failed++;
			this.logger.error(`[${libraryName}] Scan failed: ${error}`);
			return false;
		}
	}

	protected async inspectMediaFile(
		filePath: string,
	): Promise<ffmpeg.FfprobeData | null> {
		const ffmpegModule = await import("fluent-ffmpeg");
		return new Promise((resolve) => {
			ffmpegModule.default.ffprobe(filePath, (err, metadata) => {
				if (err) {
					resolve(null);
				} else {
					resolve(metadata);
				}
			});
		});
	}

	protected detectLanguageFromStreams(
		streams: ffmpeg.FfprobeStream[],
	): string | null {
		const audioStreams = streams.filter(
			(stream) => stream.codec_type === "audio",
		);

		for (const stream of audioStreams) {
			if (stream.tags?.language) {
				const langCode = stream.tags.language.toLowerCase();

				let language = findBy("1", langCode);
				if (!language) language = findBy("2", langCode);
				if (!language) language = findBy("2T", langCode);
				if (!language) language = findBy("2B", langCode);

				if (language) {
					return language["1"];
				}
			}
		}

		return null;
	}

	protected async insertGenres(
		titleId: string,
		genreNames: string[],
		tx: DbTransaction,
	): Promise<void> {
		if (genreNames.length === 0) return;

		// Batch fetch all existing genres in one query
		const existingGenres = await tx
			.select({ id: genres.id, name: genres.name })
			.from(genres)
			.where(inArray(genres.name, genreNames));

		const existingGenreMap = new Map(existingGenres.map((g) => [g.name, g.id]));

		// Determine which genres need to be created
		const genresToCreate = genreNames.filter(
			(name) => !existingGenreMap.has(name),
		);

		// Batch insert new genres if any
		if (genresToCreate.length > 0) {
			const newGenres = await tx
				.insert(genres)
				.values(genresToCreate.map((name) => ({ name, description: null })))
				.returning({ id: genres.id, name: genres.name });

			// Add newly created genres to the map
			for (const genre of newGenres) {
				existingGenreMap.set(genre.name, genre.id);
			}
		}

		// Batch insert all title-genre relationships
		const titleGenreValues = genreNames
			.map((genreName) => {
				const genreId = existingGenreMap.get(genreName);
				if (!genreId) return null;
				return { titleId, genreId };
			})
			.filter((value): value is NonNullable<typeof value> => value !== null);

		await tx.insert(titleGenres).values(titleGenreValues).onConflictDoNothing();

		this.logger.debug(`Inserted ${genreNames.length} genres`);
	}

	protected async insertMediaStreams(
		mediaItemId: number,
		streams: ffmpeg.FfprobeStream[],
		tx: DbTransaction,
	): Promise<void> {
		const streamsToInsert = streams.map((stream) => {
			let codecType: StreamCodecType = "DATA";
			if (stream.codec_type === "video") codecType = "VIDEO";
			else if (stream.codec_type === "audio") codecType = "AUDIO";
			else if (stream.codec_type === "subtitle") codecType = "SUBTITLE";
			else if (stream.codec_type === "attachment") codecType = "ATTACHMENT";

			let frameRate: number | null = null;
			if (stream.r_frame_rate) {
				const [num, den] = stream.r_frame_rate.split("/");
				if (num && den && Number.parseInt(den, 10) !== 0) {
					frameRate = Number.parseInt(num, 10) / Number.parseInt(den, 10);
				}
			}

			return {
				mediaItemId,
				streamIndex: stream.index,
				codecType,
				codecName: stream.codec_name || null,
				codecLongName: stream.codec_long_name || null,
				profile: (stream.profile ? String(stream.profile) : null) as
					| string
					| null,
				bitRate: stream.bit_rate ? Number(stream.bit_rate) : null,
				width: stream.width || null,
				height: stream.height || null,
				frameRate,
				channels: stream.channels || null,
				channelLayout: stream.channel_layout || null,
				sampleRate: stream.sample_rate ? Number(stream.sample_rate) : null,
				language: stream.tags?.language || null,
				title: stream.tags?.title || null,
				isDefault: stream.disposition?.default === 1,
				isForced: stream.disposition?.forced === 1,
				streamJson: JSON.stringify(stream),
			};
		});

		await tx.insert(mediaStreams).values(streamsToInsert);
		this.logger.debug(`Inserted ${streamsToInsert.length} media streams`);
	}

	protected async insertProviderIds(
		titleId: string,
		providerIds: Array<{ provider: MetadataProviderType; providerId: string }>,
		tx: DbTransaction,
	): Promise<void> {
		if (providerIds.length === 0) return;

		// Batch insert all provider IDs in one query
		const values = providerIds.map((p) => ({
			titleId,
			provider: p.provider,
			providerId: p.providerId,
		}));

		await tx.insert(titleProviders).values(values).onConflictDoNothing();

		this.logger.debug(
			`Linked ${providerIds.length} provider ID(s): ${providerIds.map((p) => `${p.provider}:${p.providerId}`).join(", ")}`,
		);
	}
}
