<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import { goto } from '$app/navigation';
import { onDestroy, onMount, untrack } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
import { getConfirmService } from '$lib/shared/services/confirm.svelte.js';
import type { ConfirmService } from '$lib/shared/services/confirm.svelte.js';
import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte';
import BulkDropZone from './BulkDropZone.svelte';
import FileSwitcherStrip from './FileSwitcherStrip.svelte';
import type { FileEntry } from './FileSwitcherStrip.svelte';
import ScopeCard from './ScopeCard.svelte';
import UploadSaveBar from './UploadSaveBar.svelte';
import WhoWhenSection from './WhoWhenSection.svelte';
import DescriptionSection from './DescriptionSection.svelte';
import PdfViewer from '$lib/document/viewer/PdfViewer.svelte';
import { bulkTitleFromFilename } from '$lib/document/filename';
import type { Tag } from '$lib/tag/TagInput.svelte';
import type { components } from '$lib/generated/api';
import { csrfFetch } from '$lib/shared/cookies';

type Person = components['schemas']['Person'];

// Mirrors the backend `DocumentBatchSummary` JSON shape one-to-one — the route
// passes the parsed `/api/documents/batch-metadata` response straight in, so
// the field names must match what the backend actually serializes (id, not
// documentId). The FileEntry built from each summary still uses both `id` and
// `documentId` so the save handler can drive the PATCH payload by UUID.
export type BulkEditEntry = {
	id: string;
	title: string;
	pdfUrl: string;
};

// Optional — not available in unit tests that don't provide CONFIRM_KEY context.
let _confirmService: ConfirmService | null;
try {
	_confirmService = getConfirmService();
} catch {
	_confirmService = null;
}

let {
	mode = 'upload',
	initialSenderId = '',
	initialSenderName = '',
	initialReceivers = [],
	initialEditEntries = []
}: {
	mode?: 'upload' | 'edit';
	initialSenderId?: string;
	initialSenderName?: string;
	initialReceivers?: Person[];
	initialEditEntries?: BulkEditEntry[];
} = $props();

// --- File state ---
let files = new SvelteMap<string, FileEntry>();
let activeId = $state<string | null>(null);
let chunkProgress = $state<{ done: number; total: number } | undefined>(undefined);
let saving = $state(false);
// Partial-failure surface: when set, the last save aborted at chunk N of M.
let partialSaved = $state<{ done: number; total: number } | null>(null);

// --- Shared metadata ---
let senderId = $state(untrack(() => initialSenderId));
let selectedReceivers = $state<Person[]>(untrack(() => initialReceivers));
let dateIso = $state('');
let tags = $state<Tag[]>([]);
// Bulk-edit only — replace-on-non-blank semantics.
let archiveBox = $state('');
let archiveFolder = $state('');

// Hydrate edit-mode entries on mount. The IDs in bulkSelectionStore drive the
// fetch upstream in the route — by the time this layout mounts, the metadata
// has already been resolved into `initialEditEntries`. Wrapped in onMount so
// the SvelteMap mutation is unambiguously tied to instance lifecycle, not to
// the script body's first execution (Felix C4 cycle 3).
onMount(() => {
	if (mode !== 'edit') return;
	for (const entry of untrack(() => initialEditEntries)) {
		files.set(entry.id, {
			id: entry.id,
			documentId: entry.id,
			title: entry.title,
			status: 'idle',
			previewUrl: entry.pdfUrl
		});
		if (!activeId) activeId = entry.id;
	}
});

// --- Derived ---
const isMulti = $derived(files.size >= 2);
const activeFile = $derived(activeId ? files.get(activeId) : null);

// --- File management ---
function addFiles(newFiles: File[]) {
	for (const file of newFiles) {
		const id = crypto.randomUUID();
		const title = bulkTitleFromFilename(file.name);
		const previewUrl = URL.createObjectURL(file);
		files.set(id, { id, file, title, status: 'idle', previewUrl });
		if (!activeId) activeId = id;
	}
}

function removeFile(id: string) {
	const entry = files.get(id);
	if (entry?.previewUrl) URL.revokeObjectURL(entry.previewUrl);
	files.delete(id);
	if (activeId === id) {
		activeId = files.keys().next().value ?? null;
	}
}

function setTitle(id: string, title: string) {
	const entry = files.get(id);
	if (entry) files.set(id, { ...entry, title });
}

function discardAll() {
	for (const entry of files.values()) {
		if (entry.previewUrl) URL.revokeObjectURL(entry.previewUrl);
	}
	files.clear();
	activeId = null;
	chunkProgress = undefined;
}

