#!/usr/bin/env node
/**
 * Precompute Shadow Integration — PR-6 Gateway Shadow Integration
 *
 * Connects the precompute gateway to the active-memory hook path as a
 * SHADOW OBSERVER only. Never modifies the primary recall result.
 *
 * Architecture:
 *
 *   before_agent_reply (hook lifecycle)
 *     │
 *     ├─ 1. Primary Recall (existing blocking, runs always)
 *     │     └─ Returns memories → agent context augmentation
 *     │
 *     ├─ 2. Shadow Precompute (only when flag ON)
 *     │     └─ gateway.lookup() → metrics only
 *     │
 *     └─ 3. Return primary result (NEVER shadow)
 *
 * Fail-Open Guarantee:
 *   - Any shadow error → silently swallowed
 *   - Primary recall always runs and returns
 *   - Shadow timeout: shadow latency is measured but NOT subtracted
 *     from user-visible time (shadow runs AFTER primary returns)
 *
 * @module precompute-shadow-integration
 */

import { isPrecomputeEnabled, getPrecomputeFlagRaw, isPrecomputeReplaceEnabled, getReplaceFlagRaw, isKillSwitchActive, getKillSwitchRaw, getRolloutPercent, getRolloutPercentRaw } from "./precompute-flag.mjs";
import { createPrecomputeGateway } from "./precompute-gateway.mjs";
import { computeShadowDiff } from "./precompute-shadow-diff.mjs";
import { evaluateReadiness } from "./precompute-readiness-gate.mjs";
import { selectReplacementResult } from "./precompute-replacement-policy.mjs";
import { createCircuitBreaker } from "./precompute-circuit-breaker.mjs";
import { checkRolloutEnrollment } from "./precompute-rollout-controller.mjs";

// ─── Shadow Metrics & Diff Store ─────────────────────

/**
 * Creates an in-memory metrics store for shadow observations.
 * Separate from gateway metrics for clear accountability.
 */
