import { deduplicateResults } from "../dedup.js";
import { decomposeQuery } from "../query.js";
import { validateLinks } from "../validator.js";
import { rerankResults } from "../rerank.js";
import { applyAllFilters } from "../filters.js";
import { runSynthesis } from "../answer-synthesis.js";
import { applyEvidenceDefault } from "../evidence.js";
import { normalizeQueries, fanOutSearch, synthesizeIntent, expandIfSingle } from "../multi-query.js";
import { filterByLanguageWithFallback } from "../language-filter.js";
import { hasRecencyIntent } from "../reranker/recency.js";
import { cacheSearchResults, getCachedSearchResults } from "../../cache/store.js";
import { fetchContentForResults } from "../content-fetch.js";
import { getConfig } from "../../config.js";
import { resolveMode } from "../../util/mode.js";
import { createLogger } from "../../logger.js";
const log = createLogger("search");
const DEFAULT_MAX_RESULTS = 5;
const MAX_RESULTS_CAP = 20;
const DEFAULT_CONTENT_MAX_CHARS = 3e4;
const DEFAULT_MAX_TOTAL_CHARS = 5e4;
function collectExactMatchUrls(results, phrases) {
  const hits = /* @__PURE__ */ new Set();
  if (phrases.length === 0) return hits;
  for (const r of results) {
    const hay = `${r.title} ${r.snippet}`.toLowerCase();
    if (phrases.some((p) => hay.includes(p))) hits.add(r.url);
  }
  return hits;
}
function stripMarkdownBodiesForAnswerMode(input, results) {
  if (input.format !== "answer" && input.format !== "stream_answer") return;
  if (input.include_full_markdown) return;
  for (const r of results) {
    if (r.markdown_content !== void 0) r.markdown_content = void 0;
  }
}
async function runSearxngSearch(input, ctx) {
  const { engines, router, backendStatus, samplingServer, onProgress } = ctx;
  const mode = resolveMode(input.mode);
  const start = Date.now();
  const config = getConfig();
  const RETIRED_FORMATS = /* @__PURE__ */ new Set(["full", "context", "highlights"]);
  const VALID_FORMATS = /* @__PURE__ */ new Set(["answer", "stream_answer"]);
  if (input.format != null) {
    const fmt = String(input.format);
    if (RETIRED_FORMATS.has(fmt)) {
      return {
        ok: false,
        error: "invalid_format",
        error_reason: `format renamed; pass 'evidence' (default \u2014 omit) or 'answer'/'stream_answer' for synthesis`,
        stage: "search"
      };
    }
    if (!VALID_FORMATS.has(fmt)) {
      return {
        ok: false,
        error: "invalid_format",
        error_reason: `unknown format='${fmt}'. Valid: omit (evidence), 'answer', 'stream_answer'`,
        stage: "search"
      };
    }
  }
  const maxResults = Math.min(input.max_results ?? DEFAULT_MAX_RESULTS, MAX_RESULTS_CAP);
  const includeContent = input.include_content ?? true;
  const contentMaxChars = input.content_max_chars ?? DEFAULT_CONTENT_MAX_CHARS;
  const maxContentChars = input.max_content_chars;
  const maxTotalChars = input.max_total_chars ?? DEFAULT_MAX_TOTAL_CHARS;
  const totalTimeoutMs = config.searchTotalTimeoutMs;
  const fetchTimeoutMs = config.searchFetchTimeoutMs;
  {
    const queryStr2 = Array.isArray(input.query) ? input.query.join(" ") : input.query;
    if (typeof queryStr2 === "string" && hasRecencyIntent(queryStr2)) {
      if (input.force_refresh !== false) input.force_refresh = true;
      if (!input.time_range) input.time_range = "week";
    }
  }
  const streamProgress = input.format === "stream_answer" ? onProgress : void 0;
  const emit = async (progress, total, message) => {
    if (!streamProgress) return;
    try {
      await streamProgress({ progress, total, message });
    } catch (err) {
      log.debug("progress notification failed", { error: String(err) });
    }
  };
  const exactPhrases = [];
  if (input.exact_match) {
    if (typeof input.query === "string") {
      const trimmed = input.query.trim().replace(/^"|"$/g, "");
      if (trimmed) exactPhrases.push(trimmed.toLowerCase());
      input.query = trimmed ? `"${trimmed}"` : input.query;
    } else if (Array.isArray(input.query)) {
      const quoted = [];
      for (const q of input.query) {
        if (typeof q !== "string") {
          quoted.push(q);
          continue;
        }
        const trimmed = q.trim().replace(/^"|"$/g, "");
        if (trimmed) exactPhrases.push(trimmed.toLowerCase());
        quoted.push(trimmed ? `"${trimmed}"` : q);
      }
      input.query = quoted;
    }
  }
  let normalizedQuery = input.query;
  let autoExpanded = false;
  if (mode !== "cache" && typeof normalizedQuery === "string") {
    normalizedQuery = expandIfSingle(normalizedQuery);
    autoExpanded = true;
  }
  const isMultiQuery = Array.isArray(normalizedQuery);
  if (isMultiQuery) {
    const normalizedQueries = normalizeQueries(normalizedQuery);
    if (normalizedQueries.length === 0) {
      return {
        ok: false,
        error: "invalid_input",
        error_reason: "All queries were empty after normalization",
        stage: "search"
      };
    }
    const displayQuery = autoExpanded && typeof input.query === "string" ? input.query : normalizedQueries[0];
    const cacheKey = normalizedQueries.join(" | ");
    const staleMaxSeconds2 = mode === "cache" ? config.fastStaleMaxHours * 3600 : 0;
    const cached2 = input.force_refresh ? null : getCachedSearchResults(cacheKey, { staleMaxSeconds: staleMaxSeconds2 });
    if (cached2 && !includeContent) {
      log.info("serving multi-query search results from cache", {
        queries: normalizedQueries,
        stale: cached2.stale ?? false
      });
      const stamped = cached2.results.slice(0, maxResults).map((r) => ({
        ...r,
        cached: true,
        cached_at: cached2.searched_at,
        ...cached2.stale ? { stale: true } : {}
      }));
      const _elapsedMq1 = Date.now() - start;
      const output3 = {
        results: stamped,
        query: displayQuery,
        engines_used: cached2.engines_used,
        total_time_ms: _elapsedMq1,
        response_time_ms: _elapsedMq1,
        queries_executed: normalizedQueries
      };
      const warning = backendStatus?.consumeWarning();
      if (warning) output3.warning = warning;
      if (input.format === "answer" || input.format === "stream_answer") {
        const synth = await runSynthesis({
          query: displayQuery,
          results: output3.results,
          samplingServer,
          maxTotalChars
        });
        if (!synth.ok) {
          output3.error = synth.error;
          output3.error_reason = synth.error_reason;
          output3.stage = synth.stage;
          if (synth.hint) output3.hint = synth.hint;
        } else {
          output3.answer = synth.data.answer;
          output3.citations = synth.data.citations;
          if (input.format === "stream_answer") output3.streaming = true;
          if (synth.data.warning) {
            output3.warning = output3.warning ? `${output3.warning}; ${synth.data.warning}` : synth.data.warning;
          }
        }
        stripMarkdownBodiesForAnswerMode(input, output3.results);
      } else if (output3.results.length > 0 && mode !== "cache") {
        await applyEvidenceDefault(input, output3, output3.results, displayQuery);
      }
      return { ok: true, data: output3 };
    }
    let activeEngines2 = engines;
    if (input.search_engines && input.search_engines.length > 0) {
      activeEngines2 = engines.filter((e) => input.search_engines.includes(e.name));
      if (activeEngines2.length === 0) {
        log.warn("no engines matched search_engines filter, using all", { requested: input.search_engines });
        activeEngines2 = engines;
      }
    }
    await emit(1, 5, `Running ${normalizedQueries.length} search queries across engines...`);
    const { results: rawResults, enginesUsed: enginesUsed2, errors: errors2 } = await fanOutSearch(
      normalizedQueries,
      mode === "cache" ? activeEngines2.slice(0, 1) : activeEngines2,
      {
        maxResults,
        timeRange: input.time_range,
        language: input.language,
        includeDomains: input.include_domains,
        excludeDomains: input.exclude_domains,
        fromDate: input.from_date,
        toDate: input.to_date,
        category: input.category
      }
    );
    const filterTargetMq = (input.language ?? "en").slice(0, 2).toLowerCase();
    const filteredMq = filterByLanguageWithFallback(rawResults, {
      target: filterTargetMq,
      dropThreshold: 0.7
    });
    const filterWarningsMq = filteredMq.warnings.join("; ");
    const filteredRaw = filteredMq.results;
    if (filteredRaw.length === 0 && input.format !== "answer" && input.format !== "stream_answer") {
      return {
        ok: false,
        error: "no_results",
        error_reason: errors2.length > 0 ? errors2.join("; ") : "No results found",
        stage: "search"
      };
    }
    await emit(2, 5, `Deduplicating and reranking ${filteredRaw.length} results...`);
    const exactMatchUrlsMq = collectExactMatchUrls(filteredRaw, exactPhrases);
    let merged2 = deduplicateResults(filteredRaw);
    merged2 = applyAllFilters(merged2, {
      includeDomains: input.include_domains,
      excludeDomains: input.exclude_domains,
      fromDate: input.from_date,
      toDate: input.to_date,
      category: input.category
    });
    if (exactPhrases.length > 0) {
      merged2 = merged2.filter((r) => {
        if (exactMatchUrlsMq.has(r.url)) return true;
        const hay = `${r.title} ${r.snippet}`.toLowerCase();
        return exactPhrases.some((p) => hay.includes(p));
      });
    }
    const intentString = synthesizeIntent(normalizedQueries);
    merged2 = await rerankResults(intentString, merged2, { skip: mode === "cache" });
    if (mode !== "cache") merged2 = await validateLinks(merged2);
    merged2 = merged2.slice(0, maxResults);
    const results2 = merged2.map((m) => ({
      title: m.title,
      url: m.url,
      snippet: m.snippet,
      relevance_score: m.relevance_score,
      ...m.published_date ? { published_date: m.published_date } : {}
    }));
    const searchElapsed2 = Date.now() - start;
    let fetchElapsed2 = 0;
    if (includeContent && results2.length > 0) {
      await emit(3, 5, `Fetching content from ${results2.length} sources...`);
      const fetchStart = Date.now();
      await fetchContentForResults(results2, router, {
        contentMaxChars,
        maxContentChars,
        maxTotalChars,
        fetchTimeoutMs,
        totalDeadline: start + totalTimeoutMs,
        forceRefresh: input.force_refresh ?? false,
        maxFetches: input.max_fetches
      });
      fetchElapsed2 = Date.now() - fetchStart;
    }
    try {
      cacheSearchResults(cacheKey, results2, enginesUsed2);
    } catch (err) {
      log.warn("failed to cache multi-query search results", { error: String(err) });
    }
    const _elapsedMq2 = Date.now() - start;
    const output2 = {
      results: results2,
      query: displayQuery,
      engines_used: enginesUsed2,
      total_time_ms: _elapsedMq2,
      response_time_ms: _elapsedMq2,
      search_time_ms: searchElapsed2,
      fetch_time_ms: fetchElapsed2,
      queries_executed: normalizedQueries
    };
    const combinedMq = [filterWarningsMq, backendStatus?.consumeWarning()].filter(Boolean).join("; ");
    if (combinedMq) output2.warning = combinedMq;
    if (input.format === "answer" || input.format === "stream_answer") {
      const synth = await runSynthesis({
        query: displayQuery,
        results: results2,
        samplingServer,
        maxTotalChars
      });
      if (!synth.ok) {
        output2.error = synth.error;
        output2.error_reason = synth.error_reason;
        output2.stage = synth.stage;
        if (synth.hint) output2.hint = synth.hint;
      } else {
        output2.answer = synth.data.answer;
        output2.citations = synth.data.citations;
        if (input.format === "stream_answer") output2.streaming = true;
        if (synth.data.warning) {
          output2.warning = output2.warning ? `${output2.warning}; ${synth.data.warning}` : synth.data.warning;
        }
      }
      stripMarkdownBodiesForAnswerMode(input, results2);
    } else if (results2.length > 0 && mode !== "cache") {
      await applyEvidenceDefault(input, output2, results2, displayQuery);
    }
    return { ok: true, data: output2 };
  }
  const queryStr = input.query;
  const staleMaxSeconds = mode === "cache" ? config.fastStaleMaxHours * 3600 : 0;
  const cached = input.force_refresh ? null : getCachedSearchResults(queryStr, { staleMaxSeconds });
  if (cached && !includeContent) {
    log.info("serving search results from cache", {
      query: queryStr,
      stale: cached.stale ?? false
    });
    const stamped = cached.results.slice(0, maxResults).map((r) => ({
      ...r,
      cached: true,
      cached_at: cached.searched_at,
      ...cached.stale ? { stale: true } : {}
    }));
    const _elapsedSq1 = Date.now() - start;
    const output2 = {
      results: stamped,
      query: queryStr,
      engines_used: cached.engines_used,
      total_time_ms: _elapsedSq1,
      response_time_ms: _elapsedSq1
    };
    const warning = backendStatus?.consumeWarning();
    if (warning) output2.warning = warning;
    if (input.format === "answer" || input.format === "stream_answer") {
      const synth = await runSynthesis({
        query: queryStr,
        results: output2.results,
        samplingServer,
        maxTotalChars
      });
      if (!synth.ok) {
        output2.error = synth.error;
        output2.error_reason = synth.error_reason;
        output2.stage = synth.stage;
        if (synth.hint) output2.hint = synth.hint;
      } else {
        output2.answer = synth.data.answer;
        output2.citations = synth.data.citations;
        if (input.format === "stream_answer") output2.streaming = true;
        if (synth.data.warning) {
          output2.warning = output2.warning ? `${output2.warning}; ${synth.data.warning}` : synth.data.warning;
        }
      }
      stripMarkdownBodiesForAnswerMode(input, output2.results);
    } else if (output2.results.length > 0 && mode !== "cache") {
      await applyEvidenceDefault(input, output2, output2.results, queryStr);
    }
    return { ok: true, data: output2 };
  }
  let activeEngines = engines;
  if (input.search_engines && input.search_engines.length > 0) {
    activeEngines = engines.filter((e) => input.search_engines.includes(e.name));
    if (activeEngines.length === 0) {
      log.warn("no engines matched search_engines filter, using all", { requested: input.search_engines });
      activeEngines = engines;
    }
  }
  const subQueries = decomposeQuery(queryStr);
  log.debug("query decomposition", { original: queryStr, parts: subQueries.length });
  const effectiveEngines = mode === "cache" ? activeEngines.slice(0, 1) : activeEngines;
  await emit(1, 5, `Running ${subQueries.length} search queries across ${effectiveEngines.length} engines...`);
  const allRaw = [];
  const enginesUsed = /* @__PURE__ */ new Set();
  const errors = [];
  const hasFilterAttrition = !!(input.include_domains?.length || input.exclude_domains?.length);
  const overfetchFactor = hasFilterAttrition ? 3 : 2;
  const searchPromises = effectiveEngines.flatMap(
    (engine) => subQueries.map(async (query) => {
      try {
        const results2 = await engine.search(query, {
          maxResults: maxResults * overfetchFactor,
          timeRange: input.time_range,
          language: input.language,
          includeDomains: input.include_domains,
          excludeDomains: input.exclude_domains,
          fromDate: input.from_date,
          toDate: input.to_date,
          category: input.category
        });
        for (const r of results2) {
          allRaw.push(r);
          enginesUsed.add(engine.name);
        }
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        log.warn("engine search failed", { engine: engine.name, query, error: msg });
        errors.push(`${engine.name}: ${msg}`);
      }
    })
  );
  await Promise.allSettled(searchPromises);
  const filterTargetSq = (input.language ?? "en").slice(0, 2).toLowerCase();
  const filteredSq = filterByLanguageWithFallback(allRaw, {
    target: filterTargetSq,
    dropThreshold: 0.4
  });
  const filterWarningsSq = filteredSq.warnings.join("; ");
  const filteredAllRaw = filteredSq.results;
  if (filteredAllRaw.length === 0 && input.format !== "answer" && input.format !== "stream_answer") {
    return {
      ok: false,
      error: "no_results",
      error_reason: errors.length > 0 ? errors.join("; ") : "No results found",
      stage: "search"
    };
  }
  await emit(2, 5, `Deduplicating and reranking ${filteredAllRaw.length} results...`);
  const exactMatchUrlsSq = collectExactMatchUrls(filteredAllRaw, exactPhrases);
  let merged = deduplicateResults(filteredAllRaw);
  merged = applyAllFilters(merged, {
    includeDomains: input.include_domains,
    excludeDomains: input.exclude_domains,
    fromDate: input.from_date,
    toDate: input.to_date,
    category: input.category
  });
  if (exactPhrases.length > 0) {
    merged = merged.filter((r) => {
      if (exactMatchUrlsSq.has(r.url)) return true;
      const hay = `${r.title} ${r.snippet}`.toLowerCase();
      return exactPhrases.some((p) => hay.includes(p));
    });
  }
  merged = await rerankResults(queryStr, merged, { skip: mode === "cache" });
  if (mode !== "cache") merged = await validateLinks(merged);
  merged = merged.slice(0, maxResults);
  const results = merged.map((m) => ({
    title: m.title,
    url: m.url,
    snippet: m.snippet,
    relevance_score: m.relevance_score,
    ...m.published_date ? { published_date: m.published_date } : {}
  }));
  const searchElapsed = Date.now() - start;
  let fetchElapsed = 0;
  if (includeContent && results.length > 0) {
    await emit(3, 5, `Fetching content from ${results.length} sources...`);
    const fetchStart = Date.now();
    await fetchContentForResults(results, router, {
      contentMaxChars,
      maxContentChars,
      maxTotalChars,
      fetchTimeoutMs,
      totalDeadline: start + totalTimeoutMs,
      forceRefresh: input.force_refresh ?? false,
      maxFetches: input.max_fetches
    });
    fetchElapsed = Date.now() - fetchStart;
  }
  try {
    cacheSearchResults(queryStr, results, [...enginesUsed]);
  } catch (err) {
    log.warn("failed to cache search results", { error: String(err) });
  }
  const _elapsedSq2 = Date.now() - start;
  const output = {
    results,
    query: queryStr,
    engines_used: [...enginesUsed],
    total_time_ms: _elapsedSq2,
    response_time_ms: _elapsedSq2,
    search_time_ms: searchElapsed,
    fetch_time_ms: fetchElapsed
  };
  const combinedSq = [filterWarningsSq, backendStatus?.consumeWarning()].filter(Boolean).join("; ");
  if (combinedSq) output.warning = combinedSq;
  if (input.format === "answer" || input.format === "stream_answer") {
    const synth = await runSynthesis({
      query: queryStr,
      results,
      samplingServer,
      maxTotalChars
    });
    if (!synth.ok) {
      output.error = synth.error;
      output.error_reason = synth.error_reason;
      output.stage = synth.stage;
      if (synth.hint) output.hint = synth.hint;
    } else {
      output.answer = synth.data.answer;
      output.citations = synth.data.citations;
      if (input.format === "stream_answer") output.streaming = true;
      if (synth.data.warning) {
        output.warning = output.warning ? `${output.warning}; ${synth.data.warning}` : synth.data.warning;
      }
    }
    stripMarkdownBodiesForAnswerMode(input, results);
  } else if (results.length > 0 && mode !== "cache") {
    await applyEvidenceDefault(input, output, results, queryStr);
  }
  return { ok: true, data: output };
}
export {
  runSearxngSearch,
  stripMarkdownBodiesForAnswerMode
};
//# sourceMappingURL=searxng-orchestrator.js.map