// v2.8.0: Workflow resumability cache — SHA-256 hash-based in-memory cache
// for completed agent task results. When a workflow re-runs, completed agents
// with unchanged specs skip execution and return cached results.
//
// The cache is purely in-memory (Map). No DB persistence for v1.
// All functions are exported for testing.

import { createHash } from 'node:crypto';
import type { AgentTaskSpec } from './types.js';

/**
 * Shape of a cached agent task result. Mirrors the successful fields of
 * `AgentTaskResult` without the runtime-only `cached` flag.
 */
export interface CachedResult {
  ok: boolean;
  output: unknown;
  error?: string;
  token_usage?: { prompt: number; completion: number };
}

/**
 * Internal cache entry with insertion timestamp for TTL support.
 */
interface CacheEntry {
  result: CachedResult;
  insertedAt: number;
}

/**
 * Default TTL for cached entries (30 minutes).
 * After this period entries are considered stale and are evicted on access.
 */
const DEFAULT_TTL_MS = 1_800_000;

/**
 * Maximum number of entries before the cache starts evicting oldest entries.
 */
const MAX_ENTRIES = 500;

/**
 * In-memory cache store: SHA-256 hash → cached result.
 */
const cache = new Map<string, CacheEntry>();

/**
 * Build a deterministic SHA-256 hash for an agent task specification.
 *
 * The hash is computed from a stable-ordered JSON serialisation of the spec
 * (prompt + options) so that identical specs always produce the same key
 * regardless of JavaScript property insertion order.
 *
 * @param spec   - The agent task specification (prompt, options, etc.).
 * @param args   - Additional arguments string (e.g. workflow args fingerprint).
 * @returns A 64-character hex SHA-256 digest.
 */
export function cacheKey(spec: AgentTaskSpec, args: string): string {
  const hash = createHash('sha256');

  // Stable-sorted serialisation of the spec
  hash.update(stableJson(spec));

  // Append the args fingerprint
  hash.update('\0');
  hash.update(args);

  return hash.digest('hex');
}

/**
 * Look up a cached result by its cache key.
 *
 * Returns `null` when:
 *   - The key doesn't exist in the cache.
 *   - The cached entry has exceeded the TTL (evicted silently).
 *
 * @param key - The SHA-256 hex key returned by `cacheKey()`.
 * @returns The cached result, or `null` if not found or expired.
 */
export function getCachedResult(key: string): CachedResult | null {
  const entry = cache.get(key);
  if (!entry) return null;

  // TTL check — stale entries are evicted on access
  if (Date.now() - entry.insertedAt > DEFAULT_TTL_MS) {
    cache.delete(key);
    return null;
  }

  return entry.result;
}

/**
 * Store an agent task result in the cache.
 *
 * If the cache has reached `MAX_ENTRIES`, the oldest entry (by insertion time)
 * is evicted first. This is a simple FIFO eviction — not a full LRU — because
 * workflow runs are expected to exhibit high temporal locality (recently
 * completed steps in the current run are the most likely to be re-queried).
 *
 * @param key    - The SHA-256 hex key returned by `cacheKey()`.
 * @param result - The result to cache.
 */
export function setCachedResult(key: string, result: CachedResult): void {
  // Evict oldest entry if at capacity
  if (cache.size >= MAX_ENTRIES) {
    let oldestKey: string | undefined;
    let oldestTime = Infinity;

    for (const [k, entry] of cache) {
      if (entry.insertedAt < oldestTime) {
        oldestTime = entry.insertedAt;
        oldestKey = k;
      }
    }

    if (oldestKey) {
      cache.delete(oldestKey);
    }
  }

  cache.set(key, {
    result,
    insertedAt: Date.now(),
  });
}

/**
 * Invalidate all cached entries that were produced during a specific workflow
 * run. The `runKey` is matched as a prefix of the cache key — this works
 * because `cacheKey()` incorporates the args string, and the caller passes
 * a run-specific token as the `args` parameter.
 *
 * @param runKey - The run-specific key prefix to invalidate.
 */
export function invalidateRun(runKey: string): void {
  for (const key of cache.keys()) {
    if (key.startsWith(runKey)) {
      cache.delete(key);
    }
  }
}

/**
 * Clear the entire cache. Used for testing and manual reset.
 */
export function clearCache(): void {
  cache.clear();
}

/**
 * Return the current number of entries in the cache.
 * Useful for testing assertions.
 */
export function cacheSize(): number {
  return cache.size;
}

/**
 * Stable JSON serialisation that produces the same output string for the same
 * data regardless of JavaScript object property insertion order.
 *
 * - Object keys are sorted lexicographically.
 * - Arrays preserve their element order.
 * - Primitives are serialised via `JSON.stringify`.
 */
function stableJson(value: unknown): string {
  if (value === null) return 'null';
  if (typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) {
    return `[${value.map(stableJson).join(',')}]`;
  }
  const keys = Object.keys(value as Record<string, unknown>).sort();
  const pairs = keys.map(
    (k) =>
      `${JSON.stringify(k)}:${stableJson((value as Record<string, unknown>)[k])}`,
  );
  return `{${pairs.join(',')}}`;
}
