import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist';
import { m } from '$lib/paraglide/messages.js';

export type LibLoader = () => Promise<readonly [typeof import('pdfjs-dist'), { default: string }]>;

const defaultLibLoader: LibLoader = () =>
	Promise.all([import('pdfjs-dist'), import('pdfjs-dist/build/pdf.worker.min.mjs?url')]);

// pdf.js 5.x decodes JBIG2 / CCITTFax / JPEG2000 images via WebAssembly and
// needs to know where the .wasm modules are served. Must be a directory URL
// with a trailing slash — pdf.js appends `jbig2.wasm` etc. Served from our own
// origin by vite-plugin-static-copy (see vite.config.ts). See issue #708.
const WASM_URL = '/pdfjs-wasm/';

export function createPdfRenderer(libLoader: LibLoader = defaultLibLoader) {
	// Reactive state — exposed via getters
	let currentPage = $state(1);
	let totalPages = $state(0);
	let scale = $state(1.5);
	let loading = $state(false);
	let error = $state<string | null>(null);
	let pdfjsReady = $state(false);

	// Internal mutable refs — NOT $state to avoid reactive loops
	let pdfDoc: PDFDocumentProxy | null = null;
	let canvasEl: HTMLCanvasElement | null = null;
	let textLayerEl: HTMLDivElement | null = null;
	let renderTask: RenderTask | null = null;
	let textLayerInstance: { cancel: () => void } | null = null;
	let pdfjsLib: typeof import('pdfjs-dist') | null = null;

	async function init(): Promise<void> {
		if (pdfjsReady) return;
		const [lib, { default: workerUrl }] = await libLoader();
		lib.GlobalWorkerOptions.workerSrc = workerUrl;
		pdfjsLib = lib;
		pdfjsReady = true;
	}

	function setElements(canvas: HTMLCanvasElement, textLayer: HTMLDivElement): void {
		canvasEl = canvas;
		textLayerEl = textLayer;
	}

	async function loadDocument(src: string): Promise<void> {
		if (!pdfjsLib) return;
		loading = true;
		error = null;
		pdfDoc = null;
		currentPage = 1;
		totalPages = 0;

		try {
			const loadingTask = pdfjsLib.getDocument({ url: src, wasmUrl: WASM_URL });
			const doc = await loadingTask.promise;
			pdfDoc = doc;
			totalPages = doc.numPages;
		} catch {
			// Never surface the raw pdf.js message — show a localized failure
			// that routes into the viewer's error UI (message + download link).
			error = m.doc_render_failed();
		} finally {
			loading = false;
		}
	}

	async function renderCurrentPage(): Promise<void> {
		if (!pdfjsLib || !canvasEl || !textLayerEl || !pdfDoc) return;

		if (renderTask) {
			renderTask.cancel();
			renderTask = null;
		}
		if (textLayerInstance) {
			textLayerInstance.cancel();
			textLayerInstance = null;
		}

		let page;
		try {
			page = await pdfDoc.getPage(currentPage);
		} catch {
			return;
		}

		const dpr = window.devicePixelRatio || 1;
		const viewport = page.getViewport({ scale: scale * dpr });

		const canvas = canvasEl;
		const ctx = canvas.getContext('2d');
		if (!ctx) return;

		canvas.width = viewport.width;
		canvas.height = viewport.height;
		canvas.style.width = `${viewport.width / dpr}px`;
		canvas.style.height = `${viewport.height / dpr}px`;

		const task = page.render({ canvas, canvasContext: ctx, viewport });
		renderTask = task;

		try {
			await task.promise;
		} catch (e: unknown) {
			if (
				typeof e === 'object' &&
				e !== null &&
				'name' in e &&
				(e as { name: string }).name === 'RenderingCancelledException'
			)
				return;
			// A real decode/render failure (e.g. a wasm decoder that could not
			// initialise) — surface a localized message instead of leaving a
			// silent blank canvas. Never leak the raw pdf.js error text.
			renderTask = null;
			error = m.doc_render_failed();
			return;
		}
		renderTask = null;

		const textDiv = textLayerEl;
		if (!textDiv) return;
		textDiv.innerHTML = '';
		textDiv.style.width = `${viewport.width / dpr}px`;
		textDiv.style.height = `${viewport.height / dpr}px`;

		const tl = new pdfjsLib.TextLayer({
			textContentSource: page.streamTextContent(),
			container: textDiv,
			viewport
		});
		textLayerInstance = tl;
		try {
			await tl.render();
		} catch {
			// cancelled
		}
	}

	async function prerender(): Promise<void> {
		if (!pdfDoc) return;
		const neighbors = [currentPage - 1, currentPage + 1].filter(
			(n) => n >= 1 && n <= (pdfDoc?.numPages ?? 0)
		);
		for (const n of neighbors) {
			try {
				await pdfDoc.getPage(n);
			} catch {
				// ignore
			}
		}
	}

	function prevPage(): void {
		if (currentPage > 1) currentPage -= 1;
	}

	function nextPage(): void {
		if (currentPage < totalPages) currentPage += 1;
	}

	function goToPage(n: number): void {
		if (n >= 1 && n <= totalPages) currentPage = n;
	}

	function zoomIn(): void {
		scale += 0.25;
	}

	function zoomOut(): void {
		if (scale > 0.5) scale -= 0.25;
	}

	function destroy(): void {
		if (renderTask) {
			renderTask.cancel();
			renderTask = null;
		}
		if (textLayerInstance) {
			textLayerInstance.cancel();
			textLayerInstance = null;
		}
		pdfDoc?.destroy();
		pdfDoc = null;
	}

	return {
		get currentPage() {
			return currentPage;
		},
		get totalPages() {
			return totalPages;
		},
		get scale() {
			return scale;
		},
		get loading() {
			return loading;
		},
		get error() {
			return error;
		},
		get isLoaded() {
			return totalPages > 0;
		},
		get pdfjsReady() {
			return pdfjsReady;
		},
		setElements,
		init,
		loadDocument,
		renderCurrentPage,
		prerender,
		prevPage,
		nextPage,
		goToPage,
		zoomIn,
		zoomOut,
		destroy
	};
}
