<script lang="ts">
import { tick } from 'svelte';
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
import { csrfFetch } from '$lib/shared/cookies';
import { getErrorMessage } from '$lib/shared/errors';
import DocumentPickerDropdown from '$lib/document/DocumentPickerDropdown.svelte';
import type { DocumentOption } from '$lib/document/documentTypeahead';

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

interface Props {
	geschichteId: string;
	items?: JourneyItemView[];
}

let { geschichteId, items: initialItems = [] }: Props = $props();

const uid = $props.id();
const pickerInputId = `story-doc-picker-${uid}`;

// Initial-state snapshot — the panel owns the list after mount and updates
// it from API responses; the parent re-mounts to reset (same contract as
// GeschichteEditor/JourneyEditor).
// svelte-ignore state_referenced_locally
let items: JourneyItemView[] = $state([...initialItems].sort((a, b) => a.position - b.position));
let errorMessage = $state('');
let liveAnnounce = $state('');
let announceTimer: ReturnType<typeof setTimeout> | null = null;
let sectionEl: HTMLElement | null = $state(null);

$effect(() => () => {
	if (announceTimer) clearTimeout(announceTimer);
});

const alreadyAddedIds = $derived(
	new Set(items.filter((i) => i.document).map((i) => i.document!.id))
);

function announce(message: string) {
	liveAnnounce = message;
	if (announceTimer) clearTimeout(announceTimer);
	announceTimer = setTimeout(() => {
		liveAnnounce = '';
		announceTimer = null;
	}, 500);
}

function itemTitle(item: JourneyItemView): string {
	return item.document?.title ?? m.geschichte_documents_deleted_placeholder();
}

/** Maps a failed mutation to a user-facing message — story wording for the
 *  two journey-flavored 409s, whose generic messages say "Lesereise". */
async function failureMessage(res: Response): Promise<string> {
	const code = (await res.json().catch(() => ({})))?.code;
	if (code === 'JOURNEY_AT_CAPACITY') return m.geschichte_documents_capacity();
	if (code === 'JOURNEY_DOCUMENT_ALREADY_ADDED') return m.geschichte_documents_duplicate();
	return code ? getErrorMessage(code) : m.journey_mutation_error_reload();
}

/** Pessimistic append — the list updates only with the server's response. */
async function handleAdd(doc: DocumentOption) {
	errorMessage = '';
	try {
		const res = await csrfFetch(`/api/geschichten/${geschichteId}/items`, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ documentId: doc.id })
		});
		if (!res.ok) {
			errorMessage = await failureMessage(res);
			return;
		}
		const newItem: JourneyItemView = await res.json();
		items = [...items, newItem];
		announce(m.geschichte_documents_added_announce({ title: itemTitle(newItem) }));
	} catch (e) {
		console.error('Story document add failed', e);
		errorMessage = m.journey_mutation_error_reload();
	}
}

/** The removed row's button leaves the DOM — without this, focus drops to
 *  <body> and a keyboard user is teleported to page top. */
async function moveFocusAfterRemove(removedIdx: number) {
	await tick();
	if (items.length === 0) {
		sectionEl?.querySelector<HTMLElement>(`#${pickerInputId}`)?.focus();
		return;
	}
	const target = items[Math.max(removedIdx - 1, 0)];
	sectionEl
		?.querySelector<HTMLElement>(`[data-item-id="${CSS.escape(target.id)}"] [data-remove-btn]`)
		?.focus();
}

/** Optimistic removal with snapshot-and-rollback. */
async function handleRemove(item: JourneyItemView) {
	const idx = items.findIndex((i) => i.id === item.id);
	const prev = [...items];
	errorMessage = '';
	items = items.filter((i) => i.id !== item.id);
	await moveFocusAfterRemove(idx);
	try {
		const res = await csrfFetch(`/api/geschichten/${geschichteId}/items/${item.id}`, {
			method: 'DELETE'
		});
		if (!res.ok) {
			items = prev;
			await tick();
			sectionEl
				?.querySelector<HTMLElement>(`[data-item-id="${CSS.escape(item.id)}"] [data-remove-btn]`)
				?.focus();
			errorMessage = await failureMessage(res);
			return;
		}
		announce(m.geschichte_documents_removed_announce({ title: itemTitle(item) }));
	} catch (e) {
		console.error('Story document remove failed', e);
		items = prev;
		await tick();
		sectionEl
			?.querySelector<HTMLElement>(`[data-item-id="${CSS.escape(item.id)}"] [data-remove-btn]`)
			?.focus();
		errorMessage = m.journey_mutation_error_reload();
	}
}
</script>

<!-- Screen-reader live region for add/remove confirmations -->
<div aria-live="polite" aria-atomic="true" class="sr-only">{liveAnnounce}</div>

<details open class="sm:contents">
	<summary
		class="flex min-h-[44px] cursor-pointer items-center px-4 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase sm:hidden"
	>
		{m.geschichte_documents_heading()}
	</summary>
	<section bind:this={sectionEl} class="rounded border border-line bg-surface p-4 shadow-sm">
		<h2
			class="mb-2 hidden font-sans text-xs font-bold tracking-widest text-ink-3 uppercase sm:block"
		>
			{m.geschichte_documents_heading()}
		</h2>
		<p class="mb-3 font-sans text-xs text-ink-3">{m.geschichte_documents_hint()}</p>

		{#if errorMessage}
			<p
				role="alert"
				class="mb-3 flex items-start gap-2 rounded border border-danger bg-danger/10 px-3 py-2 font-sans text-sm text-danger"
			>
				<svg
					class="mt-0.5 h-4 w-4 flex-none"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
					stroke-width="2"
					aria-hidden="true"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
					/>
				</svg>
				{errorMessage}
			</p>
		{/if}

		{#if items.length === 0}
			<p class="mb-3 font-sans text-xs text-ink-3">{m.geschichte_documents_empty()}</p>
		{:else}
			<ul class="m-0 mb-3 flex list-none flex-col p-0">
				{#each items as item (item.id)}
					<li
						data-item-id={item.id}
						class="flex items-center justify-between gap-2 border-b border-line/60 last:border-b-0"
					>
						{#if item.document}
							<span class="min-w-0 flex-1 truncate font-serif text-sm text-ink">
								{item.document.title}
							</span>
						{:else}
							<span class="min-w-0 flex-1 truncate font-serif text-sm text-ink-3 italic">
								{m.geschichte_documents_deleted_placeholder()}
							</span>
						{/if}
						<button
							type="button"
							data-remove-btn
							onclick={() => handleRemove(item)}
							aria-label={m.geschichte_documents_remove_label({ title: itemTitle(item) })}
							class="inline-flex h-11 min-w-[44px] items-center justify-center rounded text-ink-3 hover:text-danger focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
						>
							<svg
								class="h-4 w-4"
								fill="none"
								viewBox="0 0 24 24"
								stroke="currentColor"
								stroke-width="2"
							>
								<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
							</svg>
						</button>
					</li>
				{/each}
			</ul>
		{/if}

		<label for={pickerInputId} class="mb-1 block font-sans text-xs font-medium text-ink-2">
			{m.geschichte_documents_picker_label()}
		</label>
		<DocumentPickerDropdown
			inputId={pickerInputId}
			alreadyAddedIds={alreadyAddedIds}
			placeholder={m.geschichte_documents_picker_placeholder()}
			onSelect={handleAdd}
		/>
	</section>
</details>
