#!/usr/bin/env node
/**
 * Precompute Readiness Gate — PR-8 Active Memory Replacement Readiness Evaluation
 *
 * Evaluates whether the precompute gateway is ready to replace the blocking
 * (primary) recall path, based on shadow diff metrics from PR-7.
 *
 * 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 shadowResult participate in user replies
 *
 * The gate evaluates readiness using these default thresholds:
 *   - avgOverlapRatio    >= 0.85
 *   - avgTopKOverlap     >= 0.85  (normalized by topK)
 *   - sameTopResultRate  >= 0.95
 *   - shadowErrorRate    <= 0.01
 *   - shadowTimeoutRate  <= 0.01
 *   - totalSamples       >= 100
 *
 * Status determination:
 *   - All thresholds met                         → pass
 *   - Sample count insufficient, or minor misses → warn
 *   - High error/timeout rate, or severe shortfall → fail
 *
 * @module precompute-readiness-gate
 */

// ─── Default Thresholds ────────────────────────────────

const DEFAULT_THRESHOLDS = {
  /** Minimum average Jaccard overlap between primary and shadow result sets */
  avgOverlapRatio: 0.85,
  /** Minimum normalized top-K overlap (0-1, compared as metric/topK) */
  avgTopKOverlap: 0.85,
  /** Minimum rate at which the top result is identical */
  sameTopResultRate: 0.95,
  /** Maximum acceptable shadow execution error rate */
  shadowErrorRate: 0.01,
  /** Maximum acceptable shadow timeout rate */
  shadowTimeoutRate: 0.01,
  /** Minimum sample count for statistical relevance */
  totalSamples: 100,
};

// ─── Metric Score Weights (sum = 100) ─────────────────
const METRIC_WEIGHTS = {
  avgOverlapRatio: 25,
  avgTopKOverlap: 20,
  sameTopResultRate: 20,
  shadowErrorRate: 15,
  shadowTimeoutRate: 10,
  totalSamples: 10,
};

// ─── Metric Direction ─────────────────────────────────
const HIGHER_IS_BETTER = new Set([
  "avgOverlapRatio",
  "avgTopKOverlap",
  "sameTopResultRate",
  "totalSamples",
]);

const LOWER_IS_BETTER = new Set([
  "shadowErrorRate",
  "shadowTimeoutRate",
]);

// ─── Readiness Evaluation ──────────────────────────────

/**
 * Evaluate whether the precompute gateway is ready to replace blocking recall.
 *
 * @param {object|null|undefined} summary - Shadow diff summary object from PR-7 report.
 *   Expected fields: avgOverlapRatio, avgTopKOverlap, sameTopResultRate,
 *   shadowErrorRate, shadowTimeoutRate, totalSamples.
 *
 * @param {object} [options]
 * @param {object} [options.thresholds] - Override individual thresholds.
 *   Supports all DEFAULT_THRESHOLDS keys. Partial overrides merge with defaults.
 * @param {number} [options.topK] - The K value used in shadow diffs (default: 5).
 *   Used to normalize avgTopKOverlap from raw count to 0-1 ratio.
 * @returns {object} Readiness evaluation result:
 *   - status: "pass" | "warn" | "fail"
 *   - score: number 0-100
 *   - reasons: string[] — explanation for status
 *   - metrics: object — actual measured values
 *   - thresholds: object — thresholds used (merged)
 *   - recommendation: string — human-readable guidance
 *   - safeToReplace: boolean — only true when status === "pass"
 */
