<script lang="ts">
import { enhance } from '$app/forms';
import { beforeNavigate } from '$app/navigation';
import { m } from '$lib/paraglide/messages.js';
import type { components } from '$lib/generated/api';
import type { DatePrecision } from '$lib/shared/utils/documentDate';
import { toPersonOption, type PersonOption } from '$lib/person/personOption';
import { type DocumentOption } from '$lib/document/documentTypeahead';
import { getConfirmService } from '$lib/shared/services/confirm.svelte.js';
import PersonMultiSelect from '$lib/person/PersonMultiSelect.svelte';
import DocumentMultiSelect from '$lib/document/DocumentMultiSelect.svelte';
import DatePrecisionField from '$lib/shared/primitives/DatePrecisionField.svelte';
import EventTypeSelect from '$lib/timeline/EventTypeSelect.svelte';
import BackButton from '$lib/shared/primitives/BackButton.svelte';

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

/**
 * Curator create/edit form for a timeline event. One component, two routes:
 * `/new` renders it empty, `/[id]/edit` renders it seeded with `event`. The
 * markup is never forked. All data flows through the route's +page.server.ts
 * load + form action (SSR) — there is no client fetch('/api/...') here.
 */
interface FormResult {
	error?: string;
	titleError?: string;
	dateError?: string;
	title?: string;
	description?: string;
	type?: string;
	personIds?: string[];
	documentIds?: string[];
	// Rehydrated chip data (id + label) so the pickers re-render after a fail(400)
	// even on a no-JS full reload — bare ids alone can't rebuild a chip (REQ-010).
	persons?: PersonOption[];
	documents?: DocumentOption[];
}

let {
	event = undefined,
	initialPersons = [],
	initialDocuments = [],
	originPersonId = '',
	form = null
}: {
	event?: TimelineEventView;
	initialPersons?: PersonOption[];
	initialDocuments?: DocumentOption[];
	originPersonId?: string;
	form?: FormResult | null;
} = $props();

// Initial-state snapshot from incoming props, preferring a preserved fail payload
// over the seeded `event`. This component is intentionally single-shot: props are
// snapshotted into $state once, so a parent re-render with a different `event`
// won't update the form — the two dedicated routes always remount, which is fine.
let title = $state(form?.title ?? event?.title ?? '');
let description = $state(form?.description ?? event?.description ?? '');
let type = $state<string>(form?.type ?? event?.type ?? 'PERSONAL');
let dateIso = $state(event?.eventDate ?? '');
let precision = $state<DatePrecision>((event?.precision as DatePrecision) ?? 'DAY');
let endDateIso = $state(event?.eventDateEnd ?? '');

// On a fail(400) the server returns rehydrated chip data (form.persons/documents)
// so the pickers survive the round-trip — even without JS — ahead of the seeded
// `event` or the prefill initials (REQ-010 / Decision 6).
let selectedPersons = $state<PersonOption[]>(
	form?.persons ?? (event?.persons ? event.persons.map(toPersonOption) : initialPersons)
);
let selectedDocuments = $state<DocumentOption[]>(
	form?.documents ??
		(event?.documents
			? event.documents.map((d) => ({
					// Graceful degradation: DocumentRef has no precision fields. formatDocumentOption
					// defaults a missing precision to DAY, so the chip shows the full documentDate.
					id: d.id,
					title: d.title,
					documentDate: d.documentDate
				}))
			: initialDocuments)
);

const isEdit = $derived(event !== undefined);

// Captured at init — Svelte context is init-only, so reading it lazily inside an
// event handler throws even when <ConfirmDialog> is mounted. Gates the delete.
const { confirm } = getConfirmService();

let titleTouched = $state(false);
let submitting = $state(false);
let dirty = $state(false);

const titleEmpty = $derived(title.trim().length === 0);
// Required-field errors derive from the CURRENT field value, not the stale server
// payload: a server titleError/dateError seeds the message, but typing a valid
// value clears it immediately instead of sticking until the next submit.
const titleError = $derived(
	titleEmpty && (titleTouched || !!form?.titleError) ? m.event_editor_title_required() : ''
);
const dateError = $derived(dateIso ? '' : (form?.dateError ?? ''));

