#!/usr/bin/env node
/**
 * Precompute Replacement Policy — PR-9 Controlled Rollout Decision Layer
 *
 * Decides whether to use the precompute result or the primary recall result.
 * Only selects precompute when ALL safety gates pass AND replacement is
 * explicitly enabled.
 *
 * Decision priorities (PR-9 + PR-10):
 *   1. Replacement flag OFF                         → primary (disabled)
 *   2. Kill switch active                           → primary (kill_switch_active)
 *   3. Circuit breaker open / blocked               → primary (circuit_open/blocked)
 *   4. Readiness not pass / safeToReplace not true  → primary (readiness_not_pass)
 *   5. Precompute result null/malformed/empty       → primary (precompute_empty/error)
 *   6. Dry-run mode                                 → primary (dryrun_fallback)
 *   7. All gates pass + precompute valid            → precompute (selected_precompute)
 *
 * This is a PURE FUNCTION module. It does NOT:
 *   - Read or write any files
 *   - Read or write environment variables
 *   - Read or write SQLite / FTS5 databases
 *   - Modify MEMORY.md or any memory files
 *   - Touch the precompute cache
 *   - Have any side effects whatsoever
 *   - Modify any production recall path
 *   - Let precomputeResult participate in user replies (unless policy decides)
 *
 * @module precompute-replacement-policy
 */

// ─── Decision Constants ───────────────────────────────

/** Replacement is disabled (flag off) */
export const DECISION_DISABLED = "disabled";
/** Replacement enabled but readiness gate did not pass */
export const DECISION_READINESS_NOT_PASS = "readiness_not_pass";
/** Readiness passed but precompute returned nothing usable */
export const DECISION_PRECOMPUTE_EMPTY = "precompute_empty";
/** Precompute returned a malformed or error result */
export const DECISION_PRECOMPUTE_ERROR = "precompute_error";
/** All gates passed — selected precompute result */
export const DECISION_SELECTED_PRECOMPUTE = "selected_precompute";
/** All gates passed but dry-run — fallback to primary */
export const DECISION_DRYRUN_FALLBACK = "dryrun_fallback";
/** Kill switch is active — forced primary */
export const DECISION_KILL_SWITCH_ACTIVE = "kill_switch_active";
/** Circuit breaker is open — forced primary */
export const DECISION_CIRCUIT_OPEN = "circuit_open";
/** Circuit breaker is half-open but probe quota exceeded */
export const DECISION_CIRCUIT_HALF_OPEN_BLOCKED = "circuit_half_open_blocked";
/**
 * Precompute was valid and gates passed, but an unexpected condition
 * caused fallback to primary.
 */
export const DECISION_FALLBACK_PRIMARY = "fallback_primary";

/** Replacement source labels */
export const SOURCE_PRIMARY = "primary";
export const SOURCE_PRECOMPUTE = "precompute";

// ─── Default Replacement Options ──────────────────────

const DEFAULT_OPTIONS = {
  /** Whether replacement is explicitly enabled */
  replaceEnabled: false,
  /** Dry-run mode: evaluate but always return primary */
  dryRun: false,
};

// ─── Policy Evaluation ────────────────────────────────

/**
 * Evaluate replacement policy and decide which source to use.
 *
 * @param {object} params
 * @param {*} params.primaryResult - The result from primary (blocking) recall.
 *   Must be truthy or an empty result. If null/undefined, policy assumes
 *   primary recall failed and will still attempt fallback to primary
 *   (policy never invents a precompute-only path).
 * @param {*} params.precomputeResult - The result from precompute gateway.
 *   Can be null (gateway skipped), a valid result object, or a malformed value.
 * @param {object|null|undefined} params.readiness - Readiness evaluation result
 *   from evaluateReadiness(). Expected shape: { status, safeToReplace, ... }.
 *   Null/undefined/malformed → treat as not pass.
 * @param {object} [params.hookContext] - Context from hook (sessionId, etc.).
 *   Used for logging/reasoning only, not for decision logic.
 * @param {boolean} [params.killSwitch] - If true, kill switch is active → forced primary
 * @param {string} [params.circuitState] - Circuit breaker state: "closed"|"open"|"half_open"
 * @param {boolean} [params.circuitAllowed] - Whether circuit breaker allows replacement
 * @param {object} [params.options] - Policy options.
 *   Supports replaceEnabled, dryRun, and any future overrides.
 * @returns {object} Policy decision:
 *   - selectedResult: * — the result to use (primary or precompute)
 *   - source: "primary" | "precompute"
 *   - decision: string — one of DECISION_* constants
 *   - safeToReplace: boolean — from readiness (or false if readiness unavailable)
 *   - reasons: string[] — explanation of the decision
 *   - metrics: object — decision counts and flags for observability
 */