function evaluateReadiness(summary, options = {}) {
  const thresholds = { ...DEFAULT_THRESHOLDS, ...(options.thresholds || {}) };
  const topK = typeof options.topK === "number" && options.topK > 0 ? options.topK : 5;

  // ── Handle invalid / empty summaries ────────────
  if (!summary || typeof summary !== "object") {
    return {
      status: "warn",
      score: 0,
      reasons: ["No valid summary provided: summary is null, undefined, or non-object"],
      metrics: {},
      thresholds,
      recommendation:
        "Cannot evaluate readiness without a shadow diff summary. " +
        "Run PR-7 shadow diff evaluation first to generate a summary.",
      safeToReplace: false,
    };
  }

  // ── Extract metrics with defaults ───────────────
  const rawTopKOverlap = typeof summary.avgTopKOverlap === "number" ? summary.avgTopKOverlap : 0;
  const normalizedTopKOverlap = topK > 0 ? rawTopKOverlap / topK : 0;

  const metrics = {
    avgOverlapRatio: typeof summary.avgOverlapRatio === "number" ? summary.avgOverlapRatio : 0,
    avgTopKOverlap: rawTopKOverlap,          // raw value (0 to topK)
    avgTopKOverlapNormalized: normalizedTopKOverlap, // normalized to 0-1
    sameTopResultRate: typeof summary.sameTopResultRate === "number" ? summary.sameTopResultRate : 0,
    shadowErrorRate: typeof summary.shadowErrorRate === "number" ? summary.shadowErrorRate : 0,
    shadowTimeoutRate: typeof summary.shadowTimeoutRate === "number" ? summary.shadowTimeoutRate : 0,
    totalSamples: typeof summary.totalSamples === "number" ? summary.totalSamples : 0,
  };

  // ── Run each threshold check ────────────────────
  const checks = {};

  // Higher-is-better metrics
  _checkMetric(checks, "avgOverlapRatio", metrics.avgOverlapRatio, thresholds.avgOverlapRatio, true);
  _checkMetric(checks, "avgTopKOverlap", normalizedTopKOverlap, thresholds.avgTopKOverlap, true);
  _checkMetric(checks, "sameTopResultRate", metrics.sameTopResultRate, thresholds.sameTopResultRate, true);
  _checkMetric(checks, "totalSamples", metrics.totalSamples, thresholds.totalSamples, true);

  // Lower-is-better metrics
  _checkMetric(checks, "shadowErrorRate", metrics.shadowErrorRate, thresholds.shadowErrorRate, false);
  _checkMetric(checks, "shadowTimeoutRate", metrics.shadowTimeoutRate, thresholds.shadowTimeoutRate, false);

  // ── Build reasons list ──────────────────────────
  const reasons = [];
  for (const [name, check] of Object.entries(checks)) {
    if (!check.pass) {
      reasons.push(check.reason);
    }
  }

  // ── Compute score (0-100) ───────────────────────
  const score = _computeScore(checks, normalizedTopKOverlap, metrics, thresholds, topK);

  // ── Determine status ────────────────────────────
  const status = _determineStatus(checks, reasons, metrics, thresholds);

  // ── Build recommendation ────────────────────────
  const recommendation = _buildRecommendation(status, checks, metrics, thresholds);

  return {
    status,
    score: Math.round(score * 100) / 100,
    reasons,
    metrics,
    thresholds,
    recommendation,
    safeToReplace: status === "pass",
  };
}

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

/**
 * Check a single metric against its threshold.
 *
 * @param {object} checks - Accumulator object (mutated in place)
 * @param {string} name - Metric name
 * @param {number} actual - Actual value
 * @param {number} threshold - Threshold to meet
 * @param {boolean} higherIsBetter - True if higher values are better
 */
function _checkMetric(checks, name, actual, threshold, higherIsBetter) {
  const pass = higherIsBetter
    ? actual >= threshold
    : actual <= threshold;

  checks[name] = {
    actual,
    threshold,
    pass,
    higherIsBetter,
    reason: pass
      ? null
      : _formatReason(name, actual, threshold, higherIsBetter),
  };
}

/**
 * Format a human-readable failure reason.
 */
function _formatReason(name, actual, threshold, higherIsBetter) {
  const dir = higherIsBetter ? "≥" : "≤";
  const actualStr = name === "totalSamples"
    ? String(actual)
    : (actual * 100).toFixed(2) + "%";
  const thresholdStr = name === "totalSamples"
    ? String(threshold)
    : (threshold * 100).toFixed(2) + "%";

  const labels = {
    avgOverlapRatio: "Average overlap ratio",
    avgTopKOverlap: "Normalized top-K overlap",
    sameTopResultRate: "Same top result rate",
    shadowErrorRate: "Shadow error rate",
    shadowTimeoutRate: "Shadow timeout rate",
    totalSamples: "Total samples",
  };

  const label = labels[name] || name;
  return `${label} ${actualStr} < ${dir} ${thresholdStr}`;
}

/**
 * Compute overall readiness score (0-100).
 */
function _computeScore(checks, normalizedTopKOverlap, metrics, thresholds, topK) {
  let totalScore = 0;

  for (const [name, check] of Object.entries(checks)) {
    const weight = METRIC_WEIGHTS[name] || 10;
    let contribution;

    if (check.pass) {
      contribution = weight;
    } else if (check.higherIsBetter && check.threshold > 0) {
      // Partial credit for higher-is-better metrics
      contribution = Math.min(weight, (check.actual / check.threshold) * weight);
    } else if (!check.higherIsBetter && check.threshold > 0) {
      // Partial credit for lower-is-better metrics
      const ratio = check.actual / check.threshold;
      // At 2x threshold or more → 0; below threshold → linear penalty
      contribution = ratio <= 1 ? weight : Math.max(0, weight * (2 - ratio));
    } else {
      contribution = 0;
    }

    totalScore += contribution;
  }

  return Math.max(0, Math.min(100, totalScore));
}

