import { readdir } from "node:fs/promises";
import { join } from "node:path";
import { eq, inArray } from "drizzle-orm";
import PQueue from "p-queue";
import { database } from "@/db";
import { libraryFolders, mediaItems } from "@/db/schema";
import type { LibraryType } from "@/db/schema/enums";
import type { MetadataLanguage } from "@/models";
import { BaseService } from "../BaseService";
import { MetadataService } from "../metadata";
import type { BaseLibraryScanner } from "./scanners/BaseLibraryScanner";
import { MovieLibraryScanner } from "./scanners/MovieLibraryScanner";
import { SeriesLibraryScanner } from "./scanners/SeriesLibraryScanner";

type LibraryFolderRow = typeof libraryFolders.$inferSelect;

export class LibraryScannerService extends BaseService {
	public isRunning = false;

	private scanners: Map<LibraryType, BaseLibraryScanner<unknown, unknown>> =
		new Map();

	// Video file extensions as Set for O(1) lookup
	private readonly VIDEO_EXTENSIONS = new Set([
		".mp4",
		".mkv",
		".avi",
		".mov",
		".wmv",
		".flv",
		".webm",
		".m4v",
		".mpg",
		".mpeg",
	]);

	// Queue for controlled concurrent directory processing
	private directoryQueue: PQueue;

	constructor() {
		super("LibraryScannerService");

		// Initialize directory processing queue with concurrency limit
		// This prevents overwhelming the system when scanning directories with many subfolders
		this.directoryQueue = new PQueue({
			concurrency: 15, // Process up to 15 directories concurrently
			autoStart: true,
		});
	}

	private get metadataService(): MetadataService {
		return this.services.get(MetadataService);
	}

	public async mount(): Promise<void> {
		this.logger.info("Mounting library scanner service.");
		await this.metadataService.ensureReadyForScanning();

		this.scanners.set("MOVIE", new MovieLibraryScanner(this.metadataService));
		this.scanners.set("SERIES", new SeriesLibraryScanner(this.metadataService));

		this.logger.info(`Registered ${this.scanners.size} content scanners`);
		this.isRunning = true;
	}

	public async dispose(): Promise<void> {
		this.logger.info("Disposing library scanner service.");

		// Clear directory queue
		this.directoryQueue.clear();

		this.scanners.clear();
		this.isRunning = false;
	}

	private getScannerForType(
		type: LibraryType,
	): BaseLibraryScanner<unknown, unknown> | undefined {
		return this.scanners.get(type);
	}

	public async scanFolder(): Promise<void> {
		this.logger.info("Starting library scan process...");

		const registry = this.metadataService.getProviderRegistry();
		if (!registry || !registry.hasProviders()) {
			this.logger.warn(
				"Skipping scan because no metadata providers are currently registered.",
			);
			return;
		}

		const startTime: number = Date.now();
		const folders = await database.select().from(libraryFolders);

		if (folders.length === 0) {
			this.logger.warn("No library folders found in database.");
			return;
		}

		this.logger.info(`Found ${folders.length} library folder(s) to scan`);

		for (const folder of folders) {
			await this.scanLibraryFolder(folder);
		}

		const duration: string = ((Date.now() - startTime) / 1000).toFixed(2);
		this.logger.success(`Library scan completed in ${duration} seconds`);
	}

	public async scanLibraryById(libraryId: number): Promise<void> {
		const registry = this.metadataService.getProviderRegistry();
		if (!registry || !registry.hasProviders()) {
			this.logger.warn(
				"Skipping scan because no metadata providers are currently registered.",
			);
			throw new Error("No metadata providers registered");
		}

		const [folder] = await database
			.select()
			.from(libraryFolders)
			.where(eq(libraryFolders.id, libraryId))
			.limit(1);

		if (!folder) {
			throw new Error(`Library with ID ${libraryId} not found`);
		}

		await this.scanLibraryFolder(folder);
	}

	private async scanLibraryFolder(folder: LibraryFolderRow): Promise<void> {
		const startTime: number = Date.now();

		this.logger.info(
			`Scanning library folder "${folder.name}" (${folder.type}) at ${folder.path}`,
		);

		this.logger.info(
			`Language settings - Primary: ${folder.primaryLanguage}, Fallback: ${folder.fallbackLanguage}`,
		);

		await this.scanDirectory(
			folder.path,
			folder.type,
			folder.name,
			{
				primary: folder.primaryLanguage,
				fallback: folder.fallbackLanguage,
			},
			folder.id,
		);

		const duration: string = ((Date.now() - startTime) / 1000).toFixed(2);
		this.logger.success(
			`Completed scanning "${folder.name}" in ${duration} seconds`,
		);
	}

	private isVideoFile(filename: string): boolean {
		const ext = filename.substring(filename.lastIndexOf(".")).toLowerCase();
		return this.VIDEO_EXTENSIONS.has(ext);
	}

