<script lang="ts">
	import { onMount } from 'svelte';
	import { env } from '$env/dynamic/public';
	import { columnMapping, sheetData, currentStep, pictures, cropRects, selectedCard } from '$lib/stores';
	import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl, ensureToken } from '$lib/google';
	import Navigator from './subcomponents/Navigator.svelte';
	import PhotoCard from './subcomponents/PhotoCard.svelte';
	import * as tf from '@tensorflow/tfjs';
	import * as blazeface from '@tensorflow-models/blazeface';
	import PQueue from 'p-queue';
	import { set, clear } from 'idb-keyval';

	let photos = $state<PhotoInfo[]>([]);
	let isProcessing = $state(false);
	let processedCount = $state(0);
	let totalCount = $state(0);
	let detector: blazeface.BlazeFaceModel | undefined;
	let detectorPromise: Promise<void> | undefined;
	let downloadQueue: PQueue;
	let faceDetectionQueue: PQueue;

	interface PhotoInfo {
		name: string;
		url: string;
		status: 'loading' | 'success' | 'error';
		objectUrl?: string;
		retryCount: number;
		cropData?: { x: number; y: number; width: number; height: number };
		faceDetectionStatus?: 'pending' | 'processing' | 'completed' | 'failed' | 'manual';
	}

	function initializeDetector() {
		if (!detectorPromise) {
			detectorPromise = (async () => {
				console.log('Initializing face detector...');
				await tf.setBackend('webgl');
				await tf.ready();
				detector = await blazeface.load();
				console.log('BlazeFace model loaded');
			})();
		}

		return detectorPromise;
	}

