// optimistic overlays for eventually-consistent bobbin reads: mutations commit
// locally (the pds write is authoritative) and reconcile on natural reloads.
// `key` scopes an overlay to its subject so reused components drop it.
// TODO(bobbin): read-your-writes (serve at-or-after a commit rev) would let
// mutations invalidate loads immediately instead of waiting for navigation.

interface OptimisticCountOptions {
	key: () => string;
	loaded: () => number | null | undefined;
}

export interface OptimisticCount {
	readonly value: number;
	readonly failed: boolean;
	adjust(delta: number): void;
	fail(): void;
	resetFailure(): void;
}

export const createOptimisticCount = (options: OptimisticCountOptions): OptimisticCount => {
	let failed = $state(false);
	// a bump is a bound: fresher data may pass it, never regress across it.
	let held = $state<null | { key: string; value: number; up: boolean }>(null);
	const currentKey = $derived(options.key());
	const loaded = $derived(Math.max(0, options.loaded() ?? 0));

	const value = $derived.by(() => {
		if (held === null || held.key !== currentKey) return loaded;
		return held.up ? Math.max(loaded, held.value) : Math.min(loaded, held.value);
	});

	$effect(() => {
		if (held === null) return;
		const caughtUp = held.up ? loaded >= held.value : loaded <= held.value;
		if (held.key !== currentKey || caughtUp) held = null;
	});

	return {
		get value() {
			return value;
		},
		get failed() {
			return failed;
		},
		adjust(delta) {
			failed = false;
			held = { key: currentKey, value: Math.max(0, value + delta), up: delta > 0 };
		},
		fail() {
			failed = true;
		},
		resetFailure() {
			failed = false;
		}
	};
};

interface OptimisticRelationOptions {
	key: () => string;
	loadedRkey: () => string | null | undefined;
}

export interface OptimisticRelation {
	readonly rkey: string | null;
	readonly known: boolean;
	readonly active: boolean;
	readonly failed: boolean;
	created(rkey: string): void;
	deleted(): void;
	fail(): void;
	resetFailure(): void;
}

export const createOptimisticRelation = (
	options: OptimisticRelationOptions
): OptimisticRelation => {
	let failed = $state(false);
	let committed = $state<null | { key: string; rkey: string | null }>(null);
	const currentKey = $derived(options.key());
	const loaded = $derived(options.loadedRkey());
	const rkey = $derived(committed?.key === currentKey ? committed.rkey : (loaded ?? null));

	$effect(() => {
		if (committed === null) return;
		if (committed.key !== currentKey || loaded === committed.rkey) {
			committed = null;
			failed = false;
		}
	});

	const set = (next: string | null): void => {
		failed = false;
		committed = { key: currentKey, rkey: next };
	};

	return {
		get rkey() {
			return rkey;
		},
		get known() {
			return loaded !== undefined || committed?.key === currentKey;
		},
		get active() {
			return rkey !== null;
		},
		get failed() {
			return failed;
		},
		created: set,
		deleted: () => set(null),
		fail() {
			failed = true;
		},
		resetFailure() {
			failed = false;
		}
	};
};
