/**
 * Options for storing a cache value.
 */
export interface CacheSetOptions {
  /**
   * Time-to-live in seconds. Omit this for a value that does not expire.
   */
  ttlSeconds?: number;
}

/**
 * App-facing string cache port.
 *
 * Values are intentionally strings so adapters can map cleanly to Redis,
 * Upstash, and other key/value stores. Serialize structured values at the app
 * boundary.
 */
export interface CachePort {
  /**
   * Return a fresh value for `key`, or `null` when missing or expired.
   */
  get(key: string): Promise<string | null>;
  /**
   * Store a string value.
   */
  set(key: string, value: string, options?: CacheSetOptions): Promise<void>;
  /**
   * Delete a cache key.
   *
   * @returns `true` when the key existed.
   */
  delete(key: string): Promise<boolean>;
  /**
   * Return whether a fresh value exists for `key`.
   */
  has(key: string): Promise<boolean>;
  /**
   * Return a cached value or compute, store, and return a new value.
   *
   * Implementations are not required to provide single-flight behavior. If
   * concurrent cache fills matter, choose an adapter that documents that
   * guarantee or protect the factory at the application layer.
   */
  remember(
    key: string,
    factory: () => Promise<string>,
    options?: CacheSetOptions,
  ): Promise<string>;
}

type MemoryCacheEntry = {
  value: string;
  expiresAt: number | null;
};

function resolveExpiresAt(options: CacheSetOptions | undefined): number | null {
  if (options?.ttlSeconds == null) {
    return null;
  }

  if (options.ttlSeconds <= 0) {
    return Date.now();
  }

  return Date.now() + options.ttlSeconds * 1000;
}

function isExpired(entry: MemoryCacheEntry): boolean {
  return entry.expiresAt != null && entry.expiresAt <= Date.now();
}

/**
 * Create an in-memory cache for tests, examples, and single-process
 * development.
 *
 * This adapter is not durable or distributed. Values are lost when the process
 * exits and are not shared across workers, regions, or serverless invocations.
 *
 * @param initialValues - Optional initial string values without TTL.
 * @returns A cache port backed by a local `Map`.
 */
export function createMemoryCache(
  initialValues: Record<string, string> = {},
): CachePort {
  const values = new Map<string, MemoryCacheEntry>(
    Object.entries(initialValues).map(([key, value]) => [
      key,
      { value, expiresAt: null },
    ]),
  );

  async function getFreshEntry(key: string): Promise<MemoryCacheEntry | null> {
    const entry = values.get(key);
    if (!entry) {
      return null;
    }

    if (isExpired(entry)) {
      values.delete(key);
      return null;
    }

    return entry;
  }

  const cache: CachePort = {
    async get(key) {
      return (await getFreshEntry(key))?.value ?? null;
    },
    async set(key, value, options) {
      values.set(key, {
        value,
        expiresAt: resolveExpiresAt(options),
      });
    },
    async delete(key) {
      return values.delete(key);
    },
    async has(key) {
      return (await getFreshEntry(key)) != null;
    },
    async remember(key, factory, options) {
      const cached = await cache.get(key);
      if (cached != null) {
        return cached;
      }

      const value = await factory();
      await cache.set(key, value, options);
      return value;
    },
  };

  return cache;
}