function createShadowMetricsStore() {
  let totalShadowCalls = 0;
  let shadowHits = 0;
  let shadowMisses = 0;
  let shadowErrors = 0;
  let shadowTimeouts = 0;
  let totalShadowLatencyMs = 0;

  // ── PR-7 Diff tracking ──
  let lastDiff = null;
  let diffResults = [];
  let totalOverlapRatio = 0;
  let totalTopKOverlap = 0;
  let sameTopResultCount = 0;
  let diffCount = 0;

  // ── PR-11 Rollout tracking ──
  let rolloutRequests = 0;
  let rolloutEnrolled = 0;
  let replacementSelected = 0;
  let replacementFallback = 0;
  let replacementSuccess = 0;
  let replacementFailure = 0;
  let replacementTimeout = 0;
  let replacementMismatch = 0;

  return {
    recordHit(latencyMs) {
      totalShadowCalls++;
      shadowHits++;
      totalShadowLatencyMs += latencyMs;
    },
    recordMiss(latencyMs) {
      totalShadowCalls++;
      shadowMisses++;
      totalShadowLatencyMs += latencyMs;
    },
    recordError() {
      totalShadowCalls++;
      shadowErrors++;
    },
    recordTimeout(latencyMs) {
      totalShadowCalls++;
      shadowTimeouts++;
      totalShadowLatencyMs += latencyMs;
    },
    /**
     * PR-7: Store a diff result from comparing primary vs shadow.
     */
    recordDiff(diff) {
      if (!diff) return;
      lastDiff = diff;
      diffResults.push(diff);
      diffCount++;
      totalOverlapRatio += diff.overlapRatio || 0;
      totalTopKOverlap += diff.topKOverlap || 0;
      if (diff.sameTopResult) {
        sameTopResultCount++;
      }
    },
    /**
     * Get the most recent diff result.
     */
    getLastDiff() {
      return lastDiff;
    },
    /**
     * Get accumulated diff statistics.
     */
    getDiffStats() {
      if (diffCount === 0) {
        return {
          diff_count: 0,
          avg_overlap_ratio: 0,
          avg_topk_overlap: 0,
          same_top_result_rate: 0,
          last_diff: null,
        };
      }
      return {
        diff_count: diffCount,
        avg_overlap_ratio: Math.round((totalOverlapRatio / diffCount) * 10000) / 10000,
        avg_topk_overlap: Math.round((totalTopKOverlap / diffCount) * 100) / 100,
        same_top_result_rate: Math.round((sameTopResultCount / diffCount) * 10000) / 10000,
        last_diff: lastDiff,
      };
    },

    // ── PR-11 Rollout recording ──

    /** Record a request that went through rollout check. */
    recordRolloutRequest() { rolloutRequests++; },
    /** Record an enrolled request. */
    recordRolloutEnrolled() { rolloutEnrolled++; },
    /** Record replacement was selected by policy. */
    recordReplacementSelected() { replacementSelected++; },
    /** Record replacement fell back to primary. */
    recordReplacementFallback() { replacementFallback++; },
    /** Record replacement succeeded (healthy diff). */
    recordReplacementSuccess() { replacementSuccess++; },
    /** Record replacement failed (error). */
    recordReplacementFailure() { replacementFailure++; },
    /** Record replacement timed out. */
    recordReplacementTimeout() { replacementTimeout++; },
    /** Record replacement result mismatch. */
    recordReplacementMismatch() { replacementMismatch++; },

    /**
     * Get PR-11 rollout-specific metrics.
     */
    getRolloutMetrics() {
      const total = rolloutRequests || 0;
      return {
        rollout_requests: total,
        rollout_enrolled: rolloutEnrolled,
        replacement_selected: replacementSelected,
        replacement_fallback: replacementFallback,
        replacement_success: replacementSuccess,
        replacement_failure: replacementFailure,
        replacement_timeout: replacementTimeout,
        replacement_mismatch: replacementMismatch,
        // Computed rates
        selection_rate: total > 0 ? Math.round((replacementSelected / total) * 10000) / 10000 : 0,
        fallback_rate: total > 0 ? Math.round((replacementFallback / total) * 10000) / 10000 : 0,
        timeout_rate: total > 0 ? Math.round((replacementTimeout / total) * 10000) / 10000 : 0,
        mismatch_rate: total > 0 ? Math.round((replacementMismatch / total) * 10000) / 10000 : 0,
      };
    },

    snapshot() {
      const diffStats = this.getDiffStats();
      const rollout = this.getRolloutMetrics();
      return {
        shadow_total_calls: totalShadowCalls,
        shadow_hits: shadowHits,
        shadow_misses: shadowMisses,
        shadow_errors: shadowErrors,
        shadow_timeouts: shadowTimeouts,
        shadow_avg_latency_ms: totalShadowCalls > 0
          ? Math.round(totalShadowLatencyMs / totalShadowCalls)
          : 0,
        shadow_total_latency_ms: totalShadowLatencyMs,
        // PR-7 diff observability
        shadow_diff_count: diffStats.diff_count,
        shadow_avg_overlap_ratio: diffStats.avg_overlap_ratio,
        shadow_avg_topk_overlap: diffStats.avg_topk_overlap,
        shadow_same_top_result_rate: diffStats.same_top_result_rate,
        // PR-11 rollout metrics
        rollout,
      };
    },
    reset() {
      totalShadowCalls = 0;
      shadowHits = 0;
      shadowMisses = 0;
      shadowErrors = 0;
      shadowTimeouts = 0;
      totalShadowLatencyMs = 0;
      // PR-7: reset diff tracking
      lastDiff = null;
      diffResults = [];
      totalOverlapRatio = 0;
      totalTopKOverlap = 0;
      sameTopResultCount = 0;
      diffCount = 0;
      // PR-11: reset rollout tracking
      rolloutRequests = 0;
      rolloutEnrolled = 0;
      replacementSelected = 0;
      replacementFallback = 0;
      replacementSuccess = 0;
      replacementFailure = 0;
      replacementTimeout = 0;
      replacementMismatch = 0;
    },
  };
}