function selectReplacementResult(params) {
  // Guard against null/undefined params
  if (!params || typeof params !== "object") {
    const reasons = ["No valid parameters provided"];
    return {
      selectedResult: undefined,
      source: SOURCE_PRIMARY,
      decision: DECISION_FALLBACK_PRIMARY,
      safeToReplace: false,
      reasons,
      metrics: { replaceEnabled: false, dryRun: false, killSwitch: false, circuitState: null, circuitAllowed: false },
    };
  }

  const {
    primaryResult,
    precomputeResult,
    readiness,
    hookContext,
    options,
  } = params;

  const killSwitch = params.killSwitch === true;
  const circuitState = params.circuitState || null;
  const circuitAllowed = params.circuitAllowed !== false; // default true if not provided

  const opts = { ...DEFAULT_OPTIONS, ...(options || {}) };
  const reasons = [];
  const metrics = {
    replaceEnabled: opts.replaceEnabled,
    dryRun: opts.dryRun,
    killSwitch,
    circuitState,
    circuitAllowed,
    readinessStatus: null,
    safeToReplace: false,
    precomputeAvailable: false,
    precomputeMemoriesCount: 0,
    decision: null,
    source: null,
  };

  // ── Policy Step 1: Check replacement flag ──────────
  if (!opts.replaceEnabled) {
    metrics.decision = DECISION_DISABLED;
    metrics.source = SOURCE_PRIMARY;
    reasons.push("Replacement flag is OFF — using primary recall");
    return {
      selectedResult: primaryResult,
      source: SOURCE_PRIMARY,
      decision: DECISION_DISABLED,
      safeToReplace: false,
      reasons,
      metrics,
    };
  }

  // ── Policy Step 2: Check kill switch ──────────────
  if (killSwitch) {
    metrics.decision = DECISION_KILL_SWITCH_ACTIVE;
    metrics.source = SOURCE_PRIMARY;
    reasons.push("Kill switch is active — replacement forcibly disabled");
    return {
      selectedResult: primaryResult,
      source: SOURCE_PRIMARY,
      decision: DECISION_KILL_SWITCH_ACTIVE,
      safeToReplace: false,
      reasons,
      metrics,
    };
  }

  // ── Policy Step 3: Check circuit breaker ──────────
  if (!circuitAllowed) {
    const decision = circuitState === "open"
      ? DECISION_CIRCUIT_OPEN
      : DECISION_CIRCUIT_HALF_OPEN_BLOCKED;
    metrics.decision = decision;
    metrics.source = SOURCE_PRIMARY;
    const reason = circuitState === "open"
      ? "Circuit breaker is OPEN — replacement blocked"
      : "Circuit breaker is HALF_OPEN but probe quota exceeded — replacement blocked";
    reasons.push(reason);
    return {
      selectedResult: primaryResult,
      source: SOURCE_PRIMARY,
      decision,
      safeToReplace: false,
      reasons,
      metrics,
    };
  }

  // ── Policy Step 4: Validate readiness ─────────────
  const readinessPass = _isReadinessPass(readiness);
  const safeToReplace = readinessPass;

  metrics.readinessStatus = readiness ? readiness.status : null;
  metrics.safeToReplace = safeToReplace;

  if (!safeToReplace) {
    const reason = readiness
      ? `Readiness status is "${readiness.status}" — gates not fully satisfied`
      : "Readiness evaluation unavailable or invalid";
    metrics.decision = DECISION_READINESS_NOT_PASS;
    metrics.source = SOURCE_PRIMARY;
    reasons.push(reason);
    return {
      selectedResult: primaryResult,
      source: SOURCE_PRIMARY,
      decision: DECISION_READINESS_NOT_PASS,
      safeToReplace: false,
      reasons,
      metrics,
    };
  }

  // ── Policy Step 4: Validate precompute result ─────
  const precomputeCheck = _checkPrecomputeValid(precomputeResult);
  metrics.precomputeAvailable = precomputeCheck.valid;
  metrics.precomputeMemoriesCount = precomputeCheck.memoriesCount;

  if (!precomputeCheck.valid) {
    const reason = precomputeCheck.reason || "Precompute result is not usable";
    const decision = precomputeCheck.isError
      ? DECISION_PRECOMPUTE_ERROR
      : DECISION_PRECOMPUTE_EMPTY;
    metrics.decision = decision;
    metrics.source = SOURCE_PRIMARY;
    reasons.push(reason);
    return {
      selectedResult: primaryResult,
      source: SOURCE_PRIMARY,
      decision,
      safeToReplace,
      reasons,
      metrics,
    };
  }

  // ── Policy Step 5: Check dry-run mode ─────────────
  if (opts.dryRun) {
    metrics.decision = DECISION_DRYRUN_FALLBACK;
    metrics.source = SOURCE_PRIMARY;
    reasons.push("Dry-run mode: would select precompute but returning primary");
    return {
      selectedResult: primaryResult,
      source: SOURCE_PRIMARY,
      decision: DECISION_DRYRUN_FALLBACK,
      safeToReplace,
      reasons,
      metrics,
    };
  }

  // ── Policy Step 6: All gates pass → select precompute
  metrics.decision = DECISION_SELECTED_PRECOMPUTE;
  metrics.source = SOURCE_PRECOMPUTE;
  reasons.push("All gates passed: replacement flag ON, readiness pass, precompute valid — selecting precompute result");

  return {
    selectedResult: precomputeResult,
    source: SOURCE_PRECOMPUTE,
    decision: DECISION_SELECTED_PRECOMPUTE,
    safeToReplace,
    reasons,
    metrics,
  };
}