	private async scanDirectory(
		directoryPath: string,
		libraryType: LibraryType,
		libraryName: string,
		metadataLanguage: MetadataLanguage,
		libraryFolderId: number,
	): Promise<{ files: number; directories: number }> {
		const stats: { files: number; directories: number } = {
			files: 0,
			directories: 0,
		};

		try {
			this.logger.debug(
				`[${libraryName}] Scanning directory: ${directoryPath}`,
			);

			const entries = await readdir(directoryPath, { withFileTypes: true });
			const scanner = this.getScannerForType(libraryType);

			if (!scanner) {
				this.logger.warn(`Unsupported library type "${libraryType}"`);
				return stats;
			}

			// Separate subdirectories and video files for concurrent processing
			const subdirectories: string[] = [];
			const videoFiles: Array<{
				filename: string;
				absolutePath: string;
			}> = [];

			for (const entry of entries) {
				const fullPath = join(directoryPath, entry.name);

				if (entry.isDirectory()) {
					subdirectories.push(fullPath);
					stats.directories++;
				} else if (entry.isFile() && this.isVideoFile(entry.name)) {
					stats.files++;
					videoFiles.push({
						filename: entry.name,
						absolutePath: fullPath,
					});
				}
			}

			// Process all subdirectories with controlled concurrency using queue
			if (subdirectories.length > 0) {
				this.logger.debug(
					`[${libraryName}] Found ${subdirectories.length} subdirectories, processing with controlled concurrency...`,
				);

				// Use p-queue to limit concurrent directory processing
				// This prevents memory/CPU issues with thousands of subdirectories
				const subDirPromises = subdirectories.map((subDirPath) =>
					this.directoryQueue.add(async () =>
						this.scanDirectory(
							subDirPath,
							libraryType,
							libraryName,
							metadataLanguage,
							libraryFolderId,
						),
					),
				);

				const subDirResults = await Promise.all(subDirPromises);

				// Aggregate stats from all subdirectories
				for (const subStats of subDirResults) {
					stats.files += subStats.files;
					stats.directories += subStats.directories;
				}
			}

			// Batch check which files already exist in database (1 query instead of N)
			let filesToScan = videoFiles;
			let skippedFiles = 0;

			if (videoFiles.length > 0) {
				const allPaths = videoFiles.map((f) => f.absolutePath);
				const existingMedia = await database
					.select({ path: mediaItems.path })
					.from(mediaItems)
					.where(inArray(mediaItems.path, allPaths));

				const existingPathsSet = new Set(existingMedia.map((m) => m.path));

				// Filter out files that already exist
				filesToScan = videoFiles.filter(
					(f) => !existingPathsSet.has(f.absolutePath),
				);
				skippedFiles = videoFiles.length - filesToScan.length;

				if (skippedFiles > 0) {
					this.logger.debug(
						`[${libraryName}] Skipping ${skippedFiles} files already in database`,
					);
				}
			}

			// Process new files concurrently using scanner's internal queue
			if (filesToScan.length > 0) {
				this.logger.info(
					`[${libraryName}] Processing ${filesToScan.length} new files concurrently...`,
				);

				// Reset stats and set total count for progress tracking
				scanner.resetStats();
				for (const _file of filesToScan) {
					scanner.incrementTotal();
				}

				// Queue all files for concurrent processing
				const scanPromises = filesToScan.map((file) =>
					scanner.scanFile({
						filename: file.filename,
						absolutePath: file.absolutePath,
						libraryName,
						metadataLanguage,
						libraryFolderId,
					}),
				);

				// Wait for all scans to complete
				const results = await Promise.allSettled(scanPromises);

				// Wait for all queues to finish
				await scanner.waitForCompletion();

				// Track which files failed for detailed reporting
				const failedFilesList: string[] = [];
				results.forEach((result, index) => {
					if (result.status === "rejected" || !result.value) {
						failedFilesList.push(filesToScan[index].filename);
					}
				});

				// Count successes and failures
				const processedFiles = results.filter(
					(r) => r.status === "fulfilled" && r.value,
				).length;
				const failedFiles = failedFilesList.length;

				this.logger.info(
					`[${libraryName}] Batch complete: ${processedFiles} successful, ${failedFiles} failed`,
				);

				if (failedFilesList.length > 0 && failedFilesList.length <= 10) {
					this.logger.warn(
						`[${libraryName}] Failed files: ${failedFilesList.join(", ")}`,
					);
				} else if (failedFilesList.length > 10) {
					this.logger.warn(
						`[${libraryName}] ${failedFilesList.length} files failed (too many to list)`,
					);
				}
			}

			this.logger.info(
				`[${libraryName}] Directory scan complete: ${directoryPath}`,
			);
			this.logger.info(
				`  → Found ${stats.files} files | ${filesToScan.length} new, ${skippedFiles} skipped`,
			);

			return stats;
		} catch (error) {
			this.logger.error(
				`Failed to scan directory ${directoryPath}. Skipping.`,
				error,
			);
			return stats;
		}
	}
}
