// cache.js — bounded LRU cache with an injectable factory.
//
// Default implementation is a Map-based LRU (insertion-order eviction) that
// matches the surface the engine previously consumed from HyperbolicLRUCache:
//   constructor(capacity, { onEvict })  +  set / get / has / delete / clear / size()
// onEvict(key, value) fires both on capacity eviction and on explicit delete(),
// preserving the prior contract. Hyperbolic-specific options (sampleSize,
// sketchEpsilon, sketchDelta) are accepted and ignored.
//
// Dependency injection: Arbiter exposes `cacheFactory` (from options.cacheFactory,
// defaulting to `defaultCacheFactory` below). Every cache construction site calls
// its arbiter's cacheFactory, so a consumer can substitute any cache impl — e.g.
// `new Arbiter({ cacheFactory: (c, o) => new HyperbolicLRUCache(c, o) })` — with no
// optional imports and no call-site edits.

export class SimpleLRUCache {
  constructor(capacity = 1024, options = {}) {
    this.capacity = capacity;
    this.onEvict = options.onEvict;
    this._map = new Map();
  }

  // Current entry count. Exposed as a method to match HyperbolicLRUCache.size().
  size() {
    return this._map.size;
  }

  has(key) {
    return this._map.has(key);
  }

  get(key) {
    if (!this._map.has(key)) return undefined;
    const value = this._map.get(key);
    // Refresh recency: re-insert at the MRU end (Map preserves insertion order).
    this._map.delete(key);
    this._map.set(key, value);
    return value;
  }

  set(key, value) {
    if (this._map.has(key)) {
      // Key exists: refresh recency by re-inserting.
      this._map.delete(key);
    } else if (this.capacity > 0 && this._map.size >= this.capacity) {
      // At capacity: evict the least-recently-used (oldest) entry.
      const oldestKey = this._map.keys().next().value;
      const oldestValue = this._map.get(oldestKey);
      this._map.delete(oldestKey);
      this._fireOnEvict(oldestKey, oldestValue);
    }
    this._map.set(key, value);
  }

  delete(key) {
    if (!this._map.has(key)) return false;
    const value = this._map.get(key);
    this._map.delete(key);
    // Match HyperbolicLRUCache: explicit delete also notifies onEvict.
    this._fireOnEvict(key, value);
    return true;
  }

  // Alias of delete() matching the invalidation surface the engine
  // consumed from HyperbolicLRUCache (invalidate / invalidateMany /
  // invalidateByPattern).
  invalidate(key) {
    return this.delete(key);
  }

  invalidateMany(keys) {
    let removed = 0;
    for (const key of keys) {
      if (this.delete(key)) removed++;
    }
    return removed;
  }

  /**
   * Delete every entry whose key matches the pattern. The pattern may be
   * a RegExp or a plain string (substring match). Used by the DecisionCache
   * port to invalidate composite keys containing a given node id.
   */
  invalidateByPattern(pattern) {
    const matcher = pattern instanceof RegExp
      ? (key) => pattern.test(key)
      : (key) => typeof key === 'string' && key.includes(pattern);
    const matched = [];
    for (const key of this._map.keys()) {
      if (matcher(key)) matched.push(key);
    }
    for (const key of matched) this.delete(key);
    return matched.length;
  }

  clear() {
    this._map.clear();
  }

  _fireOnEvict(key, value) {
    if (this.onEvict) {
      try {
        this.onEvict(key, value);
      } catch (e) {
        console.error('cache onEvict callback failed:', e);
      }
    }
  }
}

// Default factory: produces a SimpleLRUCache. Override per-Arbiter via
// options.cacheFactory to plug in an alternative cache implementation.
export function defaultCacheFactory(capacity, options) {
  return new SimpleLRUCache(capacity, options);
}
