// src/recall-mmr.ts
var DEFAULT_LAMBDA = 0.7;
var DEFAULT_TOP_N = 40;
var DEFAULT_DIVERSITY_SAMPLE_SIZE = 10;
function applyMmrToCandidates(opts) {
  const candidates = Array.isArray(opts.candidates) ? opts.candidates : [];
  if (candidates.length === 0) return [];
  const lambda = clampLambda(opts.lambda);
  const topN = clampPositiveInt(opts.topN, DEFAULT_TOP_N);
  const budget = clampPositiveInt(opts.budget, candidates.length);
  if (budget <= 0) return [];
  if (candidates.length === 1) return [candidates[0]];
  const pool = candidates.slice(0, topN);
  const tail = candidates.slice(topN);
  const relevance = computeRelevanceScores(pool, opts.queryEmbedding);
  const pairSim = /* @__PURE__ */ new Map();
  const pairKey = (i, j) => i < j ? `${i}:${j}` : `${j}:${i}`;
  const sim = (i, j) => {
    if (i === j) return 1;
    const key = pairKey(i, j);
    const cached = pairSim.get(key);
    if (cached !== void 0) return cached;
    const a = pool[i];
    const b = pool[j];
    const s = similarity(a, b);
    pairSim.set(key, s);
    return s;
  };
  const selectedIdx = [];
  const remaining = /* @__PURE__ */ new Set();
  for (let i = 0; i < pool.length; i += 1) remaining.add(i);
  while (selectedIdx.length < budget && remaining.size > 0) {
    let bestIdx = -1;
    let bestMmr = Number.NEGATIVE_INFINITY;
    for (const idx of remaining) {
      const rel = relevance[idx] ?? 0;
      let maxSimToSelected = 0;
      if (selectedIdx.length > 0) {
        for (const s of selectedIdx) {
          const pairwise = sim(idx, s);
          if (pairwise > maxSimToSelected) maxSimToSelected = pairwise;
        }
      }
      const mmr = lambda * rel - (1 - lambda) * maxSimToSelected;
      if (mmr > bestMmr || // Stable tie-breaker: prefer the earlier original position.
      mmr === bestMmr && (bestIdx < 0 || idx < bestIdx)) {
        bestMmr = mmr;
        bestIdx = idx;
      }
    }
    if (bestIdx < 0) break;
    selectedIdx.push(bestIdx);
    remaining.delete(bestIdx);
  }
  const selected = selectedIdx.map((i) => pool[i]);
  if (selected.length < budget && tail.length > 0) {
    for (const c of tail) {
      if (selected.length >= budget) break;
      selected.push(c);
    }
  }
  return selected;
}
function summarizeMmrDiversity(before, after, sampleSize = DEFAULT_DIVERSITY_SAMPLE_SIZE) {
  const n = clampPositiveInt(sampleSize, DEFAULT_DIVERSITY_SAMPLE_SIZE);
  const beforeSlice = before.slice(0, n);
  const afterSlice = after.slice(0, n);
  let headReorderCount = 0;
  const compareLength = Math.min(beforeSlice.length, afterSlice.length);
  for (let i = 0; i < compareLength; i += 1) {
    if (beforeSlice[i].id !== afterSlice[i].id) headReorderCount += 1;
  }
  return {
    considered: before.length,
    kept: after.length,
    headReorderCount,
    avgPairwiseSimBefore: averagePairwiseSimilarity(beforeSlice),
    avgPairwiseSimAfter: averagePairwiseSimilarity(afterSlice)
  };
}
function clampLambda(value) {
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_LAMBDA;
  if (value < 0) return 0;
  if (value > 1) return 1;
  return value;
}
function clampPositiveInt(value, fallback) {
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
  if (value <= 0) return 0;
  return Math.floor(value);
}
function computeRelevanceScores(pool, queryEmbedding) {
  const canUseQueryEmbedding = Array.isArray(queryEmbedding) && queryEmbedding.length > 0;
  if (canUseQueryEmbedding) {
    const scores = [];
    for (const c of pool) {
      if (Array.isArray(c.embedding) && c.embedding.length > 0) {
        scores.push(cosineSimilarity(queryEmbedding, c.embedding));
      } else {
        scores.push(normalizeFinite(c.score));
      }
    }
    return normalizeVector(scores);
  }
  const raw = pool.map((c) => normalizeFinite(c.score));
  return normalizeVector(raw);
}
function similarity(a, b) {
  if (Array.isArray(a.embedding) && a.embedding.length > 0 && Array.isArray(b.embedding) && b.embedding.length > 0 && a.embedding.length === b.embedding.length) {
    return cosineSimilarity(a.embedding, b.embedding);
  }
  return jaccardSimilarity(a.content ?? "", b.content ?? "");
}
function cosineSimilarity(a, b) {
  if (a.length === 0 || b.length === 0) return 0;
  const len = Math.min(a.length, b.length);
  let dot = 0;
  let na = 0;
  let nb = 0;
  for (let i = 0; i < len; i += 1) {
    const av = a[i] ?? 0;
    const bv = b[i] ?? 0;
    dot += av * bv;
    na += av * av;
    nb += bv * bv;
  }
  if (na === 0 || nb === 0) return 0;
  const cos = dot / (Math.sqrt(na) * Math.sqrt(nb));
  if (cos < 0) return 0;
  if (cos > 1) return 1;
  return cos;
}
function normalizeTokens(text) {
  if (!text) return /* @__PURE__ */ new Set();
  const cleaned = text.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
  if (cleaned.length === 0) {
    const chars = /* @__PURE__ */ new Set();
    for (const ch of text.toLowerCase()) {
      if (/\s/.test(ch)) continue;
      chars.add(ch);
    }
    return chars;
  }
  const tokens = /* @__PURE__ */ new Set();
  for (const token of cleaned.split(/\s+/)) {
    if (!token) continue;
    if (token.length >= 2 && hasUnsegmentableScript(token)) {
      for (const ch of token) tokens.add(ch);
    } else {
      tokens.add(token);
    }
  }
  return tokens;
}
function hasUnsegmentableScript(token) {
  return /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(
    token
  );
}
function jaccardSimilarity(a, b) {
  const ta = normalizeTokens(a);
  const tb = normalizeTokens(b);
  if (ta.size === 0 && tb.size === 0) return 0;
  let intersection = 0;
  for (const t of ta) if (tb.has(t)) intersection += 1;
  const union = ta.size + tb.size - intersection;
  if (union === 0) return 0;
  return intersection / union;
}
function normalizeFinite(value) {
  if (typeof value !== "number" || !Number.isFinite(value)) return 0;
  return value;
}
function normalizeVector(values) {
  if (values.length === 0) return values;
  let max = 0;
  let min = Number.POSITIVE_INFINITY;
  for (const v of values) {
    if (!Number.isFinite(v)) continue;
    const a = Math.abs(v);
    if (a > max) max = a;
    if (v < min) min = v;
  }
  if (!Number.isFinite(min)) {
    return values.map(() => 0);
  }
  if (max === 0) {
    return values.map(() => 1);
  }
  if (min >= 0 && max <= 1) {
    return values.map(
      (v) => Number.isFinite(v) ? v < 0 ? 0 : v > 1 ? 1 : v : 0
    );
  }
  return values.map((v) => {
    if (!Number.isFinite(v)) return 0;
    const scaled = v / max;
    if (scaled < 0) return 0;
    if (scaled > 1) return 1;
    return scaled;
  });
}
function averagePairwiseSimilarity(candidates) {
  if (candidates.length < 2) return 0;
  let sum = 0;
  let count = 0;
  for (let i = 0; i < candidates.length; i += 1) {
    for (let j = i + 1; j < candidates.length; j += 1) {
      sum += similarity(candidates[i], candidates[j]);
      count += 1;
    }
  }
  if (count === 0) return 0;
  return sum / count;
}
function reorderRecallResultsWithMmr(results, options = {}) {
  const emptyReport = {
    considered: 0,
    kept: 0,
    headReorderCount: 0,
    avgPairwiseSimBefore: 0,
    avgPairwiseSimAfter: 0
  };
  const lambda = clampLambda(options.lambda);
  if (!Array.isArray(results) || results.length === 0) {
    return { reordered: [], diversity: emptyReport, lambda };
  }
  if (results.length < 2) {
    return {
      reordered: [results[0]],
      diversity: {
        considered: 1,
        kept: 1,
        headReorderCount: 0,
        avgPairwiseSimBefore: 0,
        avgPairwiseSimAfter: 0
      },
      lambda
    };
  }
  const topN = clampPositiveInt(options.topN, DEFAULT_TOP_N);
  const candidates = results.map((r, index) => ({
    id: makeRecallKey(r, index),
    content: r.snippet ?? "",
    score: typeof r.score === "number" ? r.score : 0,
    embedding: null
  }));
  const indexById = /* @__PURE__ */ new Map();
  for (let i = 0; i < candidates.length; i += 1) {
    indexById.set(candidates[i].id, i);
  }
  const selectedMmr = applyMmrToCandidates({
    candidates,
    lambda,
    topN,
    budget: results.length
  });
  const reordered = [];
  const reorderedCandidates = [];
  const seen = /* @__PURE__ */ new Set();
  for (const c of selectedMmr) {
    if (seen.has(c.id)) continue;
    const origIndex = indexById.get(c.id);
    if (origIndex === void 0) continue;
    seen.add(c.id);
    reordered.push(results[origIndex]);
    reorderedCandidates.push(candidates[origIndex]);
  }
  if (reordered.length < results.length) {
    for (let i = 0; i < results.length; i += 1) {
      const origCandidate = candidates[i];
      if (seen.has(origCandidate.id)) continue;
      seen.add(origCandidate.id);
      reordered.push(results[i]);
      reorderedCandidates.push(origCandidate);
    }
  }
  const diversity = summarizeMmrDiversity(
    candidates,
    reorderedCandidates,
    options.diversitySampleSize
  );
  return { reordered, diversity, lambda };
}
function makeRecallKey(r, index) {
  const baseKey = r.path || r.docid || "";
  return `${baseKey}::${index}`;
}

export {
  DEFAULT_LAMBDA,
  DEFAULT_TOP_N,
  applyMmrToCandidates,
  summarizeMmrDiversity,
  normalizeTokens,
  reorderRecallResultsWithMmr
};
//# sourceMappingURL=chunk-YDBIWGNI.js.map