/**
 * Tiny request cache + in-flight dedupe for provider list fetches.
 *
 * The admin app refetches a provider's records every time you switch to its tab
 * (the RecordsView reset effect drops status to `idle`, which retriggers the
 * fetch). On a dev box with only a couple of PHP-FPM workers those redundant
 * round-trips queue up and stall the UI. This layer:
 *
 *   - collapses concurrent identical requests into ONE network call (dedupe), and
 *   - serves a recent (< TTL) successful response from memory on tab revisits,
 *     so flipping between tabs doesn't re-hit the server.
 *
 * It is intentionally module-scoped (shared across the single RecordsView
 * instance) and memory-only — a page reload starts fresh. Mutations invalidate
 * the affected provider so the next read is authoritative.
 */

const TTL_MS = 30_000;

// path -> { ts: number, data: any }
const responseCache = new Map();
// path -> Promise<any>
const inFlight = new Map();

// A monotonic clock that doesn't depend on Date.now() being mockable; falls back
// to Date.now() where performance.now() is unavailable.
const now = () =>
	typeof performance !== 'undefined' && typeof performance.now === 'function'
		? performance.now()
		: Date.now();

/**
 * Whether a path has a still-fresh cached response (no network needed).
 *
 * @param {string} path REST path including its query string.
 * @return {boolean} True when a cache hit would be served.
 */
export const isListCacheFresh = ( path ) => {
	const cached = responseCache.get( path );
	return Boolean( cached && now() - cached.ts < TTL_MS );
};

/**
 * Fetch a list path through the cache.
 *
 * @param {string}   path         REST path (already includes the query string).
 * @param {Function} fetcher      () => Promise<any> that performs the real request.
 * @param {Object}   [opts]
 * @param {boolean}  [opts.force] Bypass the cached response (still deduped).
 * @return {Promise<any>} The response payload.
 */
export const fetchListCached = ( path, fetcher, opts = {} ) => {
	const { force = false } = opts;

	if ( ! force ) {
		const cached = responseCache.get( path );
		if ( cached && now() - cached.ts < TTL_MS ) {
			return Promise.resolve( cached.data );
		}

		const pending = inFlight.get( path );
		if ( pending ) {
			return pending;
		}
	}

	const promise = Promise.resolve()
		.then( fetcher )
		.then( ( data ) => {
			responseCache.set( path, { ts: now(), data } );
			inFlight.delete( path );
			return data;
		} )
		.catch( ( error ) => {
			inFlight.delete( path );
			throw error;
		} );

	inFlight.set( path, promise );
	return promise;
};

/**
 * Drop every cached/in-flight entry for a provider route, so the next read goes
 * to the server. Call after a create/update/delete on that provider.
 *
 * @param {string} routePrefix e.g. `${namespace}/${route}` — matched as a prefix.
 */
export const invalidateListCache = ( routePrefix ) => {
	if ( ! routePrefix ) {
		responseCache.clear();
		inFlight.clear();
		return;
	}

	[ responseCache, inFlight ].forEach( ( map ) => {
		Array.from( map.keys() ).forEach( ( key ) => {
			if (
				key === routePrefix ||
				key.startsWith( `${ routePrefix }?` )
			) {
				map.delete( key );
			}
		} );
	} );
};
