/**
 * Generic reactive entity cache with TTL, deduplication, and local mutations.
 *
 * Usage:
 *   const providers = createEntityCache<ServiceProvider>('/providers');
 *   // In component: const list = providers.items;  // reactive $state
 *   // await providers.fetch();                     // returns cached if fresh
 *   // providers.upsert(updated);                   // patch single item
 *   // providers.remove(id);                        // remove single item
 */

import { api } from '$lib/api';

const DEFAULT_TTL_MS = 30_000; // 30 seconds

export interface EntityCache<T extends { id: number }> {
	/** Reactive list of cached entities. */
	readonly items: T[];
	/**
	 * True only during the very first fetch — when there is no cached data
	 * to show yet. Background re-fetches keep `loading` false so consumers
	 * keep rendering the previous list and don't flash a spinner; observe
	 * `refreshing` instead if a subtle indicator is needed.
	 */
	readonly loading: boolean;
	/**
	 * True during any non-first fetch (cached items already populated).
	 * Lets consumers distinguish "show skeleton" (loading) from "show subtle
	 * shimmer/disabled state" (refreshing) without sharing one flag.
	 */
	readonly refreshing: boolean;
	/** Timestamp of last successful fetch. */
	readonly fetchedAt: number;
	/** Fetch entities — returns cached data if fresh, else hits network. */
	fetch(force?: boolean): Promise<T[]>;
	/** Force next fetch() to hit network. */
	invalidate(): void;
	/** Insert or update a single entity in the local cache. */
	upsert(entity: T): void;
	/** Remove a single entity from the local cache by id. */
	remove(id: number): void;
	/** Replace the entire cache (e.g. after a full reload). */
	set(entities: T[]): void;
	/** Clear all cached data. */
	clear(): void;
}

/** In-flight request deduplication map: endpoint → Promise */
const inflightRequests = new Map<string, Promise<any>>();

export function createEntityCache<T extends { id: number }>(
	endpoint: string,
	ttlMs: number = DEFAULT_TTL_MS,
): EntityCache<T> {
	let _items = $state<T[]>([]);
	let _loading = $state(false);
	let _refreshing = $state(false);
	let _fetchedAt = $state(0);

	function isFresh(): boolean {
		return _fetchedAt > 0 && Date.now() - _fetchedAt < ttlMs;
	}

	async function fetch(force = false): Promise<T[]> {
		if (!force && isFresh()) return _items;

		// Deduplicate concurrent requests to the same endpoint
		const existing = inflightRequests.get(endpoint);
		if (existing) return existing;

		// First-load vs background-refresh state. We split these so consumers
		// can keep the previous list visible during a re-fetch (refreshing)
		// instead of flashing a spinner placeholder (loading).
		const isFirstLoad = _fetchedAt === 0;
		if (isFirstLoad) _loading = true;
		else _refreshing = true;

		const request = api<T[]>(endpoint)
			.then((data) => {
				_items = data;
				_fetchedAt = Date.now();
				return data;
			})
			.finally(() => {
				_loading = false;
				_refreshing = false;
				inflightRequests.delete(endpoint);
			});

		inflightRequests.set(endpoint, request);
		return request;
	}

	function invalidate(): void {
		_fetchedAt = 0;
	}

	function upsert(entity: T): void {
		const idx = _items.findIndex((e) => e.id === entity.id);
		if (idx >= 0) {
			_items = _items.map((e) => (e.id === entity.id ? entity : e));
		} else {
			_items = [..._items, entity];
		}
	}

	function remove(id: number): void {
		_items = _items.filter((e) => e.id !== id);
	}

	function set(entities: T[]): void {
		_items = entities;
		_fetchedAt = Date.now();
	}

	function clear(): void {
		_items = [];
		_fetchedAt = 0;
	}

	return {
		get items() { return _items; },
		get loading() { return _loading; },
		get refreshing() { return _refreshing; },
		get fetchedAt() { return _fetchedAt; },
		fetch,
		invalidate,
		upsert,
		remove,
		set,
		clear,
	};
}
