import { createHash } from "node:crypto";
import { extractHighlights } from "./highlights.js";
import { countTokens, truncateByTokens } from "./tokens.js";
import { applyOutputBudget } from "./truncate.js";
import { createLogger } from "../logger.js";
const log = createLogger("search");
const DEFAULT_MAX_TOKENS_OUT = 4e3;
const MAX_EVIDENCE_PASSAGES = 20;
const TRUNCATION_MARKER = "[... content truncated]";
const MIN_EVIDENCE_EXCERPT_CHARS = 40;
const MAX_LINK_MARKUP_RATIO = 0.5;
function isUsefulEvidenceExcerpt(excerpt) {
  const trimmed = excerpt.trim();
  if (trimmed.length < MIN_EVIDENCE_EXCERPT_CHARS) return false;
  const linkChars = (trimmed.match(/\[[^\]]*\]\([^)]*\)|https?:\/\/\S+/g) ?? []).reduce((acc, match) => acc + match.length, 0);
  if (linkChars / trimmed.length > MAX_LINK_MARKUP_RATIO) return false;
  return true;
}
async function buildEvidenceFromMarkdown(query, title, url, markdown, opts = {}) {
  if (!markdown) return [];
  const maxItems = opts.maxItems ?? 1;
  const synthetic = [{
    title,
    url,
    snippet: "",
    markdown_content: markdown,
    relevance_score: 1
  }];
  let result;
  try {
    result = await extractHighlights(query, synthetic, Math.max(maxItems, 1));
  } catch (err) {
    log.debug("buildEvidenceFromMarkdown: extractHighlights failed", { error: String(err) });
    return [];
  }
  const ranked = result.highlights.slice().sort((a, b) => b.relevance_score - a.relevance_score).slice(0, maxItems);
  const out = [];
  let used = 0;
  const budget = opts.maxTokensOut;
  for (const h of ranked) {
    let excerpt = h.text;
    if (budget !== void 0) {
      const remaining = budget - used;
      if (remaining <= 0) break;
      excerpt = truncateByTokens(h.text, remaining);
      if (!excerpt || excerpt.trim() === TRUNCATION_MARKER) break;
    }
    const span = h.source_span ?? { start: 0, end: excerpt.length };
    out.push(buildEvidenceItem({
      title: h.source_title || title,
      url: h.source_url || url,
      sectionHeading: h.section_heading ?? null,
      excerpt,
      score: h.relevance_score,
      sourceSpan: span
    }));
    if (budget !== void 0) used += countTokens(excerpt);
  }
  return out;
}
function applyAggregateMarkdownBudget(items, getBody, setBody, opts) {
  const budget = opts.maxTokensOut;
  let used = 0;
  for (const item of items) {
    const body = getBody(item);
    if (!body) continue;
    if (budget !== void 0) {
      const remaining = budget - used;
      if (remaining <= 0) {
        setBody(item, "");
        continue;
      }
      const trimmed = applyOutputBudget(body, { maxTokensOut: remaining, maxChars: opts.maxChars });
      setBody(item, trimmed);
      used += countTokens(trimmed);
    } else {
      const trimmed = applyOutputBudget(body, { maxChars: opts.maxChars });
      setBody(item, trimmed);
    }
  }
}
function applyTokenBudget(items, maxTokensOut) {
  if (maxTokensOut <= 0) return [];
  const out = [];
  let used = 0;
  for (const item of items) {
    const remaining = maxTokensOut - used;
    if (remaining <= 0) break;
    const excerpt = truncateByTokens(item.excerpt, remaining);
    if (!excerpt) break;
    out.push({ ...item, excerpt });
    used += countTokens(excerpt);
  }
  return out;
}
function stableCitationId(url, start) {
  return createHash("sha1").update(`${url}#${start}`).digest("hex").slice(0, 12);
}
function buildEvidenceItem(input) {
  return {
    title: input.title,
    url: input.url,
    section_heading: input.sectionHeading,
    excerpt: input.excerpt,
    score: input.score,
    citation_id: stableCitationId(input.url, input.sourceSpan.start),
    source_span: input.sourceSpan
  };
}
async function applyEvidenceDefault(input, output, results, query) {
  if (results.length === 0) return;
  const includeFullMarkdown = input.include_full_markdown ?? false;
  const citationFormat = input.citation_format ?? "numbered";
  const maxTokensOut = input.max_tokens_out ?? DEFAULT_MAX_TOKENS_OUT;
  let highlightsResult;
  try {
    highlightsResult = await extractHighlights(query, results, MAX_EVIDENCE_PASSAGES);
  } catch (err) {
    log.debug("evidence extraction failed", { error: String(err) });
    const msg = "evidence extraction failed; results returned without highlights";
    output.warning = output.warning ? `${output.warning}; ${msg}` : msg;
    highlightsResult = { highlights: [], citations: [], reranker_used: false };
  }
  const ranked = highlightsResult.highlights.slice().sort((a, b) => b.relevance_score - a.relevance_score);
  let evidenceBudget = maxTokensOut;
  if (input.max_tokens_out !== void 0) {
    const skeleton = { ...output, citations: void 0, evidence: void 0, citations_xml: void 0 };
    const skeletonTokens = countTokens(JSON.stringify(skeleton));
    const resultsTokens = countTokens(JSON.stringify(results));
    const overhead = skeletonTokens + resultsTokens;
    evidenceBudget = Math.max(0, maxTokensOut - overhead);
  }
  const maxEvidence = input.max_results !== void 0 ? Math.max(0, Math.floor(input.max_results)) : Infinity;
  const evidence = [];
  let usedTokens = 0;
  for (const h of ranked) {
    if (evidence.length >= maxEvidence) break;
    if (usedTokens >= evidenceBudget) break;
    const remaining = evidenceBudget - usedTokens;
    const excerpt = truncateByTokens(h.text, remaining);
    if (!excerpt) continue;
    if (!isUsefulEvidenceExcerpt(excerpt)) continue;
    const span = h.source_span ?? { start: 0, end: excerpt.length };
    const item = buildEvidenceItem({
      title: h.source_title,
      url: h.source_url,
      sectionHeading: h.section_heading ?? null,
      excerpt,
      score: h.relevance_score,
      sourceSpan: span
    });
    evidence.push(item);
    usedTokens += countTokens(excerpt);
  }
  if (evidence.length > 0) {
    output.evidence = evidence;
  }
  const citations = buildCitationsFromEvidence(results, evidence, highlightsResult.citations);
  if (citationFormat === "numbered" || citationFormat === "json") {
    if (citations.length > 0) output.citations = citations;
  } else if (citationFormat === "anthropic_tags") {
    if (citations.length > 0) {
      output.citations = citations;
      output.citations_xml = renderCitationsXml(citations);
    }
  }
  if (!includeFullMarkdown) {
    for (const r of results) {
      if (r.markdown_content !== void 0) r.markdown_content = void 0;
    }
  } else if (input.max_tokens_out !== void 0) {
    applyAggregateMarkdownBudget(
      results,
      (r) => typeof r.markdown_content === "string" ? r.markdown_content : "",
      (r, body) => {
        r.markdown_content = body;
      },
      { maxTokensOut: input.max_tokens_out }
    );
  }
}
function buildCitationsFromEvidence(results, evidence, baseCitations) {
  const primaryByUrl = /* @__PURE__ */ new Map();
  for (const ev of evidence) {
    if (!primaryByUrl.has(ev.url)) primaryByUrl.set(ev.url, ev.citation_id);
  }
  const baseByUrl = /* @__PURE__ */ new Map();
  for (const c of baseCitations) baseByUrl.set(c.url, c);
  const out = [];
  for (let i = 0; i < results.length; i++) {
    const r = results[i];
    const base = baseByUrl.get(r.url);
    const citation = base ? { ...base } : {
      index: i + 1,
      url: r.url,
      title: r.title,
      snippet: r.snippet ?? ""
    };
    const primary = primaryByUrl.get(r.url);
    if (primary !== void 0) {
      citation.citation_id = primary;
    } else {
      delete citation.citation_id;
    }
    out.push(citation);
  }
  return out;
}
function renderCitationsXml(citations) {
  return citations.map((c) => {
    const id = c.citation_id ?? stableCitationId(c.url, 0);
    const inner = escapeXml(`${c.title}
${c.url}
${c.snippet}`);
    return `<source id="${id}">${inner}</source>`;
  }).join("\n");
}
function escapeXml(s) {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
export {
  applyAggregateMarkdownBudget,
  applyEvidenceDefault,
  applyTokenBudget,
  buildCitationsFromEvidence,
  buildEvidenceFromMarkdown,
  buildEvidenceItem,
  escapeXml,
  renderCitationsXml,
  stableCitationId
};
//# sourceMappingURL=evidence.js.map