export interface SenderCacheOptions {
  readonly ttlMs?: number;
  readonly maxSize?: number;
}

interface SenderCacheEntry {
  readonly name: string;
  readonly expiresAt: number;
}

const DEFAULT_TTL_MS = 600_000;
const DEFAULT_MAX_SIZE = 2_048;

export class SenderNameCache {
  private readonly ttlMs: number;
  private readonly maxSize: number;
  private readonly entries = new Map<string, SenderCacheEntry>();

  constructor(options: SenderCacheOptions = {}) {
    this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
    this.maxSize = Math.max(0, options.maxSize ?? DEFAULT_MAX_SIZE);
  }

  get(openId: string): string | null {
    const entry = this.entries.get(openId);
    if (entry === undefined) return null;

    if (entry.expiresAt <= Date.now()) {
      this.entries.delete(openId);
      return null;
    }

    this.entries.delete(openId);
    this.entries.set(openId, entry);
    return entry.name;
  }

  set(openId: string, name: string): void {
    if (this.maxSize === 0) return;

    this.entries.delete(openId);
    this.entries.set(openId, { name, expiresAt: Date.now() + this.ttlMs });

    while (this.entries.size > this.maxSize) {
      const oldest = this.entries.keys().next().value;
      if (oldest === undefined) break;
      this.entries.delete(oldest);
    }
  }

  async getOrFetch(
    openId: string,
    fetchFn: (openId: string) => Promise<string | null>,
  ): Promise<string | null> {
    const cached = this.get(openId);
    if (cached !== null) return cached;

    const fetched = await fetchFn(openId);
    if (fetched !== null) {
      this.set(openId, fetched);
    }
    return fetched;
  }

  clear(): void {
    this.entries.clear();
  }

  size(): number {
    this.pruneExpired();
    return this.entries.size;
  }

  private pruneExpired(): void {
    const now = Date.now();
    for (const [openId, entry] of this.entries) {
      if (entry.expiresAt <= now) {
        this.entries.delete(openId);
      }
    }
  }
}