/**
 * Determine overall readiness status.
 *
 * Rules:
 *   - All checks pass → pass
 *   - totalSamples < threshold → warn (insufficient data)
 *   - Minor metrics miss → warn
 *   - errorRate > threshold AND exceeds warn margin → fail
 *   - timeoutRate > threshold AND exceeds warn margin → fail
 *   - overlapRatio too low (< threshold * 0.5) → fail
 *   - normalizedTopK too low (< threshold * 0.5) → fail
 *
 * Warn margin: if the failing metric is within 20% of threshold → warn (not fail)
 *              if beyond 20% → fail
 */
function _determineStatus(checks, reasons, metrics, thresholds) {
  const passCount = Object.values(checks).filter(c => c.pass).length;
  const totalChecks = Object.keys(checks).length;

  if (passCount === totalChecks) {
    return "pass";
  }

  // Check totalSamples separately — insufficient data is a warn, not fail
  if (checks.totalSamples && !checks.totalSamples.pass) {
    return "warn";
  }

  // Assess severity of each failing metric
  let hasFail = false;
  let hasWarn = false;

  for (const [name, check] of Object.entries(checks)) {
    if (check.pass) continue;
    if (name === "totalSamples") continue; // Already handled above

    const severity = _assessFailSeverity(name, check, thresholds);

    if (severity === "fail") {
      hasFail = true;
    } else {
      hasWarn = true;
    }
  }

  if (hasFail) return "fail";
  if (hasWarn) return "warn";
  return "warn"; // Fallback (shouldn't reach here)
}

/**
 * Assess whether a failing metric is severe enough to fail, or just warn.
 *
 * Warn margin: actual within 20% of the threshold boundary → warn
 * Beyond 20% → fail
 *
 * For higher-is-better: actual >= threshold * 0.8 → warn, else fail
 * For lower-is-better:  actual <= threshold * 1.2 → warn, else fail
 */
function _assessFailSeverity(name, check, thresholds) {
  if (check.higherIsBetter) {
    // actual < threshold
    if (check.actual >= check.threshold * 0.8) {
      return "warn";
    }
    return "fail";
  } else {
    // actual > threshold (lower is better)
    if (check.actual <= check.threshold * 1.2) {
      return "warn";
    }
    return "fail";
  }
}

/**
 * Build a human-readable recommendation string.
 */
function _buildRecommendation(status, checks, metrics, thresholds) {
  const lines = [];

  if (status === "pass") {
    lines.push(
      "The precompute gateway meets all readiness thresholds and is safe to " +
      "replace the blocking recall path. All metrics pass the minimum quality bar. " +
      "Proceed to PR-9 for controlled rollout."
    );
  } else if (status === "warn") {
    const failing = Object.entries(checks)
      .filter(([_, c]) => !c.pass)
      .map(([name, c]) => _metricLabel(name, c));
    lines.push(
      "The precompute gateway is close to but not yet meeting all readiness thresholds. " +
      `The following metrics need attention: ${failing.join(", ")}. ` +
      "Improve shadow quality before considering replacement. " +
      "Sample count may be insufficient — collect more data and re-evaluate."
    );
  } else if (status === "fail") {
    const failing = Object.entries(checks)
      .filter(([_, c]) => !c.pass)
      .map(([name, c]) => _metricLabel(name, c));
    lines.push(
      "The precompute gateway does NOT meet readiness thresholds for replacing " +
      "the blocking recall path. " +
      `Critical metrics failing: ${failing.join(", ")}. ` +
      "Investigate and resolve shadow quality issues before attempting replacement."
    );
  }

  lines.push(
    "Note: PR-8 does NOT modify production behavior. This evaluation is read-only."
  );

  return lines.join("\n");
}

function _metricLabel(name, check) {
  const labels = {
    avgOverlapRatio: `overlap=${(check.actual * 100).toFixed(1)}% (need ≥${(check.threshold * 100).toFixed(0)}%)`,
    avgTopKOverlap: `topK=${(check.actual * 100).toFixed(1)}% (need ≥${(check.threshold * 100).toFixed(0)}%)`,
    sameTopResultRate: `sameTop=${(check.actual * 100).toFixed(1)}% (need ≥${(check.threshold * 100).toFixed(0)}%)`,
    shadowErrorRate: `errors=${(check.actual * 100).toFixed(2)}% (need ≤${(check.threshold * 100).toFixed(0)}%)`,
    shadowTimeoutRate: `timeouts=${(check.actual * 100).toFixed(2)}% (need ≤${(check.threshold * 100).toFixed(0)}%)`,
    totalSamples: `samples=${check.actual} (need ≥${check.threshold})`,
  };
  return labels[name] || `${name}: ${check.actual}`;
}

export { evaluateReadiness, DEFAULT_THRESHOLDS, METRIC_WEIGHTS };