async function handleDiscard() {
	if (_confirmService) {
		const ok = await _confirmService.confirm({
			title: m.bulk_discard_all(),
			body: m.bulk_discard_confirm(),
			destructive: true
		});
		if (!ok) return;
	}
	if (mode === 'edit') {
		// In edit mode the file map IS the user's bulk selection — discarding
		// must clear the upstream store and bounce back to the list, otherwise
		// the user is left on /documents/bulk-edit with an empty form and a
		// stale count in the bottom bar (issue #225 Bulk-Edit Panel table).
		bulkSelectionStore.clear();
		discardAll();
		await goto('/documents');
		return;
	}
	discardAll();
}

onDestroy(() => {
	for (const entry of files.values()) {
		if (entry.previewUrl) URL.revokeObjectURL(entry.previewUrl);
	}
});

// --- Save (upload mode) ---
async function saveUpload() {
	const entries = Array.from(files.values());
	// 10 files per request keeps multipart bodies well under typical reverse-proxy limits (e.g. nginx default 1 MB client_max_body_size per PDF).
	const chunkSize = 10;
	const chunks: FileEntry[][] = [];
	for (let i = 0; i < entries.length; i += chunkSize) {
		chunks.push(entries.slice(i, i + chunkSize));
	}
	chunkProgress = { done: 0, total: chunks.length };

	let hadErrors = false;
	for (let i = 0; i < chunks.length; i++) {
		const chunk = chunks[i];
		const formData = new FormData();
		chunk.forEach((entry) => entry.file && formData.append('files', entry.file));
		const metadata = {
			titles: chunk.map((e) => e.title),
			senderId: senderId || null,
			receiverIds: selectedReceivers.map((r) => r.id),
			documentDate: dateIso || null,
			tagNames: tags.map((t) => t.name)
		};
		formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
		// Raw fetch is intentional: SvelteKit form actions can't stream chunked
		// FormData with per-chunk progress. Session cookie is sent automatically
		// by the browser for same-origin requests.
		try {
			const res = await csrfFetch('/api/documents/quick-upload', {
				method: 'POST',
				body: formData
			});
			const body = await res.json().catch(() => ({ errors: [] }));
			const errorFilenames = new Set<string>(
				(body.errors ?? []).map((err: { filename: string }) => err.filename)
			);
			if (!res.ok || errorFilenames.size > 0) {
				hadErrors = true;
				for (const entry of chunk) {
					const filename = entry.file?.name;
					const isError = errorFilenames.size > 0 && filename ? errorFilenames.has(filename) : true;
					if (isError) {
						const e = files.get(entry.id);
						if (e) files.set(entry.id, { ...e, status: 'error' });
					}
				}
			}
		} catch {
			hadErrors = true;
			for (const entry of chunk) {
				const e = files.get(entry.id);
				if (e) files.set(entry.id, { ...e, status: 'error' });
			}
		}
		chunkProgress = { done: i + 1, total: chunks.length };
	}
	if (!hadErrors) goto('/documents');
}

// --- Save (edit mode) ---
async function saveBulkEdit() {
	const entries = Array.from(files.values());
	const ids = entries.map((e) => e.documentId).filter((x): x is string => !!x);

	// PATCH cap matches backend: 500 IDs per request. Sequential, stop on chunk
	// failure so the user sees a deterministic "X of N saved" outcome.
	const chunkSize = 500;
	const chunks: string[][] = [];
	for (let i = 0; i < ids.length; i += chunkSize) {
		chunks.push(ids.slice(i, i + chunkSize));
	}
	chunkProgress = { done: 0, total: chunks.length };
	partialSaved = null;

	const dto = {
		tagNames: tags.map((t) => t.name),
		senderId: senderId || null,
		receiverIds: selectedReceivers.map((r) => r.id),
		archiveBox: archiveBox || null,
		archiveFolder: archiveFolder || null
	};

	for (let i = 0; i < chunks.length; i++) {
		const chunk = chunks[i];
		try {
			const res = await csrfFetch('/api/documents/bulk', {
				method: 'PATCH',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ ...dto, documentIds: chunk })
			});
			if (!res.ok) {
				// Network/server failure: the chunk did not apply. Mark its entries
				// as errored, surface partial-save state, and stop.
				for (const id of chunk) {
					const e = files.get(id);
					if (e) files.set(id, { ...e, status: 'error' });
				}
				partialSaved = { done: i, total: chunks.length };
				return;
			}
			const body = (await res.json().catch(() => null)) as {
				updated: number;
				errors: { id: string; message: string }[];
			} | null;
			if (body && body.errors && body.errors.length > 0) {
				for (const err of body.errors) {
					const e = files.get(err.id);
					if (e) files.set(err.id, { ...e, status: 'error' });
				}
			}
		} catch {
			for (const id of chunk) {
				const e = files.get(id);
				if (e) files.set(id, { ...e, status: 'error' });
			}
			partialSaved = { done: i, total: chunks.length };
			return;
		}
		chunkProgress = { done: i + 1, total: chunks.length };
	}

	const stillErrored = Array.from(files.values()).some((e) => e.status === 'error');
	if (!stillErrored) {
		bulkSelectionStore.clear();
		goto('/documents');
	}
}