// Create a downscaled JPEG preview to reduce memory usage for UI rendering
async function createPreviewBlob(original: Blob, maxSide = 1200, quality = 0.85): Promise<Blob> {
	try {
		const bitmap = await createImageBitmap(original);
		let { width, height } = bitmap;
		const maxDim = Math.max(width, height);
		// If image is already at or below the threshold, keep it as-is
		if (maxDim <= maxSide) {
			bitmap.close();
			return original;
		}
		const scale = Math.min(1, maxSide / maxDim);
		const targetW = Math.max(1, Math.round(width * scale));
		const targetH = Math.max(1, Math.round(height * scale));

		const canvas = document.createElement('canvas');
		canvas.width = targetW;
		canvas.height = targetH;
		const ctx = canvas.getContext('2d');
		if (!ctx) throw new Error('Canvas 2D context unavailable');
		ctx.drawImage(bitmap, 0, 0, targetW, targetH);
		bitmap.close();

		const blob = await new Promise<Blob>((resolve, reject) =>
			canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/jpeg', quality)
		);
		return blob;
	} catch (e) {
		// Fallback to original if downscale fails
		return original;
	}
}

	// Force memory cleanup
	async function forceMemoryCleanup() {
		await tf.nextFrame(); // Wait for any pending GPU operations
		
		// Only run garbage collection if available, don't dispose variables
		if (typeof window !== 'undefined' && 'gc' in window) {
			(window as any).gc();
		}
	}

	async function processPhotosInParallel() {
		console.log('Starting processPhotos with queues...');
		isProcessing = true;
		processedCount = 0;

		try {
			// Clear previous session's images from IndexedDB
			await clear();
			console.log('Cleared IndexedDB.');
		} catch (e) {
			console.error('Could not clear IndexedDB:', e);
			return;
		}

		// Initialize queues with more conservative concurrency
		downloadQueue = new PQueue({ concurrency: 4 }); // Reduced from 5
		faceDetectionQueue = new PQueue({ concurrency: 1 }); // Keep at 1 for memory safety

		// When both queues are idle, we're done
		downloadQueue.on('idle', async () => {
			if (faceDetectionQueue.size === 0 && faceDetectionQueue.pending === 0) {
				await forceMemoryCleanup(); // Clean up memory when processing is complete
				isProcessing = false;
				console.log('All queues are idle. Processing complete.');
			}
		});
		faceDetectionQueue.on('idle', async () => {
			if (downloadQueue.size === 0 && downloadQueue.pending === 0) {
				await forceMemoryCleanup(); // Clean up memory when processing is complete
				isProcessing = false;
				console.log('All queues are idle. Processing complete.');
			}
		});

		const validRows = $sheetData.filter((row) => row._valid);
		const photoUrls = new Set<string>();
		const photoMap = new Map<string, any[]>();

		validRows.forEach((row: any) => {
			const photoUrl = row.pictureUrl;
			if (photoUrl && photoUrl.trim()) {
				const trimmedUrl = photoUrl.trim();
				photoUrls.add(trimmedUrl);
				if (!photoMap.has(trimmedUrl)) {
					photoMap.set(trimmedUrl, []);
				}
				photoMap.get(trimmedUrl)!.push(row);
			}
		});

		totalCount = photoUrls.size;
		console.log(`Found ${totalCount} unique photo URLs to process.`);

		photos = Array.from(photoUrls).map((url) => ({
			name: photoMap.get(url)![0].name,
			url,
			status: 'loading' as const,
			retryCount: 0,
			faceDetectionStatus: 'pending' as const
		}));

		// Add all photos to the download queue
		for (let i = 0; i < photos.length; i++) {
			downloadQueue.add(() => loadPhoto(i));
		}
	}

	// Initialize detector and process photos
	onMount(() => {
		ensureToken();
		initializeDetector(); // Start loading model
		if ($sheetData.length > 0 && $columnMapping.pictureUrl !== undefined) {
			console.log('Processing photos for gallery step');
			processPhotosInParallel();
		} else {
			console.log('No data to process: !');
		}
	});

	async function loadPhoto(index: number, isRetry = false) {
		const photo = photos[index];

		if (!isRetry) {
			photo.status = 'loading';
		}

		try {
			let blob: Blob;

			if (isGoogleDriveUrl(photo.url)) {
				// Download from Google Drive
				blob = await downloadDriveImage(photo.url);
			} else {
				// For direct URLs, convert to blob
				const response = await fetch(photo.url);
				blob = await response.blob();
			}

			// Check for HEIC/HEIF format. If so, start conversion but don't block.
			if (
				blob.type === 'image/heic' ||
				blob.type === 'image/heif' ||
				photo.url.toLowerCase().endsWith('.heic')
			) {
				photo.status = 'loading'; // Visually indicate something is happening
				// Don't await this, let it run in the background
				convertHeicPhoto(index, blob);
				return; // End loadPhoto here for HEIC, conversion will handle the rest
			}

			// For non-HEIC images, proceed as normal
			await processLoadedBlob(index, blob);
		} catch (error) {
			console.error(`Failed to load photo for ${photo.name}:`, error);
			photo.status = 'error';
			// Since this step failed, we still count it as "processed" to not stall the progress bar
			processedCount++;
		}
	}

	async function convertHeicPhoto(index: number, blob: Blob) {
		const photo = photos[index];
		try {
			// Dynamically import the browser-specific version of the library
			const { default: convert } = await import('heic-convert/browser');

			const inputBuffer = await blob.arrayBuffer();
			const outputBuffer = await convert({
				buffer: new Uint8Array(inputBuffer), // heic-convert expects a Uint8Array
				format: 'JPEG',
				quality: 0.9
			});

			const buffer = outputBuffer instanceof Uint8Array
				? outputBuffer.buffer.slice(outputBuffer.byteOffset, outputBuffer.byteOffset + outputBuffer.byteLength)
				: outputBuffer;
			const convertedBlob = new Blob([buffer as ArrayBuffer], { type: 'image/jpeg' });

			// Now that it's converted, process it like any other image
			await processLoadedBlob(index, convertedBlob);
		} catch (e) {
			console.error(`Failed to convert HEIC image for ${photo.name}:`, e);
			photo.status = 'error';
			// Since this step failed, we still count it as "processed" to not stall the progress bar
			processedCount++;
		}
	}

	async function processLoadedBlob(index: number, blob: Blob) {
		const photo = photos[index];
		try {
		// Downsize once and use this for storage, preview, and detection
		const resizedBlob = await createPreviewBlob(blob, 1600, 0.85);
		await set(photo.url, resizedBlob);
		const objectUrl = createImageObjectUrl(resizedBlob);

		// Test if downsized image loads properly
			await new Promise<void>((resolve, reject) => {
				const img = new Image();
				img.onload = () => resolve();
				img.onerror = (error) => {
			console.error(`Failed to load downsized image for ${photo.name}:`, error);
					reject(new Error('Failed to load image'));
				};
				img.src = objectUrl;
			});

			// Revoke any previous preview URL to avoid leaks
			if (photo.objectUrl && photo.objectUrl.startsWith('blob:') && photo.objectUrl !== objectUrl) {
				URL.revokeObjectURL(photo.objectUrl);
			}

			photo.objectUrl = objectUrl;
			photo.status = 'success';

			pictures.update((pics) => ({
				...pics,
				[photo.url]: {
					id: photo.url,
					url: objectUrl,
					downloaded: true,
					faceDetected: false,
					faceCount: 0
				}
			}));

	// Add face detection to its queue using the downsized image shown in UI
	faceDetectionQueue.add(() => detectFaceForPhoto(index));
		} catch (error) {
			console.error(`Failed to process blob for ${photo.name}:`, error);
			photo.status = 'error';
			// Since this step failed, we still count it as "processed" to not stall the progress bar
			processedCount++;
		}
	}

	async function detectFaceForPhoto(index: number) {
		const photo = photos[index];
		let imageTensor;
		try {
			await initializeDetector(); // Ensure detector is loaded
			if (!detector) {
				photo.faceDetectionStatus = 'failed';
				console.error('Face detector not available.');
				return;
			}

			photo.faceDetectionStatus = 'processing';
			const img = new Image();
			img.crossOrigin = 'anonymous';
	// Use the downsized UI image to keep coordinates aligned
	img.src = photo.objectUrl!;
			await new Promise((r, e) => {
				img.onload = r;
				img.onerror = e;
			});

		// Create tensor; run estimation (avoid tf.tidy here to not dispose returned tensors prematurely)
		imageTensor = tf.browser.fromPixels(img);
		const predictions: any[] = await detector.estimateFaces(imageTensor, false);

			if (predictions.length > 0) {
				const tensorToNumArray = (v: any): number[] => {
					if (Array.isArray(v)) return v as number[];
					if (typeof v === 'number') return [v];
					if (v && typeof v.dataSync === 'function') {
						const arr = Array.from(v.dataSync() as Float32Array);
						if (typeof v.dispose === 'function') v.dispose();
						return arr as number[];
					}
					return [];
				};
				const getProbability = (p: any) => tensorToNumArray(p)[0] ?? 0;

				const face = predictions.sort(
					(a, b) => getProbability(b.probability!) - getProbability(a.probability!)
				)[0];

				const topLeftArr = tensorToNumArray(face.topLeft);
				const bottomRightArr = tensorToNumArray(face.bottomRight);
				const topLeft = [topLeftArr[0], topLeftArr[1]] as [number, number];
				const bottomRight = [bottomRightArr[0], bottomRightArr[1]] as [number, number];

				let [x1, y1] = topLeft;
				let [x2, y2] = bottomRight;
				// Use natural sizes; detection ran on original if provided
				const scaleX = 1;
				const scaleY = 1;
				const faceWidth = (x2 - x1) * scaleX;
				const faceHeight = (y2 - y1) * scaleY;
				const faceCenterX = (x1 + (x2 - x1) / 2) * scaleX;
				const faceCenterY = (y1 + (y2 - y1) / 2) * scaleY;

				// Use the photo card aspect ratio from the selected card
				const photoDimensions = $selectedCard!.photo;
				const cropRatio = photoDimensions.width / photoDimensions.height;
				const offsetX = parseFloat(env.PUBLIC_FACE_OFFSET_X || '0.0');
				const offsetY = parseFloat(env.PUBLIC_FACE_OFFSET_Y || '0.0');
				const cropScale = parseFloat(env.PUBLIC_CROP_SCALE || '2.5');

				let cropWidth = faceWidth * cropScale;
				let cropHeight = cropWidth / cropRatio;

				if (cropWidth > img.naturalWidth || cropHeight > img.naturalHeight) {
					const widthRatio = img.naturalWidth / cropWidth;
					const heightRatio = img.naturalHeight / cropHeight;
					const scale = Math.min(widthRatio, heightRatio);
					cropWidth *= scale;
					cropHeight *= scale;
				}

				let centerX = faceCenterX + cropWidth * offsetX;
				let centerY = faceCenterY + cropHeight * offsetY;

				centerX = Math.max(cropWidth / 2, Math.min(centerX, img.naturalWidth - cropWidth / 2));
				centerY = Math.max(
					cropHeight / 2,
					Math.min(centerY, img.naturalHeight - cropHeight / 2)
				);

				const cropX = centerX - cropWidth / 2;
				const cropY = centerY - cropHeight / 2;

				const crop = {
					x: Math.round(Math.max(0, cropX)),
					y: Math.round(Math.max(0, cropY)),
					width: Math.round(cropWidth),
					height: Math.round(cropHeight)
				};
				photo.cropData = crop;
				photo.faceDetectionStatus = 'completed';

				cropRects.update((crops) => ({
					...crops,
					[photo.url]: crop
				}));

				pictures.update((pics) => ({
					...pics,
					[photo.url]: {
						...pics[photo.url],
						faceDetected: true,
						faceCount: predictions.length
					}
				}));
			} else {
				photo.faceDetectionStatus = 'failed';
			}
		} catch (error) {
			console.error(`Face detection failed for ${photo.name}:`, error);
			photo.faceDetectionStatus = 'failed';
		} finally {
			// Manually dispose of the input tensor to prevent memory leaks
			if (imageTensor) {
				imageTensor.dispose();
			}
			
			// Add a small delay to allow GPU memory to be freed before next operation
			await new Promise(resolve => setTimeout(resolve, 100));
			
			// This is the final step for a photo, so we increment the processed count here.
			processedCount++;
		}
	}

	async function retryPhoto(index: number) {
		const photo = photos[index];

		if (photo.retryCount >= 3) {
			return; // Max retries reached
		}

		photo.retryCount++;
		// Add the retry attempt back to the download queue
		downloadQueue.add(() => loadPhoto(index, true));
	}

	function handleCropUpdate(
		index: number,
		detail: { cropData: { x: number; y: number; width: number; height: number } }
	) {
		photos[index].cropData = detail.cropData;
		photos[index].faceDetectionStatus = 'manual';

		// Save updated crop data to store
		cropRects.update((crops) => ({
			...crops,
			[photos[index].url]: detail.cropData
		}));

		// No need to reassign photos array with $state reactivity
	}

	// Cleanup object URLs when component is destroyed
	function cleanupObjectUrls() {
		photos.forEach((photo) => {
			if (photo.objectUrl && photo.objectUrl.startsWith('blob:')) {
				URL.revokeObjectURL(photo.objectUrl);
			}
		});
	}

	const canProceed = $derived(() => {
		const hasPhotos = photos.length > 0;
		const allLoaded = photos.every((photo) => photo.status === 'success');
		const allCropped = photos.every((photo) => photo.cropData);

		return hasPhotos && allLoaded && allCropped;
	});

	// Cleanup on unmount using $effect
	$effect(() => {
		return () => {
			// Clear queues on component unmount to stop any ongoing processing
			if (downloadQueue) {
				downloadQueue.clear();
			}
			if (faceDetectionQueue) {
				faceDetectionQueue.clear();
			}
			cleanupObjectUrls();
			// Dispose the detector model if possible to release GPU/CPU memory
			if (detector && typeof (detector as any).dispose === 'function') {
				(detector as any).dispose();
			}
			detector = undefined;
		};
	});
