<script lang="ts">
import EventPill from './EventPill.svelte';
import WorldBand from './WorldBand.svelte';
import LetterCard from './LetterCard.svelte';
import YearLetterStrip from './YearLetterStrip.svelte';
import LetterBucket from './LetterBucket.svelte';
import { isDense } from './timelineDensity';
import { entryKey } from './entryKey';
import {
	bucketLetters,
	type GroupingMode,
	type LetterBucket as LetterBucketModel
} from './timelineGrouping';
import type { components } from '$lib/generated/api';

type TimelineYearDTO = components['schemas']['TimelineYearDTO'];
type TimelineEntryDTO = components['schemas']['TimelineEntryDTO'];

/**
 * One year of the timeline: a <section> with a sticky <h2> (REQ-006). Events
 * render in DTO order as pills/bands; letters render as individual cards while
 * the band holds ≤ 12 (REQ-011) or collapse to a single density strip above that
 * (REQ-012). Entries are never re-sorted — DTO order is preserved (REQ-003).
 *
 * In Ereignis/Thema mode (#827) the event pills/world-bands render identically
 * (REQ-001); only the loose letters re-bundle into per-year buckets below them
 * (REQ-002/003/004). Datum mode is the original individual-card / density-strip
 * path, untouched.
 */
let {
	year,
	canWrite = false,
	groupingMode = 'date',
	eventLookup = new Map<string, string>()
}: {
	year: TimelineYearDTO;
	canWrite?: boolean;
	groupingMode?: GroupingMode;
	eventLookup?: Map<string, string>;
} = $props();

type Row =
	| { t: 'event'; entry: TimelineEntryDTO }
	| { t: 'eventcard'; entry: TimelineEntryDTO; bucket: LetterBucketModel }
	| { t: 'letter'; entry: TimelineEntryDTO; side: 'left' | 'right' }
	| { t: 'strip' }
	| { t: 'bucket'; bucket: LetterBucketModel; nested: boolean };

const letters = $derived(year.entries.filter((e) => e.kind === 'LETTER'));
const dense = $derived(isDense(letters.length));
const bucketMode = $derived(groupingMode === 'thema' ? 'thema' : 'event');

const rows = $derived.by<Row[]>(() => {
	const out: Row[] = [];

	// Ereignis: events stay on the axis (REQ-001). A curated event WITH letters in this band
	// becomes the contained card's header (no separate pill — its title reads once, #827
	// redesign); a letterless/derived/world event stays a plain pill/band. A cluster whose event
	// lives in another year band (or was filtered out) renders as a text-header card here, and
	// the unlinked letters fall to the single "Weitere Briefe" drawer (REQ-003/006/019).
	if (groupingMode === 'event') {
		const buckets = bucketLetters(letters, 'event', eventLookup);
		const sameYearBucket = (id: string | undefined) =>
			id ? buckets.find((b) => b.kind === 'event' && b.key === `event:${id}`) : undefined;
		for (const entry of year.entries) {
			if (entry.kind !== 'EVENT') continue;
			const bucket = sameYearBucket(entry.eventId);
			// A curated event with same-year letters becomes the card header (card replaces pill);
			// otherwise it stays a plain pill/world-band.
			if (bucket) out.push({ t: 'eventcard', entry, bucket });
			else out.push({ t: 'event', entry });
		}
		// Cross-year clusters (no matching event entry in this band) and the fallback drawer
		// render after the axis entries, with their own text header.
		for (const bucket of buckets) {
			if (
				bucket.kind === 'fallback' ||
				!year.entries.some((e) => e.kind === 'EVENT' && `event:${e.eventId}` === bucket.key)
			) {
				out.push({ t: 'bucket', bucket, nested: false });
			}
		}
		return out;
	}

	// Thema: events stay on the axis (REQ-001); loose letters re-bundle into per-year root-tag
	// buckets below them (REQ-004) — no axis pill exists for a tag, so every bucket keeps a header.
	if (groupingMode === 'thema') {
		for (const entry of year.entries) {
			if (entry.kind === 'EVENT') out.push({ t: 'event', entry });
		}
		for (const bucket of bucketLetters(letters, 'thema', eventLookup)) {
			out.push({ t: 'bucket', bucket, nested: false });
		}
		return out;
	}

	let stripInserted = false;
	let letterIndex = 0;
	for (const entry of year.entries) {
		if (entry.kind === 'EVENT') {
			out.push({ t: 'event', entry });
		} else if (!dense) {
			out.push({ t: 'letter', entry, side: letterIndex % 2 === 0 ? 'left' : 'right' });
			letterIndex += 1;
		} else if (!stripInserted) {
			out.push({ t: 'strip' });
			stripInserted = true;
		}
	}
	return out;
});

