/* eslint-disable svelte/prefer-svelte-reactivity -- the Date instances inside
   lastEditedAt's $derived are scope-local to one computation; they're never
   stored on $state. */
import type { TranscriptionBlockData, PersonMention } from '$lib/shared/types';
import { makeCsrfFetch } from '$lib/shared/cookies';
import { saveBlockWithConflictRetry } from './saveBlockWithConflictRetry';
import { BlockConflictResolvedError } from './blockConflictMerge';

type DrawRect = {
	x: number;
	y: number;
	width: number;
	height: number;
	pageNumber: number;
};

export interface TranscriptionBlocksOptions {
	documentId: () => string;
	fetchImpl?: typeof fetch;
}

export interface TranscriptionBlocksController {
	readonly blocks: TranscriptionBlockData[];
	readonly hasBlocks: boolean;
	readonly blockNumbers: Record<string, number>;
	readonly lastEditedAt: string | null;
	readonly annotationReloadKey: number;

	load(): Promise<void>;
	save(blockId: string, text: string, mentionedPersons: PersonMention[]): Promise<void>;
	delete(blockId: string): Promise<void>;
	reviewToggle(blockId: string): Promise<void>;
	markAllReviewed(): Promise<void>;
	createFromDraw(rect: DrawRect): Promise<TranscriptionBlockData | null>;
	toggleTrainingLabel(label: string, enrolled: boolean): Promise<void>;
	deleteAnnotation(annotationId: string): Promise<void>;
	findByAnnotationId(annotationId: string): TranscriptionBlockData | undefined;
	bumpAnnotationReloadKey(): void;
}

export function createTranscriptionBlocks(
	options: TranscriptionBlocksOptions
): TranscriptionBlocksController {
	const { documentId } = options;
	const fetchImpl = makeCsrfFetch(options.fetchImpl ?? fetch);

	let blocks = $state<TranscriptionBlockData[]>([]);
	let annotationReloadKey = $state(0);

	const blockNumbers = $derived(
		Object.fromEntries(
			[...blocks].sort((a, b) => a.sortOrder - b.sortOrder).map((b, i) => [b.annotationId, i + 1])
		)
	);

	const hasBlocks = $derived(blocks.length > 0);

	const lastEditedAt = $derived.by(() => {
		if (blocks.length === 0) return null;
		const dates = blocks.filter((b) => b.updatedAt).map((b) => new Date(b.updatedAt!).getTime());
		if (dates.length === 0) return null;
		return new Date(Math.max(...dates)).toISOString();
	});

	async function load(): Promise<void> {
		const id = documentId();
		if (!id) return;
		try {
			const res = await fetchImpl(`/api/documents/${id}/transcription-blocks`);
			if (res.ok) {
				blocks = (await res.json()) as TranscriptionBlockData[];
			}
		} catch (e) {
			console.error('Failed to load transcription blocks:', e);
		}
	}

	async function save(
		blockId: string,
		text: string,
		mentionedPersons: PersonMention[]
	): Promise<void> {
		try {
			const updated = await saveBlockWithConflictRetry({
				fetchImpl,
				documentId: documentId(),
				blockId,
				text,
				mentionedPersons
			});
			blocks = blocks.map((b) => (b.id === blockId ? updated : b));
		} catch (err) {
			if (err instanceof BlockConflictResolvedError && err.merged) {
				blocks = blocks.map((b) => (b.id === blockId ? err.merged! : b));
			}
			throw err;
		}
	}

	async function deleteBlock(blockId: string): Promise<void> {
		const res = await fetchImpl(`/api/documents/${documentId()}/transcription-blocks/${blockId}`, {
			method: 'DELETE'
		});
		if (!res.ok) throw new Error('Delete failed');
		blocks = blocks.filter((b) => b.id !== blockId);
		annotationReloadKey++;
	}

	async function reviewToggle(blockId: string): Promise<void> {
		const res = await fetchImpl(
			`/api/documents/${documentId()}/transcription-blocks/${blockId}/review`,
			{ method: 'PUT' }
		);
		if (!res.ok) return;
		const updated = (await res.json()) as TranscriptionBlockData;
		blocks = blocks.map((b) => (b.id === blockId ? updated : b));
	}

	async function markAllReviewed(): Promise<void> {
		const res = await fetchImpl(`/api/documents/${documentId()}/transcription-blocks/review-all`, {
			method: 'PUT'
		});
		if (!res.ok) {
			const body = await res.json().catch(() => ({}));
			// Never render body.message — route through getErrorMessage() to prevent leaking backend internals
			throw new Error((body as { code?: string })?.code ?? 'INTERNAL_ERROR');
		}
		const updated = (await res.json()) as { id: string; reviewed: boolean }[];
		for (const b of updated) {
			const existing = blocks.find((x) => x.id === b.id);
			if (existing) existing.reviewed = b.reviewed;
		}
	}

	async function createFromDraw(rect: DrawRect): Promise<TranscriptionBlockData | null> {
		try {
			const res = await fetchImpl(`/api/documents/${documentId()}/transcription-blocks`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({
					pageNumber: rect.pageNumber,
					x: rect.x,
					y: rect.y,
					width: rect.width,
					height: rect.height,
					text: '',
					label: null
				})
			});
			if (res.ok) {
				const created = (await res.json()) as TranscriptionBlockData;
				blocks = [...blocks, created];
				return created;
			}
			return null;
		} catch (e) {
			console.error('Failed to create transcription block:', e);
			return null;
		}
	}

	async function toggleTrainingLabel(label: string, enrolled: boolean): Promise<void> {
		const res = await fetchImpl(`/api/documents/${documentId()}/training-labels`, {
			method: 'PATCH',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ label, enrolled })
		});
		if (!res.ok) throw new Error('Failed to update training label');
	}

	async function deleteAnnotation(annotationId: string): Promise<void> {
		const block = blocks.find((b) => b.annotationId === annotationId);
		if (block) {
			await deleteBlock(block.id);
			return;
		}
		const res = await fetchImpl(`/api/documents/${documentId()}/annotations/${annotationId}`, {
			method: 'DELETE'
		});
		if (!res.ok) throw new Error('Delete annotation failed');
		annotationReloadKey++;
	}

	function findByAnnotationId(annotationId: string): TranscriptionBlockData | undefined {
		return blocks.find((b) => b.annotationId === annotationId);
	}

	function bumpAnnotationReloadKey(): void {
		annotationReloadKey++;
	}

	return {
		get blocks() {
			return blocks;
		},
		get hasBlocks() {
			return hasBlocks;
		},
		get blockNumbers() {
			return blockNumbers;
		},
		get lastEditedAt() {
			return lastEditedAt;
		},
		get annotationReloadKey() {
			return annotationReloadKey;
		},
		load,
		save,
		delete: deleteBlock,
		reviewToggle,
		markAllReviewed,
		createFromDraw,
		toggleTrainingLabel,
		deleteAnnotation,
		findByAnnotationId,
		bumpAnnotationReloadKey
	};
}
