import { LRUCache } from 'lru-cache'
import { config } from '../config'
import type { WebhookEntry } from './db'

/** Reserved cache key for the bounded backlinks query. */
export const BACKLINK_CACHE_KEY = '__backlinks__'

/**
 * DB query results keyed by scoped DID. Empty arrays are deliberate cache values:
 * a high-volume DID without subscriptions must not trigger a DB query per event.
 */
const cache = new LRUCache<string, readonly WebhookEntry[]>({
	max: config.webhookCacheMax,
	ttl: config.webhookCacheTtlMs,
	updateAgeOnGet: false,
	allowStale: false,
})

// Per-key generations prevent stale fills without invalidating unrelated cache
// entries. The map is hard-bounded; evicting metadata advances a global epoch so
// an old in-flight token can never match a freshly absent key.
const generations = new Map<string, number>()
let globalGeneration = 0
const MAX_GENERATION_KEYS = config.webhookCacheMax

function hasControlCharacter(value: string): boolean {
	for (let index = 0; index < value.length; index++) {
		if ((value.charCodeAt(index) || 0) < 32) return true
	}
	return false
}

function validCacheKey(key: string): boolean {
	return key === BACKLINK_CACHE_KEY || (key.length > 0 && key.length <= 2_048 && !hasControlCharacter(key))
}

export function getCached(scopeDid: string): readonly WebhookEntry[] | undefined {
	if (!validCacheKey(scopeDid)) return undefined
	return cache.get(scopeDid)
}

export function getCacheGeneration(scopeDid: string): string {
	return validCacheKey(scopeDid) ? `${globalGeneration}:${generations.get(scopeDid) ?? 0}` : 'invalid'
}

export function setCachedIfCurrent(scopeDid: string, entries: readonly WebhookEntry[], generation: string): boolean {
	if (!validCacheKey(scopeDid) || getCacheGeneration(scopeDid) !== generation) return false
	// Copy the array so callers cannot mutate a cached empty/non-empty result after storing it.
	cache.set(scopeDid, [...entries])
	return true
}

export function invalidate(scopeDid: string): void {
	if (!validCacheKey(scopeDid)) return
	if (!generations.has(scopeDid) && generations.size >= MAX_GENERATION_KEYS) {
		globalGeneration++
		generations.clear()
	}
	generations.set(scopeDid, (generations.get(scopeDid) ?? 0) + 1)
	cache.delete(scopeDid)
}

export function invalidateMany(scopeDids: Iterable<string>): void {
	for (const scopeDid of scopeDids) invalidate(scopeDid)
}

export function clearRegistryCache(): void {
	globalGeneration++
	generations.clear()
	cache.clear()
}
