// Injected shell for smart-scroll: binds a real scrollable element to the pure
// reducer (logic/smart-scroll). It owns the reactive `showButton` flag (a thin
// rune wrapper over the reducer state), runs the scroll COMMANDS the reducer
// emits against the element, and listens at the outermost edges (the element's
// `scroll`/`scrollend` events + a ResizeObserver on the content). No ambient
// state: the consumer instantiates ONE controller per scroll region and disposes
// it on unmount.

import {
	createSmartScrollState,
	onContentChange,
	onReset,
	onResume,
	onScroll,
	type ScrollCommand,
	type ScrollGeometry,
	type SmartScrollResult,
	type SmartScrollState,
} from "../logic/smart-scroll";

export interface SmartScrollController {
	/** Reactive: show the "scroll to bottom" affordance (the user has scrolled up). */
	readonly showButton: boolean;
	/**
	 * Attach to the scroll container; returns a teardown to call on unmount.
	 * Pass the inner CONTENT element to also follow height changes that aren't a
	 * transcript update (async markdown/highlight, image loads, a collapse toggling,
	 * viewport reflow) via a ResizeObserver.
	 */
	attach(el: HTMLElement, content?: HTMLElement): () => void;
	/**
	 * Notify that the transcript content changed (a streamed delta / new message).
	 * While stuck, keeps the view pinned to the bottom.
	 */
	contentChanged(): void;
	/** Reset for a new transcript context (e.g. conversation switch): snap to bottom. */
	reset(): void;
	/** The user clicked the affordance: re-stick and smooth-scroll to the bottom. */
	resume(): void;
}

function geometryOf(el: HTMLElement): ScrollGeometry {
	return {
		scrollTop: el.scrollTop,
		scrollHeight: el.scrollHeight,
		clientHeight: el.clientHeight,
	};
}

export function createSmartScrollController(): SmartScrollController {
	let state: SmartScrollState = createSmartScrollState();
	let showButton = $state(false);
	let el: HTMLElement | null = null;
	// True while WE drive a programmatic scroll, so the resulting `scroll` event
	// doesn't get misread as the user scrolling up. Cleared on `scrollend`.
	let selfScrolling = false;

	function run(command: ScrollCommand | null): void {
		if (!command || !el) return;
		selfScrolling = true;
		el.scrollTo({
			top: el.scrollHeight,
			behavior: command.animate ? "smooth" : "instant",
		});
	}

	function apply(r: SmartScrollResult): void {
		state = r.state;
		showButton = r.showButton;
		run(r.command);
	}

	function handleScroll(): void {
		if (!el || selfScrolling) return;
		apply(onScroll(state, geometryOf(el)));
	}

	function handleScrollEnd(): void {
		selfScrolling = false;
	}

	return {
		get showButton(): boolean {
			return showButton;
		},

		attach(node: HTMLElement, content?: HTMLElement): () => void {
			el = node;
			node.addEventListener("scroll", handleScroll, { passive: true });
			node.addEventListener("scrollend", handleScrollEnd);

			// A ResizeObserver keeps the view pinned through height changes that are
			// NOT a transcript update — async markdown/syntax-highlight, image loads, a
			// collapse toggling, font swaps, viewport reflow — which a content-count
			// signal can't see. Observe the CONTENT (it grows) and the container (it
			// changes on viewport resize). Routed through `onContentChange`, so it only
			// scrolls while stuck and never fights the reader. The `selfScrolling` guard
			// (and the fact that scrolling doesn't resize content) prevents any loop.
			let ro: ResizeObserver | null = null;
			if (typeof ResizeObserver !== "undefined") {
				ro = new ResizeObserver(() => {
					if (!el || selfScrolling) return;
					apply(onContentChange(state, geometryOf(el)));
				});
				if (content) ro.observe(content);
				ro.observe(node);
			}

			return () => {
				node.removeEventListener("scroll", handleScroll);
				node.removeEventListener("scrollend", handleScrollEnd);
				ro?.disconnect();
				if (el === node) el = null;
			};
		},

		contentChanged(): void {
			if (!el) return;
			apply(onContentChange(state, geometryOf(el)));
		},

		reset(): void {
			apply(onReset());
		},

		resume(): void {
			apply(onResume(state));
		},
	};
}
