#!/usr/bin/env node
/**
 * Precompute Rollout Controller — PR-11 Active Memory Controlled Rollout Validation
 *
 * Determines whether a request should be enrolled in the replacement rollout
 * based on a configured rollout percentage. Uses deterministic hashing for
 * stable bucket assignment.
 *
 * Design:
 *   - Pure function: no side effects, no randomness, no I/O
 *   - Deterministic: same requestId always maps to same bucket
 *   - Bucket range: 0-99 (100 buckets)
 *   - Enrollment: bucket < rolloutPercent → enrolled
 *
 * Supported rollout percentages:
 *   0%, 1%, 5%, 10%, 25%, 50%, 100%
 *
 * Safety:
 *   - < 0 → treated as 0 (no enrollment)
 *   - > 100 → treated as 100 (full enrollment)
 *   - NaN / non-numeric → treated as 0
 *   - Missing requestId → not enrolled
 *   - Never throws
 *
 * @module precompute-rollout-controller
 */

// ─── Hash Function ────────────────────────────────────

/**
 * Hash a string to a stable integer (0-99) using FNV-1a variant.
 *
 * Properties:
 *   - Deterministic: same input always produces same output
 *   - Uniform distribution: spreads input evenly across 100 buckets
 *   - No cryptographic properties needed (this is for traffic splitting)
 *
 * @param {string} input - The request identifier to hash
 * @returns {number} Integer in range [0, 99]
 */
function _hashToBucket(input) {
  // FNV-1a 32-bit variant: hash = (hash XOR byte) * FNV_prime
  // FNV_offset = 0x811c9dc5, FNV_prime = 0x01000193
  let hash = 0x811c9dc5;
  for (let i = 0; i < input.length; i++) {
    hash ^= input.charCodeAt(i);
    // Multiply by FNV prime, use only lower 32 bits
    hash = Math.imul(hash, 0x01000193);
  }
  // Convert to non-negative and modulo 100
  return ((hash >>> 0) % 100);
}

// ─── Normalization ────────────────────────────────────

/**
 * Normalize a rollout percent value to a safe integer in [0, 100].
 *
 * Rules:
 *   - NaN → 0
 *   - < 0 → 0
 *   - > 100 → 100
 *   - Non-integer → floor to integer
 *   - Never throws
 *
 * @param {*} raw - Raw rollout percent value (from env var or config)
 * @returns {number} Normalized integer in [0, 100]
 */
function normalizeRolloutPercent(raw) {
  try {
    const n = Number(raw);
    if (Number.isNaN(n)) return 0;
    const floored = Math.floor(n);
    if (floored < 0) return 0;
    if (floored > 100) return 100;
    return floored;
  } catch {
    return 0;
  }
}

// ─── Rollout Check ────────────────────────────────────

/**
 * Determine if a request is enrolled in the replacement rollout.
 *
 * This is a PURE FUNCTION:
 *   - No side effects
 *   - No randomness (Math.random never called)
 *   - Deterministic output for same inputs
 *   - Never throws
 *
 * @param {object} params
 * @param {number|string} params.rolloutPercent - Rollout percentage 0-100.
 *   Will be normalized to a safe integer.
 * @param {string} params.requestId - Stable request identifier.
 *   Typically `sessionId` or a composite like `${sessionId}-${messageIndex}`.
 *   Same ID always maps to same bucket.
 * @returns {{ enrolled: boolean, bucket: number, rolloutPercent: number }}
 *   - enrolled: whether this request is in the rollout
 *   - bucket: the hash bucket (0-99) for this request
 *   - rolloutPercent: the normalized rollout percentage used
 */
export function checkRolloutEnrollment({ rolloutPercent, requestId } = {}) {
  // Normalize rollout percent
  const normalized = normalizeRolloutPercent(rolloutPercent);

  // If rollout is 0%, no one is enrolled
  if (normalized === 0) {
    return { enrolled: false, bucket: -1, rolloutPercent: 0 };
  }

  // If rollout is 100%, everyone is enrolled
  if (normalized === 100) {
    return { enrolled: true, bucket: -1, rolloutPercent: 100 };
  }

  // Without a requestId, cannot determine bucket → not enrolled
  if (!requestId || typeof requestId !== "string" || requestId.length === 0) {
    return { enrolled: false, bucket: -1, rolloutPercent: normalized };
  }

  // Deterministic hash to bucket
  const bucket = _hashToBucket(requestId);
  const enrolled = bucket < normalized;

  return { enrolled, bucket, rolloutPercent: normalized };
}

/**
 * Get a human-readable status string for the rollout.
 *
 * @param {object} result - Result from checkRolloutEnrollment()
 * @returns {string} Human-readable description
 */
export function describeEnrollment(result) {
  if (!result) return "unknown";
  if (result.rolloutPercent === 0) return "rollout_disabled";
  if (result.rolloutPercent === 100) return "full_rollout";
  if (result.enrolled) return "enrolled";
  return "not_enrolled";
}

export { normalizeRolloutPercent, _hashToBucket };
