/*
 * Priority Forge — Idempotency-Key support for POST /tasks (CONC-007).
 *
 * Standard HTTP idempotency pattern: a client retrying a POST request
 * (e.g., after a timeout where the original response may or may not have
 * been received) sends the same Idempotency-Key header on both. The server
 * deduplicates: the first successful call's result is cached for 24h, and
 * subsequent calls with the same key return the same task instead of
 * creating a new one.
 *
 * Concurrency model:
 *   - `inFlight`: Map<key, Promise<task>> for requests currently executing.
 *     Two simultaneous requests with the same key share a single promise.
 *     The check-and-set is synchronous (no await between `.get` and `.set`)
 *     so Node's single-threaded event loop prevents racing.
 *   - `done`: Map<key, {taskId, expiresAt}> for completed requests within TTL.
 *     Looked up first so repeat requests return cached result without
 *     hitting createTask.
 *   - Failures are NOT cached — caller can retry the same key safely.
 *   - Cache is in-memory only; process restart wipes it. Safe because
 *     duplicate creates post-restart are vanishingly unlikely (and if they
 *     happen, the underlying createTask's TOCTOU fix / uniqueness constraint
 *     catches them).
 *
 * Eviction: a background timer removes expired entries every 10 minutes.
 * `.unref()` so it doesn't keep the process alive during tests.
 */

import { WeightedTask } from '../types/schema';

const TTL_MS = 24 * 60 * 60 * 1000; // 24h
const EVICT_INTERVAL_MS = 10 * 60 * 1000; // 10min

interface CachedEntry {
  taskId: string;
  expiresAt: number;
}

const inFlight = new Map<string, Promise<WeightedTask>>();
const done = new Map<string, CachedEntry>();

/**
 * Wrap a create operation with idempotency-key deduplication.
 *
 * @param key The client-provided Idempotency-Key header value.
 * @param factory Invoked to create the resource on first (non-cached, non-in-flight) request.
 * @param verifyExists Called on cached entries to detect stale state (e.g. task
 *   was deleted). If it returns null, the cache entry is dropped and the
 *   factory is invoked fresh.
 * @returns The task (cached, in-flight, or freshly created).
 */
export async function withIdempotencyKey(
  key: string,
  factory: () => Promise<WeightedTask>,
  verifyExists: (taskId: string) => Promise<WeightedTask | null>,
): Promise<WeightedTask> {
  // Step 1: done-cache lookup (sync). If still valid AND the task still
  // exists, short-circuit with the cached result.
  const cached = done.get(key);
  if (cached && cached.expiresAt > Date.now()) {
    const existing = await verifyExists(cached.taskId);
    if (existing) return existing;
    // Stale cache — underlying task was deleted. Drop and re-create.
    done.delete(key);
  }

  // Step 2: in-flight check-and-set (atomic — Map ops are sync, no yield
  // between get and set). If another request with this key is already
  // executing, share its promise.
  let p = inFlight.get(key);
  if (!p) {
    p = factory()
      .then((result) => {
        done.set(key, { taskId: result.id, expiresAt: Date.now() + TTL_MS });
        return result;
      })
      .finally(() => {
        inFlight.delete(key);
      });
    inFlight.set(key, p);
  }
  return p;
}

// Eviction timer. Unref'd so tests and short-lived processes can exit cleanly.
const evictor = setInterval(() => {
  const now = Date.now();
  for (const [key, entry] of done) {
    if (entry.expiresAt <= now) done.delete(key);
  }
}, EVICT_INTERVAL_MS);
evictor.unref();

/**
 * Test-only: clear all idempotency state. Exported with an underscore
 * prefix to discourage production use.
 */
export function _clearIdempotencyForTests(): void {
  inFlight.clear();
  done.clear();
}

/** Test-only: inspect current cache size. */
export function _idempotencyCacheSizes(): { inFlight: number; done: number } {
  return { inFlight: inFlight.size, done: done.size };
}