// ─── Shadow Integration Factory ──────────────────────

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

/**
 * Create a shadow integration instance.
 *
 * @param {string} workspaceRoot - Absolute path to workspace
 * @returns {object} { shadowBeforeAgentReply, getShadowMetrics, resetShadowMetrics,
 *                     getGatewayMetrics, isShadowActive, getFlagRaw }
 */
export function createShadowIntegration(workspaceRoot) {
  const root = workspaceRoot;
  const shadowMetrics = createShadowMetricsStore();
  /** PR-10: Circuit breaker instance */
  const breaker = createCircuitBreaker();

  // Gateway is created lazily on first use to avoid side effects during import
  let _gateway = null;
  let _pendingShadow = Promise.resolve();
  // PR-7: Snapshot of primary result for diff computation (set before each shadow run)
  let _lastPrimaryResult = null;

  function getGateway() {
    if (!_gateway) {
      _gateway = createPrecomputeGateway(root);
    }
    return _gateway;
  }

  /**
   * Shadow integration for the before_agent_reply hook lifecycle.
   *
   * Contract:
   *   1. Always calls primaryRecallFn first and returns its result
   *   2. After primary returns, optionally runs gateway lookup as shadow
   *   3. Shadow result is NEVER returned to caller — only used for metrics
   *   4. Any shadow error is silently swallowed
   *
   * Usage in hook:
   *   const integration = createShadowIntegration(workspaceRoot);
   *   // In before_agent_reply:
   *   const memories = await integration.shadowBeforeAgentReply(
   *     () => existingBlockingRecall(messages),
   *     { sessionId, messages, queryHint }
   *   );
   *
   * @param {Function} primaryRecallFn - Async function that performs the existing recall
   * @param {HookContext} hookContext - Context for shadow precompute
   * @param {number} [shadowTimeoutMs] - Max time for shadow to complete (default: 5000ms)
   * @returns {Promise<any>} The result from primaryRecallFn (unchanged)
   */
  async function shadowBeforeAgentReply(primaryRecallFn, hookContext, shadowTimeoutMs = 5000) {
    // Step 1: Always run primary recall first
    let primaryResult;
    try {
      primaryResult = await primaryRecallFn();
    } catch (primaryError) {
      // If primary fails, re-throw — we never swallow primary errors
      throw primaryError;
    }

    // Step 2: Check flag — shadow only runs when enabled
    let shadowActive = false;
    try {
      shadowActive = isPrecomputeEnabled();
    } catch {
      shadowActive = false;
    }

    if (!shadowActive) {
      // Flag OFF: no shadow, no gateway call, no cache check, no FTS5 query
      return primaryResult;
    }

    // Step 3: Capture primary result snapshot for diff (PR-7)
    _lastPrimaryResult = primaryResult;

    // Step 4: Run shadow precompute (fire-and-forget safety, tracked for test flush)
    _pendingShadow = _runShadow(getGateway(), hookContext, shadowTimeoutMs);

    // Step 5: Always return primary result immediately
    return primaryResult;
  }

  /**
   * Internal shadow runner — all errors swallowed.
   * Runs AFTER primary recall so it never affects user-visible latency.
   *
   * PR-7: after shadow completes, computes diff with primary result
   * and stores it in metrics for offline analysis.
   */
  async function _runShadow(gw, hookContext, timeoutMs) {
    const sessionId = (hookContext && hookContext.sessionId) || "shadow-unknown";
    const messages = (hookContext && hookContext.messages) || [];
    const queryHint = (hookContext && hookContext.queryHint) || undefined;
    const limit = (hookContext && hookContext.limit) || 5;
    const shadowStart = Date.now();

    // Capture primary result snapshot for diff comparison.
    // This is the same object already returned to the caller — no new data exposed.
    const primaryForDiff = _lastPrimaryResult;

    let shadowResult = null;

    try {
      // Race the gateway lookup against a timeout
      const shadowPromise = gw.lookup({ sessionId, recentMessages: messages, queryHint, limit });
      const timeoutPromise = new Promise((_, reject) =>
        setTimeout(() => reject(new Error("SHADOW_TIMEOUT")), timeoutMs)
      );

      shadowResult = await Promise.race([shadowPromise, timeoutPromise]);

      const elapsed = Date.now() - shadowStart;

      if (shadowResult === null) {
        // Gateway returned null (miss, no memories, or flag was off mid-flight)
        shadowMetrics.recordMiss(elapsed);
      } else if (shadowResult && shadowResult.memories && shadowResult.memories.length > 0) {
        // Gateway had a cache hit or search result
        shadowMetrics.recordHit(elapsed);
      } else {
        // Gateway returned a result but no memories
        shadowMetrics.recordMiss(elapsed);
      }

      // PR-7: Compute diff when shadow completed successfully
      _recordDiff(primaryForDiff, shadowResult, elapsed, hookContext, limit);
    } catch (shadowError) {
      const elapsed = Date.now() - shadowStart;
      const isTimeout = shadowError && (
        shadowError.message === "SHADOW_TIMEOUT" ||
        shadowError.code === "SHADOW_TIMEOUT" ||
        (shadowError.name === "TimeoutError")
      );

      if (isTimeout) {
        shadowMetrics.recordTimeout(elapsed);
        // PR-7: Record diff with shadow_timeout status
        _recordTimeoutDiff(primaryForDiff, elapsed);
      } else {
        shadowMetrics.recordError();
        // PR-7: Record diff with shadow_error status
        _recordErrorDiff(primaryForDiff, elapsed);
      }
    }
  }

  /**
   * Check if shadow is active (flag ON).
   */
  function isShadowActive() {
    try {
      return isPrecomputeEnabled();
    } catch {
      return false;
    }
  }

  /**
   * Get shadow-specific metrics.
   * Read-only snapshot — never affects behavior.
   */
  function getShadowMetrics() {
    return shadowMetrics.snapshot();
  }

  /**
   * Get gateway-level metrics (precompute_hits, etc.).
   */
  function getGatewayMetrics() {
    try {
      return getGateway().getMetrics();
    } catch {
      return null;
    }
  }

  /**
   * Reset shadow metrics.
   */
  function resetShadowMetrics() {
    shadowMetrics.reset();
  }

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

  /**
   * Record a normal diff with a shadow result.
   * Uses the pure computeShadowDiff function. Never throws.
   */
  function _recordDiff(primaryResult, shadowResult, elapsed, hCtx, limitVal) {
    try {
      const diff = computeShadowDiff({
        primaryResult,
        shadowResult,
        hookContext: hCtx,
        latencyMs: elapsed,
        topK: limitVal || 5,
      });
      shadowMetrics.recordDiff(diff);
    } catch {
      // Diff computation is pure and should never throw, but guard anyway.
      shadowMetrics.recordDiff({
        status: "diff_error",
        primaryCount: 0,
        shadowCount: 0,
        overlapCount: 0,
        primaryOnlyCount: 0,
        shadowOnlyCount: 0,
        overlapRatio: 0,
        primaryOnly: [],
        shadowOnly: [],
        sameTopResult: true,
        topKOverlap: 0,
        latencyMs: elapsed,
      });
    }
  }

  /**
   * Record a diff when shadow timed out.
   */
  function _recordTimeoutDiff(primaryResult, elapsed) {
    try {
      let primaryCount = 0;
      if (primaryResult) {
        if (Array.isArray(primaryResult)) {
          primaryCount = primaryResult.length;
        } else if (primaryResult && Array.isArray(primaryResult.memories)) {
          primaryCount = primaryResult.memories.length;
        }
      }
      shadowMetrics.recordDiff({
        status: "shadow_timeout",
        primaryCount,
        shadowCount: 0,
        overlapCount: 0,
        primaryOnlyCount: primaryCount,
        shadowOnlyCount: 0,
        overlapRatio: 0,
        primaryOnly: [],
        shadowOnly: [],
        sameTopResult: false,
        topKOverlap: 0,
        latencyMs: elapsed,
      });
    } catch {
      // swallow
    }
  }

  /**
   * Record a diff when shadow errored.
   */
  function _recordErrorDiff(primaryResult, elapsed) {
    try {
      let primaryCount = 0;
      if (primaryResult) {
        if (Array.isArray(primaryResult)) {
          primaryCount = primaryResult.length;
        } else if (primaryResult && Array.isArray(primaryResult.memories)) {
          primaryCount = primaryResult.memories.length;
        }
      }
      shadowMetrics.recordDiff({
        status: "shadow_error",
        primaryCount,
        shadowCount: 0,
        overlapCount: 0,
        primaryOnlyCount: primaryCount,
        shadowOnlyCount: 0,
        overlapRatio: 0,
        primaryOnly: [],
        shadowOnly: [],
        sameTopResult: false,
        topKOverlap: 0,
        latencyMs: elapsed,
      });
    } catch {
      // swallow
    }
  }

  /**
   * PR-9 + PR-10: Controlled replacement entry point for before_agent_reply hook.
   *
   * Default behavior: identical to shadowBeforeAgentReply (shadow only, no replacement).
   * When replacement flag ON + kill switch OFF + circuit closed + readiness pass + precompute valid:
   *   returns precompute result instead of primary.
   *
   * PR-10 additions:
   *   - Kill switch check (emergency disable)
   *   - Circuit breaker check (automatic degradation)
   *   - Breaker event recording (success/failure/timeout/mismatch)
   *
   * Contract:
   *   1. Always runs primary recall first
   *   2. Always has primaryResult available as fallback
   *   3. Any exception → returns primaryResult (fail-open)
   *   4. dryRun mode → returns primaryResult but records decision
   *   5. precompute errors/timeouts → silently handled, primary returned, breaker notified
   *   6. Kill switch active → primary forced
   *   7. Circuit breaker open → primary forced
   *
   * @param {Function} primaryRecallFn - Async function that performs existing recall
   * @param {HookContext} hookContext - Context for precompute
   * @param {object} [replacementOptions]
   * @param {boolean} [replacementOptions.dryRun] - If true, never returns precompute
   * @param {number} [replacementOptions.shadowTimeoutMs] - Shadow timeout (default: 5000ms)
   * @returns {Promise<{result: *, decision: object}>}
   *   - result: the selected result (primary or precompute)
   *   - decision: full policy decision object
   */
  async function controlledBeforeAgentReply(primaryRecallFn, hookContext, replacementOptions = {}) {
    const dryRun = replacementOptions.dryRun === true;
    const shadowTimeoutMs = replacementOptions.shadowTimeoutMs || 5000;

    // Step 1: Always run primary recall first (same as shadowBeforeAgentReply)
    let primaryResult;
    try {
      primaryResult = await primaryRecallFn();
    } catch (primaryError) {
      // Primary failure is always re-thrown — never swallowed
      throw primaryError;
    }

    // Step 2: Check kill switch (PR-10) — takes priority over everything
    let killSwitchActive = false;
    try {
      killSwitchActive = isKillSwitchActive();
    } catch {
      killSwitchActive = false;
    }

    if (killSwitchActive) {
      // Kill switch ON: forced primary, no precompute, minimal work
      return {
        result: primaryResult,
        decision: {
          source: "primary",
          decision: "kill_switch_active",
          safeToReplace: false,
          reasons: ["Emergency kill switch is active — replacement forcibly disabled"],
          metrics: { replaceEnabled: false, dryRun, killSwitchActive: true },
        },
      };
    }

    // Step 3: Check replacement flag
    let replaceActive = false;
    try {
      replaceActive = isPrecomputeReplaceEnabled();
    } catch {
      replaceActive = false;
    }

    if (!replaceActive) {
      // Replacement flag OFF: behave like shadowBeforeAgentReply
      _lastPrimaryResult = primaryResult;
      _pendingShadow = _runShadow(getGateway(), hookContext, shadowTimeoutMs);
      return {
        result: primaryResult,
        decision: {
          source: "primary",
          decision: "disabled",
          safeToReplace: false,
          reasons: ["Replacement flag OFF — shadow observer mode"],
          metrics: { replaceEnabled: false, dryRun, killSwitchActive: false },
        },
      };
    }

    // Step 4: Check circuit breaker (PR-10)
    const circuitAllowed = breaker.shouldAllowReplacement();
    const circuitState = breaker.getState();

    // Step 5: Check rollout controller (PR-11)
    // If rollout > 0%, only enrolled requests may use replacement.
    // Non-enrolled requests behave like shadow observer mode.
    // rollout=0% → behavior unchanged from PR-10 (rollout disabled, no gating)
    let rolloutPercent = 0;
    let rolloutEnrollment = null;
    try {
      rolloutPercent = getRolloutPercent();
      const requestId = (hookContext && hookContext.sessionId) || "rollout-unknown";
      rolloutEnrollment = checkRolloutEnrollment({ rolloutPercent, requestId });
      shadowMetrics.recordRolloutRequest();
    } catch {
      rolloutPercent = 0;
      rolloutEnrollment = { enrolled: false, bucket: -1, rolloutPercent: 0 };
      shadowMetrics.recordRolloutRequest();
    }

    // If rollout is active but request not enrolled → shadow mode
    if (rolloutPercent > 0 && !rolloutEnrollment.enrolled) {
      _lastPrimaryResult = primaryResult;
      _pendingShadow = _runShadow(getGateway(), hookContext, shadowTimeoutMs);
      return {
        result: primaryResult,
        decision: {
          source: "primary",
          decision: "rollout_not_enrolled",
          safeToReplace: false,
          reasons: [`rollout at ${rolloutPercent}% — request not in enrolled bucket (bucket ${rolloutEnrollment.bucket})`],
          metrics: { replaceEnabled: true, dryRun, killSwitchActive, circuitState, circuitAllowed, rolloutPercent, rolloutEnrolled: false },
        },
      };
    }

    // Track enrolled requests
    if (rolloutEnrollment && rolloutEnrollment.enrolled) {
      shadowMetrics.recordRolloutEnrolled();
    }

    // Step 6: Run precompute for readiness evaluation + potential replacement
    _lastPrimaryResult = primaryResult;
    let precomputeResult = null;
    let shadowElapsed = 0;
    const shadowStart = Date.now();

    try {
      const shadowPromise = getGateway().lookup({
        sessionId: (hookContext && hookContext.sessionId) || "replacement-unknown",
        recentMessages: (hookContext && hookContext.messages) || [],
        queryHint: (hookContext && hookContext.queryHint) || undefined,
        limit: (hookContext && hookContext.limit) || 5,
      });
      const timeoutPromise = new Promise((_, reject) =>
        setTimeout(() => reject(new Error("SHADOW_TIMEOUT")), shadowTimeoutMs)
      );

      precomputeResult = await Promise.race([shadowPromise, timeoutPromise]);
      shadowElapsed = Date.now() - shadowStart;

      // Record shadow metrics
      if (precomputeResult === null) {
        shadowMetrics.recordMiss(shadowElapsed);
      } else if (precomputeResult && precomputeResult.memories && precomputeResult.memories.length > 0) {
        shadowMetrics.recordHit(shadowElapsed);
      } else {
        shadowMetrics.recordMiss(shadowElapsed);
      }
      _recordDiff(primaryResult, precomputeResult, shadowElapsed, hookContext, hookContext && hookContext.limit);
    } catch (shadowError) {
      const elapsed = Date.now() - shadowStart;
      const isTimeout = shadowError && (
        shadowError.message === "SHADOW_TIMEOUT" ||
        shadowError.code === "SHADOW_TIMEOUT" ||
        (shadowError.name === "TimeoutError")
      );
      if (isTimeout) {
        shadowMetrics.recordTimeout(elapsed);
        shadowMetrics.recordReplacementTimeout();
        shadowMetrics.recordReplacementFallback();
        _recordTimeoutDiff(primaryResult, elapsed);
        // PR-10: Record timeout to circuit breaker
        try { breaker.recordTimeout(); } catch {}
      } else {
        shadowMetrics.recordError();
        shadowMetrics.recordReplacementFailure();
        shadowMetrics.recordReplacementFallback();
        _recordErrorDiff(primaryResult, elapsed);
        // PR-10: Record failure to circuit breaker
        try { breaker.recordFailure(shadowError.message); } catch {}
      }
      // Precompute error → skip policy, return primary
      return {
        result: primaryResult,
        decision: {
          source: "primary",
          decision: "precompute_error",
          safeToReplace: false,
          reasons: ["Precompute lookup failed — falling back to primary"],
          metrics: { replaceEnabled: true, dryRun, killSwitchActive, circuitState, circuitAllowed, precomputeError: true },
        },
      };
    }

    // Step 6: Evaluate readiness from accumulated metrics
    let readinessResult;
    try {
      const summary = _buildReadinessSummary();
      readinessResult = evaluateReadiness(summary, { topK: hookContext && hookContext.limit ? hookContext.limit : 5 });
    } catch {
      readinessResult = { status: "fail", safeToReplace: false };
    }

    // Step 7: Run replacement policy with kill switch and circuit breaker context
    let policyDecision;
    try {
      policyDecision = selectReplacementResult({
        primaryResult,
        precomputeResult,
        readiness: readinessResult,
        hookContext,
        killSwitch: killSwitchActive,
        circuitState,
        circuitAllowed,
        options: { replaceEnabled: true, dryRun },
      });
    } catch {
      // Policy failure → safe fallback to primary
      policyDecision = {
        selectedResult: primaryResult,
        source: "primary",
        decision: "fallback_primary",
        safeToReplace: false,
        reasons: ["Replacement policy evaluation failed — fallback to primary"],
        metrics: { replaceEnabled: true, dryRun, killSwitchActive, policyError: true },
      };
    }

    // Step 8: Record rollout metrics (PR-11)
    if (policyDecision.decision === "selected_precompute") {
      shadowMetrics.recordReplacementSelected();
    } else if (policyDecision.source === "primary" && rolloutEnrollment && rolloutEnrollment.enrolled) {
      shadowMetrics.recordReplacementFallback();
    }

    // Step 9: Circuit breaker feedback (PR-10)
    // Record mismatch/success based on the actual outcome and diff quality
    if (policyDecision.decision === "selected_precompute") {
      // Check diff quality for mismatch detection
      const lastDiff = shadowMetrics.getLastDiff();
      if (lastDiff) {
        const hasSevereMismatch = lastDiff.status !== "ok" ||
          (lastDiff.overlapRatio !== undefined && lastDiff.overlapRatio < 0.5) ||
          (lastDiff.sameTopResult === false && lastDiff.primaryCount > 0);
        if (hasSevereMismatch) {
          try { breaker.recordMismatch(); } catch {}
          shadowMetrics.recordReplacementMismatch();
        } else {
          try { breaker.recordSuccess(); } catch {}
          shadowMetrics.recordReplacementSuccess();
        }
      } else {
        try { breaker.recordSuccess(); } catch {}
        shadowMetrics.recordReplacementSuccess();
      }
    }

    return {
      result: policyDecision.selectedResult,
      decision: policyDecision,
    };
  }

  /**
   * PR-9: Build a readiness-compatible summary from accumulated shadow metrics.
   * Converts internal shadow metrics to the format expected by evaluateReadiness().
   */
  function _buildReadinessSummary() {
    const metrics = shadowMetrics.snapshot();
    const diffStats = shadowMetrics.getDiffStats();

    // Compute shadow rate metrics from counters
    const totalCalls = metrics.shadow_total_calls || 0;

    return {
      totalSamples: diffStats.diff_count || 0,
      avgOverlapRatio: diffStats.avg_overlap_ratio || 0,
      avgTopKOverlap: diffStats.avg_topk_overlap || 0,
      sameTopResultRate: diffStats.same_top_result_rate || 0,
      shadowErrorRate: totalCalls > 0
        ? (metrics.shadow_errors || 0) / totalCalls
        : 0,
      shadowTimeoutRate: totalCalls > 0
        ? (metrics.shadow_timeouts || 0) / totalCalls
        : 0,
    };
  }

  /**
   * PR-9: Check if replacement is active (flag ON).
   */
  function isReplaceActive() {
    try {
      return isPrecomputeReplaceEnabled();
    } catch {
      return false;
    }
  }

  /**
   * PR-9: Get raw replacement flag value.
   */
  function getReplaceFlagRawValue() {
    try {
      return getReplaceFlagRaw();
    } catch {
      return "unavailable";
    }
  }

  /**
   * PR-9: Get a readiness summary from accumulated diff data.
   * Can be fed directly to evaluateReadiness().
   */
  function getReadinessSummary() {
    return _buildReadinessSummary();
  }

  /**
   * PR-10: Check if kill switch is active.
   */
  function isKillSwitchActiveValue() {
    try {
      return isKillSwitchActive();
    } catch {
      return false;
    }
  }

  /**
   * PR-10: Get raw kill switch flag value.
   */
  function getKillSwitchRawValue() {
    try {
      return getKillSwitchRaw();
    } catch {
      return "unavailable";
    }
  }

  /**
   * PR-10: Get combined replacement monitoring snapshot.
   * Includes readiness, circuit breaker state, and shadow metrics.
   */
  function getReplacementMonitoringSnapshot() {
    return {
      shadowMetrics: shadowMetrics.snapshot(),
      diffStats: shadowMetrics.getDiffStats(),
      readinessSummary: _buildReadinessSummary(),
      circuitBreaker: breaker.getSnapshot(),
      killSwitchActive: isKillSwitchActiveValue(),
      replaceActive: isReplaceActive(),
      timestamp: new Date().toISOString(),
    };
  }

  /**
   * PR-10: Reset all replacement monitoring state.
   * Clears shadow metrics, diff data, and circuit breaker.
   */
  function resetReplacementMonitoring() {
    shadowMetrics.reset();
    breaker.reset();
  }

  return {
    shadowBeforeAgentReply,
    controlledBeforeAgentReply,
    getShadowMetrics,
    resetShadowMetrics,
    getGatewayMetrics,
    isShadowActive,
    isReplaceActive,
    getFlagRaw,
    getReplaceFlagRaw: getReplaceFlagRawValue,
    getReadinessSummary,
    isKillSwitchActive: isKillSwitchActiveValue,
    getKillSwitchRaw: getKillSwitchRawValue,
    getReplacementMonitoringSnapshot,
    resetReplacementMonitoring,
    // PR-11: Rollout metrics accessors
    getRolloutMetrics: () => shadowMetrics.getRolloutMetrics(),
    getRolloutPercent: () => { try { return getRolloutPercent(); } catch { return 0; } },
    getRolloutPercentRaw: () => { try { return getRolloutPercentRaw(); } catch { return "unavailable"; } },
    /**
     * Await any pending shadow to complete.
     * Used in tests to verify metrics before checking.
     * In production this is optional — shadow runs async and doesn't block.
     */
    flushShadow: async () => {
      await _pendingShadow;
    },
    /**
     * PR-7: Get the last computed diff result.
     * Returns null if no diff has been computed yet.
     */
    getLastDiff: () => {
      return shadowMetrics.getLastDiff();
    },
    /**
     * PR-7: Get accumulated diff statistics.
     * Returns averages across all diff computations.
     */
    getDiffStats: () => {
      return shadowMetrics.getDiffStats();
    },
    // Exposed for testing
    _gateway: getGateway,
    _shadowMetrics: shadowMetrics,
  };
}

export default createShadowIntegration;