async function save() {
	if (saving) return;
	saving = true;
	try {
		if (mode === 'edit') {
			await saveBulkEdit();
		} else {
			await saveUpload();
		}
	} finally {
		saving = false;
	}
}

async function retrySave() {
	partialSaved = null;
	await save();
}
</script>

<div class="fixed inset-x-0 bottom-0 flex flex-col" style="top: var(--header-height)">
	<!-- Topbar -->
	<div class="flex shrink-0 items-center gap-3 border-b border-line bg-surface px-6 py-3">
		<a
			href="/documents"
			class="flex items-center gap-1.5 text-xs font-bold tracking-widest text-ink-3 uppercase hover:text-ink"
		>
			<svg
				class="h-3.5 w-3.5"
				fill="none"
				stroke="currentColor"
				viewBox="0 0 24 24"
				aria-hidden="true"
			>
				<path
					stroke-linecap="round"
					stroke-linejoin="round"
					stroke-width="2"
					d="M10 19l-7-7m0 0l7-7m-7 7h18"
				/>
			</svg>
			{m.btn_back_to_overview()}
		</a>
		<span class="text-ink-3" aria-hidden="true">·</span>
		<span class="font-serif text-sm font-bold text-ink">
			{#if mode === 'edit'}
				{m.bulk_edit_topbar_title()}
			{:else}
				{isMulti ? m.bulk_title_multi() : m.bulk_title_single()}
			{/if}
		</span>
		{#if isMulti}
			<span class="ml-auto flex items-center gap-3">
				<span class="rounded-[2px] bg-accent px-2 py-0.5 text-xs font-bold text-primary">
					{#if mode === 'edit'}
						{m.bulk_edit_count_pill({ count: files.size })}
					{:else}
						{m.bulk_count_pill({ count: files.size })}
					{/if}
				</span>
				<button
					type="button"
					data-testid="discard-all-btn"
					onclick={handleDiscard}
					class="text-xs font-medium text-red-600/70 hover:text-red-700"
				>
					{m.bulk_discard_all()}
				</button>
			</span>
		{/if}
	</div>

	<!-- Split panel -->
	<div class="flex flex-1 overflow-hidden">
		<!-- Left: PDF preview / drop zone (55%) -->
		<div class="relative flex flex-[55] flex-col overflow-hidden border-r border-line bg-pdf-bg">
			{#if mode === 'upload' && files.size === 0}
				<!-- N=0: centred drop-zone box fills the panel (upload only) -->
				<BulkDropZone onFilesAdded={addFiles} />
			{:else if files.size > 0}
				<!-- PDF preview: blob URL in upload mode, server URL in edit mode -->
				<div class="relative flex-1 overflow-hidden">
					{#if activeFile}
						<PdfViewer url={activeFile.previewUrl} />
					{/if}
				</div>
				{#if isMulti}
					<!-- File switcher strip pinned to bottom of left panel -->
					<FileSwitcherStrip
						files={Array.from(files.values())}
						activeId={activeId ?? ''}
						onSelect={(id) => (activeId = id)}
						onRemove={removeFile}
					/>
				{/if}
			{/if}
		</div>

		<!-- Right: metadata form (45%) -->
		<div class="flex flex-[45] flex-col overflow-hidden">
			<!-- Scrollable form area — greyed out and non-interactive when no files selected -->
			<div
				class="flex-1 space-y-4 overflow-y-auto p-4 transition-opacity"
				class:opacity-60={files.size === 0}
				class:pointer-events-none={files.size === 0}
			>
				{#if mode === 'edit'}
					<!-- Onboarding callout: tells the user that empty fields are skipped
						 and that tags/receivers are added rather than replaced.
						 No aria-label — role=note + the visible text content is
						 self-describing; an aria-label would override that text for
						 AT users on non-DE locales. -->
					<div
						role="note"
						data-testid="bulk-edit-callout"
						class="rounded-sm border border-accent/40 bg-accent/15 px-4 py-3 text-sm text-ink-2"
					>
						{m.bulk_edit_hint()}
					</div>
				{/if}

				{#if isMulti}
					<!-- N≥2: per-file card (title) + shared card (metadata) -->
					<ScopeCard variant="per-file">
						{#if activeFile}
							{#if mode === 'edit'}
								<div data-testid="readonly-title">
									<span class="mb-1 block text-xs font-medium tracking-widest text-ink-2 uppercase">
										{m.form_label_title()}
									</span>
									<p class="font-serif text-base text-ink">{activeFile.title}</p>
								</div>
							{:else}
								<label class="block">
									<span class="mb-1 block text-xs font-medium tracking-widest text-ink-2 uppercase">
										{m.form_label_title()} <span class="text-danger">*</span>
									</span>
									<input
										type="text"
										value={activeFile.title}
										oninput={(e) =>
											setTitle(activeId!, (e.currentTarget as HTMLInputElement).value)}
										class="block w-full rounded-sm border border-line bg-surface p-2 text-sm focus:border-accent focus:outline-none"
									/>
								</label>
							{/if}
						{/if}
					</ScopeCard>

					<ScopeCard variant="shared" count={files.size}>
						<WhoWhenSection
							bind:senderId={senderId}
							bind:selectedReceivers={selectedReceivers}
							bind:dateIso={dateIso}
							initialSenderName={initialSenderName}
							hideDate={mode === 'edit'}
							editMode={mode === 'edit'}
						/>
						<DescriptionSection
							bind:tags={tags}
							bind:archiveBox={archiveBox}
							bind:archiveFolder={archiveFolder}
							hideTitle
							editMode={mode === 'edit'}
						/>
					</ScopeCard>
				{:else}
					<!-- N=0 (disabled placeholder) or N=1 (active): title + shared form -->
					<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
						{#if mode === 'edit' && activeFile}
							<div data-testid="readonly-title">
								<span class="mb-1 block text-xs font-bold tracking-widest text-ink-3 uppercase">
									{m.form_label_title()}
								</span>
								<p class="font-serif text-base text-ink">{activeFile.title}</p>
							</div>
						{:else}
							<label class="block">
								<span class="mb-1 block text-xs font-bold tracking-widest text-ink-3 uppercase">
									{m.form_label_title()} <span class="text-danger">*</span>
								</span>
								{#if activeFile}
									<input
										type="text"
										value={activeFile.title}
										oninput={(e) =>
											setTitle(activeId!, (e.currentTarget as HTMLInputElement).value)}
										class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
									/>
								{:else}
									<input
										type="text"
										disabled
										placeholder="—"
										class="block w-full rounded border border-line p-2 text-sm text-ink-3 shadow-sm"
									/>
								{/if}
							</label>
						{/if}
					</div>

					<WhoWhenSection
						bind:senderId={senderId}
						bind:selectedReceivers={selectedReceivers}
						bind:dateIso={dateIso}
						initialSenderName={initialSenderName}
						hideDate={mode === 'edit'}
						editMode={mode === 'edit'}
					/>
					<DescriptionSection
						bind:tags={tags}
						bind:archiveBox={archiveBox}
						bind:archiveFolder={archiveFolder}
						hideTitle
						editMode={mode === 'edit'}
					/>
				{/if}

				{#if partialSaved}
					<div
						role="alert"
						data-testid="bulk-edit-partial-failure"
						class="rounded-sm border border-danger/40 bg-danger/10 px-4 py-3 text-sm text-danger"
					>
						<p class="font-medium">
							{m.bulk_edit_save_partial({
								done: partialSaved.done,
								total: partialSaved.total
							})}
						</p>
						<button
							type="button"
							onclick={retrySave}
							class="mt-2 inline-flex items-center bg-primary px-4 py-2 text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90"
						>
							{m.bulk_edit_retry()}
						</button>
					</div>
				{/if}
			</div>

			<!-- Action bar: always visible at bottom of right panel -->
			<UploadSaveBar
				fileCount={files.size}
				chunkProgress={chunkProgress}
				onSave={save}
				onDiscard={handleDiscard}
				disabled={saving}
				editMode={mode === 'edit'}
			/>
		</div>
	</div>
</div>
