#!/usr/bin/env node
/**
 * Precompute Gateway — PR-5 Controlled Rollout
 * 
 * Grayscale ingress for the async precompute shadow.
 * 
 * Responsibilities:
 *   1. Check feature flag (default OFF)
 *   2. Check cache for existing precompute results
 *   3. Trigger precompute when beneficial
 *   4. Safety: ANY exception → return null (existing behavior)
 *   5. Metrics: counters for observability
 * 
 * Contract:
 *   - lookup(input) → null | PrecomputeOutput
 *   - null means "caller should proceed with existing behavior"
 *   - Never throws. Never blocks the main reply path.
 * 
 * Safety guarantees:
 *   - Cache corruption     → treated as miss, continue
 *   - Missing cache file   → treated as miss, continue
 *   - Search timeout       → return null, increment failures
 *   - Invalid query        → return null, increment failures
 *   - Empty memory index   → return null (no memories to offer)
 *   - Any unexpected error → return null, increment failures
 *   - Flag OFF             → return null, no search triggered
 * 
 * @module precompute-gateway
 */

import { isPrecomputeEnabled, getPrecomputeFlagRaw } from "./precompute-flag.mjs";
import { createPrecomputeRunner } from "./precompute-runner.mjs";
import { createPrecomputeCache } from "./precompute-cache.mjs";

// ─── Metrics ──────────────────────────────────────────

/**
 * Internal in-memory metrics store.
 * Read-only via getMetrics(). Only the gateway writes to it.
 */
function createMetricsStore() {
  let requests = 0;
  let hits = 0;
  let misses = 0;
  let failures = 0;
  let totalLatencyMs = 0;

  return {
    recordRequest() { requests++; },
    recordHit() { hits++; },
    recordMiss() { misses++; },
    recordFailure() { failures++; },
    recordLatency(ms) { totalLatencyMs += ms; },
    snapshot() {
      const avgMs = requests > 0
        ? Math.round(totalLatencyMs / requests)
        : 0;
      return {
        precompute_requests: requests,
        precompute_hits: hits,
        precompute_misses: misses,
        precompute_failures: failures,
        average_lookup_ms: avgMs,
        total_latency_ms: totalLatencyMs,
      };
    },
    reset() {
      requests = 0;
      hits = 0;
      misses = 0;
      failures = 0;
      totalLatencyMs = 0;
    },
  };
}

// ─── Gateway Factory ──────────────────────────────────

/**
 * @typedef {object} GatewayLookupInput
 * @property {string} sessionId
 * @property {Array<{role:string, content:string}>} [recentMessages]
 * @property {string} [queryHint]
 * @property {number} [limit]
 */

/**
 * Create a precompute gateway instance.
 * 
 * @param {string} workspaceRoot
 * @returns {object} { lookup, getMetrics, resetMetrics, isActive, getFlagRaw }
 */
