import { TagCache, type CacheEvent, type CacheSetOptions, type TagCacheOptions } from "./memory.ts";

export type CacheLayerName = "data" | "component" | "page";

export interface CacheInspection {
  layers: Record<CacheLayerName, ReturnType<TagCache<unknown>["snapshot"]>>;
  recentEvents: Array<CacheEvent & { layer: CacheLayerName }>;
}

export interface CacheCoordinatorOptions extends Omit<TagCacheOptions, "onEvent"> {
  eventLimit?: number;
  onEvent?: (event: CacheEvent & { layer: CacheLayerName }) => void;
}

/** A request-lifetime cache: deduplicates work without leaking values between requests. */
export class RequestCache {
  private pending = new Map<string, Promise<unknown>>();

  getOrLoad<V>(key: string, loader: () => V | Promise<V>): Promise<V> {
    const existing = this.pending.get(key);
    if (existing) return existing as Promise<V>;
    const value = Promise.resolve().then(loader);
    this.pending.set(key, value);
    return value as Promise<V>;
  }

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

/** Owns the three cross-request cache layers and creates isolated request caches. */
export class CacheCoordinator {
  readonly data: TagCache<unknown>;
  readonly component: TagCache<unknown>;
  readonly page: TagCache<unknown>;
  private readonly events: Array<CacheEvent & { layer: CacheLayerName }> = [];
  private readonly eventLimit: number;

  constructor(options: CacheCoordinatorOptions = {}) {
    const { eventLimit = 200, onEvent, ...cacheOptions } = options;
    this.eventLimit = Math.max(1, eventLimit);
    const create = (layer: CacheLayerName) =>
      new TagCache<unknown>({
        ...cacheOptions,
        onEvent: (event) => {
          const item = { ...event, layer };
          this.events.push(item);
          if (this.events.length > this.eventLimit)
            this.events.splice(0, this.events.length - this.eventLimit);
          onEvent?.(item);
        },
      });
    this.data = create("data");
    this.component = create("component");
    this.page = create("page");
  }

  request(): RequestCache {
    return new RequestCache();
  }

  layer(name: CacheLayerName): TagCache<unknown> {
    return this[name];
  }

  getOrLoad<V>(
    layer: CacheLayerName,
    key: string,
    loader: () => V | Promise<V>,
    options?: CacheSetOptions,
  ): Promise<V> {
    return this.layer(layer).getOrLoad(key, loader, options) as Promise<V>;
  }

  invalidateTags(tags: Iterable<string>): number {
    return (
      this.data.invalidateTags(tags) +
      this.component.invalidateTags(tags) +
      this.page.invalidateTags(tags)
    );
  }

  inspect(): CacheInspection {
    return {
      layers: {
        data: this.data.snapshot(),
        component: this.component.snapshot(),
        page: this.page.snapshot(),
      },
      recentEvents: this.events.map((event) => ({ ...event, tags: event.tags && [...event.tags] })),
    };
  }

  clear(): void {
    this.data.clear();
    this.component.clear();
    this.page.clear();
  }
}