beforeNavigate(({ cancel }) => {
	if (dirty && !submitting) {
		const ok = window.confirm(m.event_editor_unsaved_changes());
		if (!ok) cancel();
	}
});

// Every editable control routes its change through markDirty so the
// beforeNavigate guard catches edits to the date/precision/end-date and the
// pickers too — not just title/type/description (their onchange callbacks call
// this). No $effect: marking dirty from the actual edit events avoids a
// snapshot-vs-effect mount-timing trap.
function markDirty() {
	dirty = true;
}
</script>

<div class="mx-auto max-w-5xl px-4 py-8">
	<div class="mb-6">
		<BackButton />
	</div>

	<h1 class="mb-6 font-serif text-3xl font-bold text-ink">
		{isEdit ? m.event_editor_edit_title() : m.event_editor_new_title()}
	</h1>

	{#if form?.error}
		<p
			class="mb-4 rounded-sm border border-danger/40 bg-danger/10 px-4 py-3 text-sm text-danger"
			role="alert"
		>
			{form.error}
		</p>
	{/if}

	<form
		method="POST"
		action="?/save"
		use:enhance={({ cancel }) => {
			// Client-side guard against a blank title. enhance ignores onsubmit
			// preventDefault(), so cancel() is the only thing that actually stops the
			// POST; the server still re-validates and owns the authoritative fail(400).
			titleTouched = true;
			if (titleEmpty) {
				cancel();
				return;
			}
			submitting = true;
			return async ({ update }) => {
				submitting = false;
				dirty = false;
				await update();
			};
		}}
	>
		<input type="hidden" name="originPersonId" value={originPersonId} />
		{#if event}
			<!-- Optimistic-lock version travels back to the PUT so #3 can reject a
				 stale edit with 409. -->
			<input type="hidden" name="version" value={event.version} />
		{/if}

		<div class="grid grid-cols-1 gap-6 lg:grid-cols-[2fr_1fr]">
			<!-- Main column -->
			<div class="flex flex-col gap-6">
				<!-- Titel + Typ + Datum -->
				<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
					<h2 class="mb-5 text-xs font-bold tracking-widest text-ink-3 uppercase">
						{m.event_editor_section_when()}
					</h2>

					<div class="mb-5">
						<label for="event-title" class="mb-1 block text-sm font-medium text-ink-2">
							{m.event_editor_title_label()}*
						</label>
						<input
							id="event-title"
							name="title"
							type="text"
							bind:value={title}
							oninput={markDirty}
							onblur={() => (titleTouched = true)}
							maxlength="255"
							placeholder={m.event_editor_title_placeholder()}
							aria-required="true"
							aria-invalid={titleError ? 'true' : undefined}
							aria-describedby={titleError ? 'event-title-error' : undefined}
							class="block min-h-[48px] w-full rounded border border-line px-3 py-3 text-base shadow-sm
								{titleError
								? 'border-red-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500'
								: 'focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'}"
						/>
						{#if titleError}
							<p id="event-title-error" class="mt-1 text-sm text-danger" role="alert">
								<span aria-hidden="true">⚠ </span>{titleError}
							</p>
						{/if}
					</div>

					<div class="mb-5">
						<span class="mb-1 block text-sm font-medium text-ink-2"
							>{m.event_editor_type_label()}</span
						>
						<EventTypeSelect value={type} name="type" onchange={(t) => {
								type = t;
								markDirty();
							}} />
					</div>

					<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
						<DatePrecisionField
							bind:dateIso={dateIso}
							bind:precision={precision}
							bind:endDateIso={endDateIso}
							dateInputName="eventDate"
							endDateInputName="eventDateEnd"
							precisionInputName="precision"
							dateLabel={m.form_label_date()}
							dateError={dateError}
							onchange={markDirty}
							dateTestId="event-date"
							precisionTestId="event-precision"
							endDateInnerTestId="event-end-date"
						/>
					</div>
				</div>

				<!-- Beschreibung -->
				<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
					<h2 class="mb-5 text-xs font-bold tracking-widest text-ink-3 uppercase">
						{m.event_editor_section_description()}
					</h2>
					<label for="event-description" class="sr-only">{m.event_editor_description_label()}</label
					>
					<textarea
						id="event-description"
						name="description"
						bind:value={description}
						oninput={markDirty}
						rows="4"
						placeholder={m.event_editor_description_placeholder()}
						class="block w-full rounded border border-line px-3 py-3 text-base shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
					></textarea>
				</div>
			</div>

			<!-- Sidebar -->
			<div class="flex flex-col gap-6">
				<!-- Beteiligte Personen -->
				<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
					<h2 class="mb-5 text-xs font-bold tracking-widest text-ink-3 uppercase">
						{m.event_editor_section_persons()}
					</h2>
					<label for="event-persons-input" class="mb-1 block text-sm font-medium text-ink-2">
						{m.event_editor_persons_label()}
					</label>
					<PersonMultiSelect
						bind:selectedPersons={selectedPersons}
						inputId="event-persons-input"
						hiddenInputName="personIds"
						emptyLabel={m.event_editor_persons_empty()}
						onchange={markDirty}
					/>
				</div>

				<!-- Verknüpfte Briefe -->
				<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
					<h2 class="mb-5 text-xs font-bold tracking-widest text-ink-3 uppercase">
						{m.event_editor_section_documents()}
					</h2>
					<label for="event-documents-input" class="mb-1 block text-sm font-medium text-ink-2">
						{m.event_editor_documents_label()}
					</label>
					<DocumentMultiSelect
						bind:selectedDocuments={selectedDocuments}
						inputId="event-documents-input"
						hiddenInputName="documentIds"
						emptyLabel={m.event_editor_documents_empty()}
						onchange={markDirty}
					/>
				</div>
			</div>
		</div>

		<!-- Save bar -->
		<div
			class="sticky bottom-0 z-10 -mx-4 mt-6 flex flex-col gap-3 border-t border-line bg-surface px-6 py-4 shadow-[0_-2px_8px_rgba(0,0,0,0.06)] sm:flex-row sm:items-center sm:justify-between"
		>
			<p class="font-sans text-xs text-ink-3">{m.event_editor_save_hint()}</p>
			<div class="flex items-center gap-2">
				<button
					type="submit"
					disabled={submitting}
					class="inline-flex h-11 items-center rounded bg-primary px-4 font-sans text-sm font-medium text-primary-fg hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50"
				>
					{m.event_editor_save()}
				</button>
			</div>
		</div>
	</form>

	{#if isEdit}
		<!-- Delete lives in its own form so it posts to the dedicated ?/delete action.
			 The confirm gate runs inside the enhance submit phase: enhance ignores an
			 onsubmit preventDefault(), so awaiting confirm() and calling cancel() is the
			 only thing that actually stops the destructive POST. -->
		<form
			method="POST"
			action="?/delete"
			use:enhance={async ({ cancel }) => {
				const ok = await confirm({
					title: m.event_editor_delete_confirm_title(),
					body: m.event_editor_delete_confirm_body(),
					destructive: true,
					confirmLabel: m.event_editor_delete()
				});
				if (!ok) {
					cancel();
					return;
				}
				return async ({ update }) => {
					// Clear dirtiness so beforeNavigate doesn't prompt "unsaved changes"
					// on the post-delete redirect.
					dirty = false;
					await update();
				};
			}}
			class="mt-4"
		>
			<input type="hidden" name="originPersonId" value={originPersonId} />
			<button
				type="submit"
				class="inline-flex h-11 items-center rounded border border-danger/40 px-4 font-sans text-sm font-medium text-danger hover:bg-danger/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
			>
				{m.event_editor_delete()}
			</button>
		</form>
	{/if}
</div>
