import { runV1Search } from "./orchestrator.js";
import { applyContextRank } from "./context-rank.js";
import { expandQuery, LOW_RECALL_THRESHOLD } from "./query-expansion.js";
import { dedupAgainstRecentUrls } from "./recent-cache-dedup.js";
import { detectBrandCollision, detectLexicalCollision } from "./brand-collision.js";
import { computeFreshnessSignal } from "./freshness.js";
import { buildQueryUnderstanding } from "./query-understanding.js";
import { buildEngineWarnings } from "./engine-warnings.js";
import { faviconUrlFor } from "./favicon.js";
import { runSynthesis } from "../answer-synthesis.js";
import { applyEvidenceDefault, renderCitationsXml } from "../evidence.js";
import { fetchContentForResults } from "../content-fetch.js";
import {
  buildSearchCacheKey,
  cacheSearchResults,
  getCachedSearchResults
} from "../../cache/store.js";
import { getConfig } from "../../config.js";
import { createLogger } from "../../logger.js";
const log = createLogger("search");
const DEFAULT_CONTENT_MAX_CHARS = 3e4;
const DEFAULT_MAX_TOTAL_CHARS = 5e4;
const RRF_K = 60;
function hostnameOf(url) {
  try {
    return new URL(url).hostname.toLowerCase();
  } catch {
    return "";
  }
}
function matchesAnyDomain(url, domains) {
  const host = hostnameOf(url);
  if (!host) return false;
  for (const raw of domains) {
    const needle = raw.toLowerCase().replace(/^\./, "");
    if (host === needle || host.endsWith(`.${needle}`)) return true;
  }
  return false;
}
function normalizeArrayQueries(queries) {
  const seen = /* @__PURE__ */ new Set();
  const out = [];
  for (const raw of queries) {
    if (typeof raw !== "string") continue;
    const q = raw.trim();
    if (q.length === 0) continue;
    const key = q.toLowerCase().replace(/\s+/g, " ");
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(q);
  }
  return out;
}
function fuseRankedLists(lists) {
  const scores = /* @__PURE__ */ new Map();
  const firstSeen = /* @__PURE__ */ new Map();
  for (const list of lists) {
    for (let rank = 0; rank < list.length; rank++) {
      const r = list[rank];
      const prev = scores.get(r.url) ?? 0;
      scores.set(r.url, prev + 1 / (RRF_K + rank + 1));
      if (!firstSeen.has(r.url)) firstSeen.set(r.url, r);
    }
  }
  const maxScore = Math.max(0, ...scores.values());
  return [...scores.entries()].sort((a, b) => b[1] - a[1]).map(([url, score]) => {
    const base = firstSeen.get(url);
    if (!base) return void 0;
    return {
      ...base,
      relevance_score: maxScore > 0 ? score / maxScore : 0
    };
  }).filter((r) => r !== void 0);
}
class CoreSearchProvider {
  name = "core";
  async search(input, ctx) {
    const isArray = Array.isArray(input.query);
    const queries = isArray ? normalizeArrayQueries(input.query) : typeof input.query === "string" && input.query.trim() !== "" ? [input.query.trim()] : [];
    if (queries.length === 0) {
      return {
        ok: false,
        error: "invalid_input",
        error_reason: "Query is empty",
        stage: "search"
      };
    }
    const category = input.category;
    const depth = input.search_depth ?? "balanced";
    const isImagesCategory = category === "images";
    const start = Date.now();
    const displayQuery = isArray ? input.query.filter(Boolean).join(" | ") : queries[0];
    const cacheKey = buildSearchCacheKey(queries.join(" | "), {
      category: input.category,
      include_domains: input.include_domains,
      exclude_domains: input.exclude_domains,
      max_results: input.max_results,
      from_date: input.from_date,
      to_date: input.to_date,
      language: input.language,
      time_range: input.time_range,
      exact_match: input.exact_match
    });
    let items = [];
    let enginesUsed = [];
    let allDegraded = false;
    let searchElapsed = 0;
    let fetchElapsed = 0;
    let contentFetched = false;
    let servedFromCache = false;
    let cachedAt;
    let engineOutcomes;
    let engineTelemetry;
    if (!input.force_refresh) {
      try {
        const cached = getCachedSearchResults(cacheKey);
        if (cached && !cached.stale) {
          let filtered = cached.results;
          if (input.include_domains?.length) {
            filtered = filtered.filter((r) => matchesAnyDomain(r.url, input.include_domains));
          }
          if (input.exclude_domains?.length) {
            filtered = filtered.filter((r) => !matchesAnyDomain(r.url, input.exclude_domains));
          }
          if (typeof input.max_results === "number" && input.max_results >= 0) {
            filtered = filtered.slice(0, input.max_results);
          }
          items = filtered.map((r) => ({ ...r, cached: true, cached_at: cached.searched_at }));
          enginesUsed = cached.engines_used;
          servedFromCache = true;
          cachedAt = cached.searched_at;
          contentFetched = items.some((i) => typeof i.markdown_content === "string" && i.markdown_content.length > 0);
        }
      } catch (err) {
        log.debug("cache lookup failed", { error: String(err) });
      }
    }
    let ultraFastMiss = false;
    if (!servedFromCache && depth === "ultra-fast") {
      ultraFastMiss = true;
    }
    const autoRewrites = [];
    if (!servedFromCache && !ultraFastMiss) {
      const dispatches = await Promise.all(
        queries.map(
          (q) => runV1Search({
            query: q,
            category,
            fromDate: input.from_date,
            toDate: input.to_date,
            maxResults: input.max_results,
            language: input.language,
            includeDomains: input.include_domains,
            excludeDomains: input.exclude_domains,
            includeScoreBreakdown: input.include_engine_outcomes,
            country: input.country,
            timeRange: input.time_range,
            exactMatch: input.exact_match
          })
        )
      );
      let fused = dispatches.length === 1 ? dispatches[0].results : fuseRankedLists(dispatches.map((d) => d.results));
      if (category !== "images" && queries.length === 1 && fused.length <= LOW_RECALL_THRESHOLD) {
        const rewrite = expandQuery(queries[0]);
        if (rewrite && rewrite.trim() !== queries[0].trim()) {
          log.debug("low-recall expansion firing", {
            original: queries[0],
            rewrite,
            initialResultCount: fused.length
          });
          const retry = await runV1Search({
            query: rewrite,
            category,
            fromDate: input.from_date,
            toDate: input.to_date,
            maxResults: input.max_results,
            language: input.language,
            includeDomains: input.include_domains,
            excludeDomains: input.exclude_domains,
            includeScoreBreakdown: input.include_engine_outcomes,
            country: input.country,
            timeRange: input.time_range,
            exactMatch: input.exact_match
          });
          fused = fuseRankedLists([fused, retry.results]);
          autoRewrites.push(rewrite);
          dispatches.push(retry);
        }
      }
      const enginesUsedSet = /* @__PURE__ */ new Set();
      for (const d of dispatches) {
        for (const e of d.enginesUsed) enginesUsedSet.add(e);
      }
      enginesUsed = [...enginesUsedSet];
      allDegraded = dispatches.every((d) => d.degraded);
      if (input.include_engine_outcomes) {
        engineOutcomes = [];
        for (const d of dispatches) {
          for (const o of d.outcomes) {
            engineOutcomes.push({
              engine: o.engine,
              ok: o.ok,
              latency_ms: o.latencyMs,
              result_count: o.results.length,
              ...o.error ? { error: o.error } : {},
              ...o.skipped ? { skipped: true } : {}
            });
          }
        }
      }
      const telemetryByEngine = /* @__PURE__ */ new Map();
      const fusedUrlSet = new Set(fused.map((r) => r.url));
      for (const d of dispatches) {
        for (const o of d.outcomes ?? []) {
          const existing = telemetryByEngine.get(o.engine);
          const outcome = o.skipped ? "skipped" : o.ok ? "ok" : "error";
          const kept = o.results.reduce(
            (acc, r) => fusedUrlSet.has(r.url) ? acc + 1 : acc,
            0
          );
          if (existing) {
            existing.latency_ms += o.latencyMs;
            existing.result_count += o.results.length;
            existing.dedup_kept += kept;
            if (outcome !== "ok" && existing.outcome === "ok") existing.outcome = outcome;
            if (o.error && !existing.error) existing.error = o.error;
          } else {
            telemetryByEngine.set(o.engine, {
              name: o.engine,
              latency_ms: o.latencyMs,
              result_count: o.results.length,
              outcome,
              dedup_kept: kept,
              ...o.error ? { error: o.error } : {}
            });
          }
        }
      }
      engineTelemetry = [...telemetryByEngine.values()];
      if (engineTelemetry.length > 0) {
        enginesUsed = engineTelemetry.filter((t) => t.dedup_kept > 0).map((t) => t.name);
      } else {
        const enginesUsedSet2 = /* @__PURE__ */ new Set();
        for (const d of dispatches) {
          for (const e of d.enginesUsed) enginesUsedSet2.add(e);
        }
        enginesUsed = [...enginesUsedSet2];
      }
      let processed = fused;
      if (depth !== "fast" && (input.agent_context?.text || input.agent_context?.intent)) {
        const contextText = input.agent_context.text ?? input.agent_context.intent;
        processed = await applyContextRank(processed, queries[0], contextText);
      }
      if (input.agent_context?.recent_urls?.length) {
        processed = dedupAgainstRecentUrls(processed, input.agent_context.recent_urls);
      }
      const maxResults = input.max_results ?? processed.length;
      items = processed.slice(0, maxResults).map((r) => {
        const freshness = computeFreshnessSignal(r.url, r.published_date);
        return {
          title: r.title,
          url: r.url,
          snippet: r.snippet,
          relevance_score: r.relevance_score,
          ...r.published_date ? { published_date: r.published_date } : {},
          // Slice 8 / L2: omit the field entirely when the freshness
          // helper returns undefined (the "no parseable date" case) so the
          // response shape stays clean.
          ...freshness ? { freshness_signal: freshness } : {},
          ...r.evidence_score ? { evidence_score: r.evidence_score } : {},
          ...r.image_url ? { image_url: r.image_url } : {},
          ...r.image_alt ? { image_alt: r.image_alt } : {},
          // Image-search fields (Slice S11a): only set when the engine carried
          // them — non-image engines leave these undefined so the response
          // shape stays slim for general/code/docs/papers verticals.
          ...r.thumbnail_url ? { thumbnail_url: r.thumbnail_url } : {},
          ...typeof r.width === "number" && typeof r.height === "number" ? { width: r.width, height: r.height } : {},
          ...r._score_breakdown ? { _score_breakdown: r._score_breakdown } : {}
        };
      });
      searchElapsed = Date.now() - start;
      const includeContent = input.include_content !== false && depth !== "fast" && !isImagesCategory;
      if (includeContent && ctx.router && items.length > 0) {
        const config = getConfig();
        const fetchStart = Date.now();
        await fetchContentForResults(items, ctx.router, {
          contentMaxChars: input.content_max_chars ?? DEFAULT_CONTENT_MAX_CHARS,
          maxContentChars: input.max_content_chars,
          maxTotalChars: input.max_total_chars ?? DEFAULT_MAX_TOTAL_CHARS,
          fetchTimeoutMs: config.searchFetchTimeoutMs,
          totalDeadline: start + config.searchTotalTimeoutMs,
          forceRefresh: input.force_refresh ?? false,
          maxFetches: input.max_fetches
        });
        fetchElapsed = Date.now() - fetchStart;
        contentFetched = true;
      }
      if (items.length > 0) {
        try {
          cacheSearchResults(cacheKey, items, enginesUsed);
        } catch (err) {
          log.debug("search cache write failed", { error: String(err) });
        }
      }
    }
    void cachedAt;
    const queryUnderstanding = buildQueryUnderstanding(displayQuery, {
      category,
      language: input.language,
      rewrites: [...autoRewrites]
    });
    if (input.include_favicon) {
      for (const it of items) {
        const fav = faviconUrlFor(it.url);
        if (fav) it.favicon = fav;
      }
    }
    const totalTimeMs = Date.now() - start;
    const engineWarnings = buildEngineWarnings(engineTelemetry);
    const data = {
      results: items,
      query: displayQuery,
      engines_used: enginesUsed,
      total_time_ms: totalTimeMs,
      response_time_ms: totalTimeMs,
      search_time_ms: searchElapsed,
      fetch_time_ms: fetchElapsed,
      query_understanding: queryUnderstanding,
      ...engineOutcomes ? { engine_outcomes: engineOutcomes } : {},
      ...engineTelemetry ? { engine_telemetry: engineTelemetry } : {},
      // Always emit on engine-pool path (telemetry present); cache hits
      // intentionally omit since there's no telemetry to source from.
      ...engineTelemetry ? { engine_warnings: engineWarnings } : {}
    };
    const collisionWarning = detectBrandCollision(displayQuery, items.map((i) => i.url)) ?? detectLexicalCollision(displayQuery);
    if (collisionWarning) data.brand_collision_warning = collisionWarning;
    if (input.include_images || isImagesCategory) {
      data.images = items.filter((it) => typeof it.image_url === "string" && it.image_url.length > 0).map((it) => ({
        url: it.image_url,
        ...it.image_alt ? { alt: it.image_alt } : {},
        source_url: it.url,
        ...it.thumbnail_url ? { thumbnail_url: it.thumbnail_url } : {},
        ...typeof it.width === "number" && typeof it.height === "number" ? { width: it.width, height: it.height } : {},
        ...it.title ? { title: it.title } : {}
      }));
    }
    if (allDegraded) {
      data.warning = "all engines failed or no results";
    }
    if (ultraFastMiss) {
      data.notice = "cache miss, retry with search_depth=fast or higher";
    }
    if (input.format === "answer" || input.format === "stream_answer") {
      const synthResult = await runSynthesis({
        query: displayQuery,
        results: items,
        samplingServer: ctx.samplingServer,
        maxTotalChars: input.max_total_chars ?? DEFAULT_MAX_TOTAL_CHARS
      });
      if (synthResult.ok) {
        data.answer = synthResult.data.answer;
        if (synthResult.data.citations.length > 0) {
          data.citations = synthResult.data.citations;
        }
        if (synthResult.data.warning) {
          data.warning = synthResult.data.warning ? data.warning ? `${data.warning}; ${synthResult.data.warning}` : synthResult.data.warning : data.warning;
        }
        if (synthResult.data.synthesis_status) {
          data.synthesis_status = synthResult.data.synthesis_status;
          data.synthesis_provider = synthResult.data.synthesis_provider;
          data.synthesis_model = synthResult.data.synthesis_model;
          data.synthesis_advice = synthResult.data.synthesis_advice;
        }
      } else {
        data.warning = `synthesis failed: ${synthResult.error_reason}`;
      }
      if (input.format === "stream_answer") {
        data.streaming = true;
      }
      if (!input.include_full_markdown) {
        for (const r of items) {
          if (r.markdown_content !== void 0) r.markdown_content = void 0;
        }
      }
    } else if (items.length > 0 && contentFetched) {
      await applyEvidenceDefault(input, data, items, displayQuery);
    }
    if (input.citation_format) {
      if (!data.citations || data.citations.length === 0) {
        const built = items.map((r, i) => ({
          index: i + 1,
          url: r.url,
          title: r.title,
          snippet: r.snippet
        }));
        if (built.length > 0) data.citations = built;
      }
      if (input.citation_format === "anthropic_tags" && data.citations && data.citations.length > 0 && !data.citations_xml) {
        data.citations_xml = renderCitationsXml(data.citations);
      }
    }
    return { ok: true, data };
  }
}
export {
  CoreSearchProvider
};
//# sourceMappingURL=core-provider.js.map