export function createPrecomputeGateway(workspaceRoot) {
  const root = workspaceRoot;
  const metrics = createMetricsStore();
  const runner = createPrecomputeRunner(root);
  const cache = createPrecomputeCache(root);

  // Pre-create the cache dir so first call is not delayed
  try { runner.clearCache(); } catch { /* safe: cache dir creation deferred */ }

  /**
   * Main gateway lookup.
   * 
   * Fail-Open Contract:
   *   - Flag OFF             → return null (no search, no cache check)
   *   - Cache hit            → return cached result, increment hit
   *   - Cache miss           → run precompute, cache result, return
   *   - Any exception        → return null, increment failure
   *   - Search empty/no mems → return null (nothing to offer)
   * 
   * @param {GatewayLookupInput} input
   * @returns {Promise<object|null>} PrecomputeOutput or null
   */
  async function lookup(input) {
    const lookupStart = Date.now();

    try {
      // Phase 1: Check feature flag
      if (!isPrecomputeEnabled()) {
        // Flag OFF: no cache check, no search — pure pass-through
        return null;
      }

      metrics.recordRequest();

      // Phase 2: Check session validity
      const sessionId = input && input.sessionId;
      if (!sessionId || typeof sessionId !== "string" || sessionId.length === 0) {
        metrics.recordFailure();
        metrics.recordLatency(Date.now() - lookupStart);
        return null;
      }

      // Phase 3: Check cache hit first
      const messages = input.recentMessages || [];
      const cacheKey = cache.makeKey(sessionId, messages);
      const cached = safeCacheGet(cache, cacheKey);

      if (cached) {
        metrics.recordHit();
        metrics.recordLatency(Date.now() - lookupStart);
        return {
          ...cached,
          source: cached.source + " (precompute-cache)",
          lookupLatencyMs: Date.now() - lookupStart,
        };
      }

      metrics.recordMiss();

      // Phase 4: Trigger precompute (async, non-blocking for caller)
      const precomputeResult = await safeRun(runner, input);

      if (!precomputeResult) {
        // Search failed or returned nothing — fall through to null
        metrics.recordFailure();
        metrics.recordLatency(Date.now() - lookupStart);
        return null;
      }

      if (!precomputeResult.ok) {
        metrics.recordFailure();
        metrics.recordLatency(Date.now() - lookupStart);
        return null;
      }

      if (!precomputeResult.memories || precomputeResult.memories.length === 0) {
        // Empty memory index case: not a failure, just nothing to precompute
        metrics.recordLatency(Date.now() - lookupStart);
        return null;
      }

      // Phase 5: Cache the result for next time
      safeCacheSet(cache, cacheKey, {
        ok: precomputeResult.ok,
        sessionId: precomputeResult.sessionId,
        memories: precomputeResult.memories,
        source: precomputeResult.source,
        generatedAt: precomputeResult.generatedAt,
        ttlMs: precomputeResult.ttlMs,
      }, precomputeResult.ttlMs || 30 * 60 * 1000);

      metrics.recordLatency(Date.now() - lookupStart);

      return {
        ...precomputeResult,
        lookupLatencyMs: Date.now() - lookupStart,
      };
    } catch (err) {
      // Universal safety net: any exception → fall through
      try { metrics.recordFailure(); } catch {}
      try { metrics.recordLatency(Date.now() - lookupStart); } catch {}
      return null;
    }
  }

  /**
   * Whether the precompute gateway is active (flag ON).
   * Does not do any I/O.
   */
  function isActive() {
    try {
      return isPrecomputeEnabled();
    } catch {
      return false;
    }
  }

  /**
   * Get a snapshot of gateway metrics.
   * Read-only, no side effects.
   * 
   * @returns {object}
   */
  function getMetrics() {
    return metrics.snapshot();
  }

  /**
   * Reset all metrics counters (for testing).
   */
  function resetMetrics() {
    metrics.reset();
  }

  /**
   * Get raw flag value for diagnostics.
   */
  function getFlagRaw() {
    try {
      return getPrecomputeFlagRaw();
    } catch {
      return "unavailable";
    }
  }

  // ─── Safe Helpers ──────────────────────────────────

  /**
   * Safe cache get: any corruption → null
   */
  function safeCacheGet(cacheInstance, key) {
    try {
      return cacheInstance.get(key);
    } catch {
      return null;
    }
  }

  /**
   * Safe cache set: any write error → silently skip
   */
  function safeCacheSet(cacheInstance, key, data, ttl) {
    try {
      cacheInstance.set(key, data, ttl);
    } catch {
      // Cache write failure is non-fatal
    }
  }

  /**
   * Safe precompute run: any error → null
   */
  async function safeRun(runnerInstance, input) {
    try {
      return await runnerInstance.runWithCache(input);
    } catch {
      return null;
    }
  }

  return {
    lookup,
    getMetrics,
    resetMetrics,
    isActive,
    getFlagRaw,
    // Exposed for testing
    _cache: cache,
    _runner: runner,
    _metrics: metrics,
  };
}

export default createPrecomputeGateway;
