type DragOptions = {
	/**
	 * Maps a viewport X coordinate to a bar index, or null if outside the row.
	 * The orchestrator owns the row element bound from `TimelineBars`.
	 */
	indexFromClientX(clientX: number): number | null;
	/** Returns the bucket label at index, or undefined if out of range. */
	labelAt(index: number): string | undefined;
	/**
	 * True when a label represents an aggregated year bar — controls click-to-zoom
	 * semantics (clicking a year zooms into its 12 months; clicking a month doesn't).
	 */
	isYearLabel(label: string): boolean;
	/**
	 * Emits a selection. `includeZoom` is true for a range drag or a click on a
	 * year bar; false for a click on a month bar.
	 */
	emit(startIndex: number, endIndex: number, includeZoom: boolean): void;
};

/**
 * Drag state machine for the timeline density widget. Exposes:
 * - `isDragging`, `lowIndex`, `highIndex` reactive read-onlies for the UI
 * - `pointerDown` / `pointerEnter` / `click` handlers for bar buttons
 * - `cleanup()` to drop document-level listeners on component unmount
 *
 * Document-level listeners (pointermove, pointerup, pointercancel) keep drag
 * tracking alive while the cursor leaves the original bar or the timeline row.
 * `cleanup()` must be called from a Svelte `$effect` teardown so a route change
 * mid-drag does not leak listeners.
 */
export function createTimelineDrag(opts: DragOptions) {
	let startIndex = $state<number | null>(null);
	let endIndex = $state<number | null>(null);
	// Set after a pointerup-driven emit so the synthesized click that follows is
	// suppressed (we'd otherwise emit twice). Keyboard Enter/Space fires click
	// without preceding pointerdown, so click stays the keyboard surface.
	let suppressClick = $state(false);

	function handleDocumentMove(e: PointerEvent) {
		const idx = opts.indexFromClientX(e.clientX);
		if (idx !== null) endIndex = idx;
	}

	function handleDocumentUp() {
		cleanup();
		finalizeDrag();
	}

	function handleDocumentCancel() {
		cleanup();
		startIndex = null;
		endIndex = null;
	}

	function cleanup() {
		document.removeEventListener('pointermove', handleDocumentMove);
		document.removeEventListener('pointerup', handleDocumentUp);
		document.removeEventListener('pointercancel', handleDocumentCancel);
	}

	function finalizeDrag() {
		if (startIndex === null || endIndex === null) return;
		const start = startIndex;
		const end = endIndex;
		startIndex = null;
		endIndex = null;
		const isRangeDrag = start !== end;
		const startLabel = opts.labelAt(start);
		// Range drag → atomic zoom + filter. Same-bar release on a year bar →
		// zoom into that year's months. Same-bar release on a month bar →
		// filter only.
		const includeZoom = isRangeDrag || (!!startLabel && opts.isYearLabel(startLabel));
		opts.emit(start, end, includeZoom);
		suppressClick = true;
		queueMicrotask(() => {
			suppressClick = false;
		});
	}

	return {
		get isDragging() {
			return startIndex !== null;
		},
		get lowIndex() {
			if (startIndex === null) return null;
			if (endIndex === null) return startIndex;
			return Math.min(startIndex, endIndex);
		},
		get highIndex() {
			if (startIndex === null) return null;
			if (endIndex === null) return startIndex;
			return Math.max(startIndex, endIndex);
		},
		pointerDown(event: PointerEvent, index: number) {
			if (event.button !== 0) return;
			startIndex = index;
			endIndex = index;
			document.addEventListener('pointermove', handleDocumentMove);
			document.addEventListener('pointerup', handleDocumentUp);
			document.addEventListener('pointercancel', handleDocumentCancel);
		},
		pointerEnter(index: number) {
			if (startIndex === null) return;
			endIndex = index;
		},
		click(index: number) {
			if (suppressClick) {
				suppressClick = false;
				return;
			}
			const label = opts.labelAt(index);
			const includeZoom = !!label && opts.isYearLabel(label);
			opts.emit(index, index, includeZoom);
		},
		cleanup
	};
}
