import type { CacheAdapter } from '../domain/ports.js';
import type { CacheContract } from '../domain/cache.js';

export class CacheService {
  private readonly contracts = new Map<string, CacheContract>();
  private hits = 0;
  private misses = 0;
  private invalidations = 0;

  constructor(private readonly adapter: CacheAdapter) {}

  registerContract(contract: CacheContract): void {
    this.contracts.set(contract.name, contract);
  }

  listContracts(): CacheContract[] {
    return [...this.contracts.values()];
  }

  async read<T>(contractName: string, key: string, loader: () => Promise<T>): Promise<T> {
    const contract = this.contracts.get(contractName);
    if (!contract) {
      // Unknown contracts still load from source.
      return loader();
    }
    const cached = await this.adapter.get<T>(key);
    if (cached !== undefined) {
      this.hits += 1;
      return cached;
    }
    this.misses += 1;
    const value = await loader();
    await this.adapter.set(key, value, contract.ttlSeconds).catch(() => undefined);
    return value;
  }

  async invalidate(key: string): Promise<void> {
    this.invalidations += 1;
    await this.adapter.invalidate(key);
  }

  async invalidateByName(name: string): Promise<number> {
    const contract = this.contracts.get(name);
    if (!contract) return 0;
    this.invalidations += 1;
    // Without an index, we invalidate the key pattern prefix; the in-memory adapter
    // handles this. Real Redis adapter would use SCAN.
    await this.adapter.invalidate(contract.keyPattern.replace(/[:*]/g, ''));
    return 1;
  }

  metrics(): { hits: number; misses: number; invalidations: number; hitRatio: number } {
    const total = this.hits + this.misses;
    return {
      hits: this.hits,
      misses: this.misses,
      invalidations: this.invalidations,
      hitRatio: total === 0 ? 0 : this.hits / total,
    };
  }
}