</script>

<div class="p-6">
	<div class="mb-6">
		<h2 class="mb-2 text-xl font-semibold text-gray-900">Review & Crop Photos</h2>

		<p class="mb-4 text-sm text-gray-700">
			Photos are automatically cropped using face detection. Click the pen icon to manually adjust
			the crop area.
		</p>
	</div>

	<!-- Processing Status -->
	{#if isProcessing}
		<div class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
			<div class="flex items-center justify-between">
				<div class="flex items-center">
					<div
						class="mr-3 h-5 w-5 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"
					></div>
					<span class="text-sm text-blue-800"> Processing photos... </span>
				</div>
				<span class="text-sm text-blue-600">
					{processedCount} / {totalCount}
				</span>
			</div>

			{#if totalCount > 0}
				<div class="mt-3 h-2 w-full rounded-full bg-blue-200">
					<div
						class="h-2 rounded-full bg-blue-600 transition-all duration-300"
						style="width: {(processedCount / totalCount) * 100}%"
					></div>
				</div>
			{/if}
		</div>
	{/if}

	<!-- Summary Stats -->
	{#if !isProcessing && photos.length > 0}
		<div class="mb-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
			<h3 class="mb-3 text-sm font-medium text-gray-700">Processing Summary</h3>

			<div class="grid grid-cols-2 gap-4 text-sm md:grid-cols-5">
				<div class="text-center">
					<div class="text-2xl font-bold text-gray-900">{photos.length}</div>
					<div class="text-gray-600">Total Photos</div>
				</div>

				<div class="text-center">
					<div class="text-2xl font-bold text-green-600">
						{photos.filter((p) => p.status === 'success').length}
					</div>
					<div class="text-gray-600">Loaded</div>
				</div>

				<div class="text-center">
					<div class="text-2xl font-bold text-blue-600">
						{photos.filter((p) => p.faceDetectionStatus === 'completed').length}
					</div>
					<div class="text-gray-600">Auto-cropped</div>
				</div>

				<div class="text-center">
					<div class="text-2xl font-bold text-purple-600">
						{photos.filter((p) => p.cropData).length}
					</div>
					<div class="text-gray-600">Ready</div>
				</div>

				<div class="text-center">
					<div class="text-2xl font-bold text-red-600">
						{photos.filter((p) => p.status === 'error').length}
					</div>
					<div class="text-gray-600">Failed</div>
				</div>
			</div>
		</div>
	{/if}

	<!-- Photo Grid -->
	<div class="mb-6 overflow-hidden rounded-lg bg-white">
		{#if photos.length === 0 && !isProcessing}
			<div class="py-12 text-center">
				<svg
					class="mx-auto h-12 w-12 text-gray-400"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 002 2z"
					/>
				</svg>
				<h3 class="mt-2 text-sm font-medium text-gray-900">No photos found</h3>
				<p class="mt-1 text-sm text-gray-500">
					Go back to check your column mapping and selected rows.
				</p>
			</div>
		{:else}
			<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
				{#each photos as photo, index}
					<PhotoCard
						{photo}
						photoDimensions={$selectedCard!.photo}
						onCropUpdated={(e) => handleCropUpdate(index, e)}
						onRetry={() => retryPhoto(index)}
					/>
				{/each}
			</div>
		{/if}
	</div>

	<!-- Navigation -->
	<Navigator
		canProceed={canProceed()}
		{currentStep}
		textBack="Back to Card Selection"
		textForwardDisabled="Waiting for photos"
		textForwardEnabled={`Generate ${photos.filter((p) => p.status === 'success' && p.cropData).length} Cards`}
	/>
</div>
