import type { ChannelDirectoryEntryKind, ChannelId } from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js";

type CacheEntry<T> = {
  value: T;
  fetchedAt: number;
};

export type DirectoryCacheKey = {
  channel: ChannelId;
  accountId?: string | null;
  kind: ChannelDirectoryEntryKind;
  source: "cache" | "live";
  signature?: string | null;
};

export function buildDirectoryCacheKey(key: DirectoryCacheKey): string {
  const signature = key.signature ?? "default";
  return `${key.channel}:${key.accountId ?? "default"}:${key.kind}:${key.source}:${signature}`;
}

export class DirectoryCache<T> {
  private readonly cache = new Map<string, CacheEntry<T>>();
  private lastConfigRef: ClawdbotConfig | null = null;
  private readonly maxSize: number;

  constructor(
    private readonly ttlMs: number,
    maxSize = 2000,
  ) {
    this.maxSize = Math.max(1, Math.floor(maxSize));
  }

  get(key: string, cfg: ClawdbotConfig): T | undefined {
    this.resetIfConfigChanged(cfg);
    this.pruneExpired(Date.now());
    const entry = this.cache.get(key);
    if (!entry) {
      return undefined;
    }
    return entry.value;
  }

  set(key: string, value: T, cfg: ClawdbotConfig): void {
    this.resetIfConfigChanged(cfg);
    const now = Date.now();
    this.pruneExpired(now);
    // Refresh insertion order so active keys are less likely to be evicted.
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    this.cache.set(key, { value, fetchedAt: now });
    this.evictToMaxSize();
  }

  clearMatching(match: (key: string) => boolean): void {
    for (const key of this.cache.keys()) {
      if (match(key)) {
        this.cache.delete(key);
      }
    }
  }

  clear(cfg?: ClawdbotConfig): void {
    this.cache.clear();
    if (cfg) {
      this.lastConfigRef = cfg;
    }
  }

  private resetIfConfigChanged(cfg: ClawdbotConfig): void {
    if (this.lastConfigRef && this.lastConfigRef !== cfg) {
      this.cache.clear();
    }
    this.lastConfigRef = cfg;
  }

  private pruneExpired(now: number): void {
    if (this.ttlMs <= 0) {
      return;
    }
    for (const [cacheKey, entry] of this.cache.entries()) {
      if (now - entry.fetchedAt > this.ttlMs) {
        this.cache.delete(cacheKey);
      }
    }
  }

  private evictToMaxSize(): void {
    while (this.cache.size > this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      if (typeof oldestKey !== "string") {
        break;
      }
      this.cache.delete(oldestKey);
    }
  }
}
