import type { ApiClient } from "./client.js";
import type { RequestScopedCache, ClientMemoryCache, ServerLruCache } from "./cache.js";

export interface CachedClientOptions {
  client: ApiClient;
  requestScoped?: RequestScopedCache | undefined;
  clientMemory?: ClientMemoryCache<NonNullable<unknown>> | undefined;
  serverLru?: ServerLruCache<NonNullable<unknown>> | undefined;
  ttlMs?: number | undefined;
}

export class CachedApiClient {
  private readonly client: ApiClient;
  private readonly requestScoped: RequestScopedCache | undefined;
  private readonly clientMemory: ClientMemoryCache<NonNullable<unknown>> | undefined;
  private readonly serverLru: ServerLruCache<NonNullable<unknown>> | undefined;
  private readonly ttlMs: number | undefined;

  constructor(options: CachedClientOptions) {
    this.client = options.client;
    this.requestScoped = options.requestScoped;
    this.clientMemory = options.clientMemory;
    this.serverLru = options.serverLru;
    this.ttlMs = options.ttlMs;
  }

  async get<T>(
    path: string,
    query?: Record<string, string | number | boolean>,
  ): Promise<T> {
    const key = this.buildKey(path, query);

    // (1) Check request-scoped cache (SSR dedup)
    if (this.requestScoped) {
      const cached = this.requestScoped.get<T>(key);
      if (cached) return cached;
    }

    // (2) Check client-memory cache
    if (this.clientMemory) {
      const cached = this.clientMemory.get(key) as T | undefined;
      if (cached !== undefined) return cached;
    }

    // (3) Check server LRU cache
    if (this.serverLru) {
      const cached = this.serverLru.get(key) as T | undefined;
      if (cached !== undefined) return cached;
    }

    // Cache miss — fetch from upstream
    const promise = this.client.get<T>(path, query);

    // Store the in-flight promise in request-scoped cache for dedup
    if (this.requestScoped) {
      this.requestScoped.set(key, promise);
    }

    const result = await promise;

    // Populate longer-lived caches with the resolved value
    if (this.clientMemory) {
      this.clientMemory.set(key, result as NonNullable<unknown>, this.ttlMs);
    }
    if (this.serverLru) {
      this.serverLru.set(key, result as NonNullable<unknown>, this.ttlMs);
    }

    return result;
  }

  private buildKey(
    path: string,
    query?: Record<string, string | number | boolean>,
  ): string {
    const q = query
      ? Object.keys(query)
          .sort()
          .map((k) => `${k}=${String(query[k])}`)
          .join("&")
      : "";
    return `${path}?${q}`;
  }
}
