/**
 * Stale-while-revalidate resource cache.
 *
 * Backs list pages so that switching between sidebar tabs doesn't flash an
 * empty/skeleton frame on every visit. The first visit to a page does a cold
 * load (skeleton shown once); every subsequent visit renders the previously
 * loaded value immediately while a background refresh keeps it current.
 *
 * The cache is module-scoped (one instance per resource, created in
 * `caches.ts`) so its value survives component unmount/remount across
 * client-side navigation — that persistence is what removes the flicker.
 *
 * Style mirrors `navCounts.ts`: a classic `writable` exposed as a `Readable`
 * plus imperative `refresh()`/`set()` methods, so pages keep using `$store`
 * auto-subscription.
 */

import { writable, get, type Readable } from 'svelte/store';

export interface ResourceState<T> {
	/** Last successfully loaded value (or the initial seed before first load). */
	value: T;
	/**
	 * True only during the *cold* load — i.e. the resource has never loaded
	 * successfully and there is no error yet. Drives the skeleton. Stays false
	 * for background refreshes so revisits never flash.
	 */
	loading: boolean;
	/** True while any fetch (cold or background) is in flight. */
	refreshing: boolean;
	/** Last error message, or '' when the most recent fetch succeeded. */
	error: string;
	/** Whether at least one fetch has ever succeeded. */
	loaded: boolean;
}

export interface ResourceCache<T> extends Readable<ResourceState<T>> {
	/**
	 * (Re)fetch the resource. Aborts any in-flight fetch so the latest call
	 * wins — important after a mutation, where a stale background refresh must
	 * not clobber fresh data. Safe to call from `onMount` on every visit.
	 */
	refresh(): Promise<void>;
	/** Optimistically replace the cached value (e.g. after a local mutation). */
	set(value: T): void;
	/** Clear back to the cold/initial state (e.g. on logout). */
	reset(): void;
}

export function createResourceCache<T>(
	fetcher: (signal?: AbortSignal) => Promise<T>,
	initial: T
): ResourceCache<T> {
	const seed: ResourceState<T> = {
		value: initial,
		loading: true,
		refreshing: false,
		error: '',
		loaded: false
	};
	const store = writable<ResourceState<T>>(seed);

	let inFlight: AbortController | null = null;

	async function refresh(): Promise<void> {
		// Latest-wins: cancel a previous in-flight fetch so a slow background
		// refresh can't overwrite a newer (e.g. post-mutation) result.
		if (inFlight) inFlight.abort();
		const ac = new AbortController();
		inFlight = ac;

		store.update((s) => ({ ...s, refreshing: true, error: '' }));
		try {
			const value = await fetcher(ac.signal);
			if (ac.signal.aborted) return;
			store.set({ value, loading: false, refreshing: false, error: '', loaded: true });
		} catch (e) {
			if (ac.signal.aborted) return;
			const error = e instanceof Error ? e.message : String(e);
			// Keep the last good value (stale-while-revalidate) and surface the
			// error; clear `loading` so the cold skeleton resolves to either data
			// or an error state rather than spinning forever.
			store.update((s) => ({ ...s, loading: false, refreshing: false, error }));
		} finally {
			if (inFlight === ac) inFlight = null;
		}
	}

	function set(value: T): void {
		store.update((s) => ({ ...s, value, loaded: true, loading: false }));
	}

	function reset(): void {
		if (inFlight) {
			inFlight.abort();
			inFlight = null;
		}
		store.set({ ...seed, value: initial });
	}

	return { subscribe: store.subscribe, refresh, set, reset };
}

// ─────────────────────────────────────────────────────────────────────────
// Keyed (per-id) variant — for detail pages fetched by a route param.
//
// A single-value cache can't serve detail pages (each `[id]` is different
// data). This holds a Map<id, state> so revisiting (or A→B→A navigating) a
// detail renders the cached entity instantly while revalidating that id.
// Pages read reactively via `$cache.get(id)` (the store value is the Map) and
// call `cache.refresh(id)` / `cache.peek(id)` (methods on the object).
// ─────────────────────────────────────────────────────────────────────────

export interface KeyedResourceState<T> {
	/** Last loaded value for this key, or null before its first load. */
	value: T | null;
	/** True on the cold load for this key (never loaded, no error). */
	loading: boolean;
	/** True while a fetch for this key is in flight. */
	refreshing: boolean;
	error: string;
	loaded: boolean;
}

function coldKeyed<T>(): KeyedResourceState<T> {
	return { value: null, loading: true, refreshing: false, error: '', loaded: false };
}

export interface KeyedResourceCache<T> extends Readable<Map<string, KeyedResourceState<T>>> {
	/** (Re)fetch one key; aborts a prior in-flight fetch for the same key. */
	refresh(key: string): Promise<void>;
	/** Optimistically replace one key's value (e.g. after a mutation). */
	set(key: string, value: T): void;
	/** Non-reactive snapshot read; returns the cold state if the key is absent. */
	peek(key: string): KeyedResourceState<T>;
	/** Drop a single key (e.g. after deleting that entity) so a later visit
	 *  doesn't warm-seed a stale/phantom value. */
	remove(key: string): void;
	/** Drop all keys (e.g. on logout). */
	reset(): void;
}

export function createKeyedResourceCache<T>(
	fetcher: (key: string, signal?: AbortSignal) => Promise<T>
): KeyedResourceCache<T> {
	const store = writable<Map<string, KeyedResourceState<T>>>(new Map());
	const inFlight = new Map<string, AbortController>();

	function patch(key: string, partial: Partial<KeyedResourceState<T>>): void {
		store.update((m) => {
			const next = new Map(m);
			next.set(key, { ...(next.get(key) ?? coldKeyed<T>()), ...partial });
			return next;
		});
	}

	async function refresh(key: string): Promise<void> {
		const prev = inFlight.get(key);
		if (prev) prev.abort();
		const ac = new AbortController();
		inFlight.set(key, ac);

		patch(key, { refreshing: true, error: '' });
		try {
			const value = await fetcher(key, ac.signal);
			if (ac.signal.aborted) return;
			patch(key, { value, loaded: true, loading: false, refreshing: false, error: '' });
		} catch (e) {
			if (ac.signal.aborted) return;
			patch(key, {
				loading: false,
				refreshing: false,
				error: e instanceof Error ? e.message : String(e)
			});
		} finally {
			if (inFlight.get(key) === ac) inFlight.delete(key);
		}
	}

	function set(key: string, value: T): void {
		patch(key, { value, loaded: true, loading: false });
	}

	function peek(key: string): KeyedResourceState<T> {
		return get(store).get(key) ?? coldKeyed<T>();
	}

	function remove(key: string): void {
		const ac = inFlight.get(key);
		if (ac) {
			ac.abort();
			inFlight.delete(key);
		}
		store.update((m) => {
			if (!m.has(key)) return m;
			const next = new Map(m);
			next.delete(key);
			return next;
		});
	}

	function reset(): void {
		for (const ac of inFlight.values()) ac.abort();
		inFlight.clear();
		store.set(new Map());
	}

	return { subscribe: store.subscribe, refresh, set, peek, remove, reset };
}