function rowKey(row: Row): string {
	if (row.t === 'strip') return `strip-${year.year}`;
	if (row.t === 'bucket') return row.bucket.key;
	return entryKey(row.entry);
}
</script>

<section class="py-2">
	<h2 class="year-heading">
		<span data-testid="year-node" class="year-node bg-brand-navy" aria-hidden="true"></span>
		<span
			data-testid="year-label"
			class="year-label rounded-full bg-brand-navy px-4 py-1 font-serif text-sm font-bold text-white"
			>{year.year}</span
		>
	</h2>

	<div class="mt-3 space-y-3">
		{#each rows as row (rowKey(row))}
			{#if row.t === 'event'}
				{#if row.entry.type === 'HISTORICAL'}
					<WorldBand entry={row.entry} canWrite={canWrite} />
				{:else}
					<EventPill entry={row.entry} canWrite={canWrite} />
				{/if}
			{:else if row.t === 'eventcard'}
				<LetterBucket
					bucket={row.bucket}
					mode="event"
					year={year.year}
					event={row.entry}
					canWrite={canWrite}
				/>
			{:else if row.t === 'letter'}
				<div class="letter-row" data-side={row.side}>
					<span data-testid="letter-dot" class="letter-dot bg-surface" aria-hidden="true"></span>
					<LetterCard entry={row.entry} />
				</div>
			{:else if row.t === 'bucket'}
				<LetterBucket bucket={row.bucket} mode={bucketMode} year={year.year} nested={row.nested} />
			{:else}
				<YearLetterStrip letters={letters} year={year.year} />
			{/if}
		{/each}
	</div>
</section>

<style>
/* Sticky offset in scoped CSS so it holds in unit tests too (the global
	   header is a 64px sticky nav). #779 REQ-006 / #833 REQ-003. The sticky
	   element is also the positioning context for the year-node marker. */
.year-heading {
	position: sticky;
	top: 4rem;
	z-index: 1;
}

/* Markers ride on the spine's single source of truth: --spine-x, declared
	   once on TimelineView's .timeline-axis and inherited here (0.5rem phone /
	   50% desktop). The 0.5rem fallback only applies when a YearBand is rendered
	   outside the axis (e.g. component tests). #833 REQ-003/004/005. */

/* Phone (< 1024px): badge sits at the left spine, clearing the node marker.
	   The badge sits above the node (z-index) so on desktop, where the centered
	   pill covers the centered node, the white year text is never occluded. */
.year-label {
	display: inline-block;
	margin-left: 1.75rem;
	position: relative;
	z-index: 2;
}

/* Navy node marker on the spine. On phone it shows to the left of the badge;
	   on desktop it sits behind the centered pill, which is itself the
	   axis interruption. */
.year-node {
	position: absolute;
	top: 50%;
	left: var(--spine-x, 0.5rem);
	width: 11px;
	height: 11px;
	border-radius: 9999px;
	transform: translate(-50%, -50%);
	z-index: 1;
}

/* Per-letter connector dot (white fill via bg-surface, mint ring) on the spine. */
.letter-row {
	position: relative;
	padding-left: 1.75rem;
}
.letter-dot {
	position: absolute;
	top: 0.9rem;
	left: var(--spine-x, 0.5rem);
	width: 13px;
	height: 13px;
	border-radius: 9999px;
	border: 2.5px solid var(--palette-mint);
	transform: translate(-50%, -50%);
	z-index: 2;
}

/* Desktop (≥ 1024px): centered axis. The badge centers on the spine, the node
	   sits at the spine centre, and letters alternate left/right with the
	   connector dot on the centred spine between card and axis. #833 REQ-003/004/005. */
@media (min-width: 1024px) {
	.year-label {
		display: block;
		width: fit-content;
		margin-left: auto;
		margin-right: auto;
	}
	/* .year-node needs no desktop override — it inherits --spine-x: 50% from
		   the axis. */
	.letter-row {
		width: 50%;
		padding-left: 0;
	}
	.letter-row[data-side='left'] {
		margin-right: auto;
		padding-right: 1.75rem;
	}
	.letter-row[data-side='right'] {
		margin-left: auto;
		padding-left: 1.75rem;
	}
	.letter-row[data-side='left'] .letter-dot {
		left: auto;
		right: 0;
		transform: translate(50%, -50%);
	}
	.letter-row[data-side='right'] .letter-dot {
		left: 0;
		right: auto;
		transform: translate(-50%, -50%);
	}
}
</style>