// ─── Internal Helpers ─────────────────────────────────

/**
 * Check if readiness indicates safe-to-replace.
 *
 * @param {*} readiness - Readiness result object or null/undefined
 * @returns {boolean} true if readiness.status === "pass" AND readiness.safeToReplace === true
 */
function _isReadinessPass(readiness) {
  if (!readiness || typeof readiness !== "object") {
    return false;
  }
  try {
    return readiness.status === "pass" && readiness.safeToReplace === true;
  } catch {
    return false;
  }
}

/**
 * Check if a precompute result is valid and usable.
 *
 * Valid:
 *   - Is an object (not null, not array, not primitive)
 *   - Has a truthy `memories` array with at least one item
 *   - OR has some other positive indicator of usability
 *
 * Malformed / error:
 *   - null / undefined
 *   - String, number, or other primitive
 *   - Array (not expected shape)
 *   - Object with `error` or `status: "error"`
 *   - Object without `memories` or with empty `memories`
 *
 * @param {*} result - Precompute result to validate
 * @returns {{ valid: boolean, memoriesCount: number, reason: string|null, isError: boolean }}
 */
function _checkPrecomputeValid(result) {
  // null / undefined → empty
  if (result === null || result === undefined) {
    return { valid: false, memoriesCount: 0, reason: "Precompute result is null/undefined", isError: false };
  }

  // Non-object → malformed
  if (typeof result !== "object") {
    return { valid: false, memoriesCount: 0, reason: "Precompute result is not an object", isError: true };
  }

  // Array → unexpected shape
  if (Array.isArray(result)) {
    return { valid: false, memoriesCount: 0, reason: "Precompute result is an array (unexpected shape)", isError: true };
  }

  // Object with explicit error
  if (result.status === "error" || result.error) {
    return { valid: false, memoriesCount: 0, reason: result.error || "Precompute result has error status", isError: true };
  }

  // Check memories array
  let memories = null;
  if (Array.isArray(result.memories)) {
    memories = result.memories;
  } else if (Array.isArray(result.results)) {
    memories = result.results;
  } else if (Array.isArray(result.items)) {
    memories = result.items;
  }

  const memoriesCount = memories ? memories.length : 0;

  if (memoriesCount === 0) {
    return { valid: false, memoriesCount: 0, reason: "Precompute result has no memories", isError: false };
  }

  return { valid: true, memoriesCount, reason: null, isError: false };
}

// ─── Utility ──────────────────────────────────────────

/**
 * Get a human-readable label for a decision code.
 * @param {string} decision - One of DECISION_* constants
 * @returns {string} Human-readable label
 */
function getDecisionLabel(decision) {
  const labels = {
    [DECISION_DISABLED]: "Replacement disabled (flag off)",
    [DECISION_READINESS_NOT_PASS]: "Replacement not ready (readiness gates not satisfied)",
    [DECISION_PRECOMPUTE_EMPTY]: "Precompute returned no memories",
    [DECISION_PRECOMPUTE_ERROR]: "Precompute returned error/malformed result",
    [DECISION_SELECTED_PRECOMPUTE]: "Selected precompute result",
    [DECISION_DRYRUN_FALLBACK]: "Dry-run mode — would have selected precompute",
    [DECISION_KILL_SWITCH_ACTIVE]: "Kill switch active — replacement forcibly disabled",
    [DECISION_CIRCUIT_OPEN]: "Circuit breaker open — replacement blocked",
    [DECISION_CIRCUIT_HALF_OPEN_BLOCKED]: "Circuit breaker half-open blocked — probe quota exceeded",
    [DECISION_FALLBACK_PRIMARY]: "Fallback to primary (unexpected condition)",
  };
  return labels[decision] || `Unknown decision: ${decision}`;
}

export {
  selectReplacementResult,
  DEFAULT_OPTIONS,
  _isReadinessPass,
  _checkPrecomputeValid,
  getDecisionLabel,
};
