import { createLogger } from "../../logger.js";
import { classifyIntentDetailed } from "./intent-router.js";
import {
  runEnginesParallel
} from "./engine-base.js";
import { recencyMultiplier, hasTemporalIntent } from "./recency-boost.js";
import { applyAuthorityBoost } from "../reranker/authority-boost.js";
import { domainQualityScore } from "./domain-quality.js";
import { lexicalAlignment } from "./lexical-alignment.js";
import { resolveTimeRange } from "./time-range.js";
import { getConfig } from "../../config.js";
const RETAIL_TLD_RE = /\.(?:co\.uk|shop|store|deals|sale|boutique|fashion)$/i;
const BRAND_COLLISION_PENALTY = 0.3;
function explainEvidence(parts) {
  const tokens = [];
  tokens.push(`base=${parts.base.toFixed(3)}`);
  tokens.push(`domain=${parts.dq.toFixed(2)}`);
  tokens.push(`lex=${parts.la.toFixed(2)}`);
  if (parts.recencyMul !== 1) tokens.push(`recency=${parts.recencyMul.toFixed(2)}`);
  tokens.push(`engines=${parts.engineConsensus}`);
  return tokens.join(", ");
}
function applyBrandCollisionGuard(query, results) {
  const tokens = query.trim().split(/\s+/).filter(Boolean);
  if (tokens.length > 2) return results;
  if (results.length === 0) return results;
  return results.map((r) => {
    let host = "";
    try {
      host = new URL(r.url).hostname.toLowerCase();
    } catch {
      return r;
    }
    if (RETAIL_TLD_RE.test(host)) {
      return { ...r, relevance_score: r.relevance_score * BRAND_COLLISION_PENALTY };
    }
    return r;
  });
}
import { resolveEngineWeight } from "./engine-quality.js";
import { canonicalizeUrl } from "./canonical-url.js";
import { getGeneralEngines, _resetGeneralEnginesForTest } from "./verticals/general.js";
import { getNewsEngines, _resetNewsEnginesForTest } from "./verticals/news.js";
import { getCodeEngines, _resetCodeEnginesForTest } from "./verticals/code.js";
import { getDocsEngines, _resetDocsEnginesForTest } from "./verticals/docs.js";
import { getPapersEngines, _resetPapersEnginesForTest } from "./verticals/papers.js";
import { getImageEngines, _resetImageEnginesForTest } from "./verticals/images.js";
const log = createLogger("search");
const RRF_K = 60;
const DEFAULT_MAX_RESULTS = 10;
const DEFAULT_TIMEOUT_MS = 1e4;
function getEntriesForVertical(vertical) {
  switch (vertical) {
    case "general":
      return getGeneralEngines();
    case "news":
      return getNewsEngines();
    case "code":
      return getCodeEngines();
    case "docs":
      return getDocsEngines();
    case "papers":
      return getPapersEngines();
    case "images":
      return getImageEngines();
  }
}
function hostnameOf(url) {
  try {
    return new URL(url).hostname.toLowerCase();
  } catch {
    return "";
  }
}
function matchesDomain(host, domain) {
  const needle = domain.toLowerCase().replace(/^\./, "");
  if (!host) return false;
  return host === needle || host.endsWith(`.${needle}`);
}
function applyDomainFilters(results, includeDomains, excludeDomains) {
  let filtered = results;
  if (excludeDomains?.length) {
    filtered = filtered.filter((r) => {
      const host = hostnameOf(r.url);
      if (!host) return false;
      return !excludeDomains.some((d) => matchesDomain(host, d));
    });
  }
  if (!includeDomains?.length) return filtered;
  return filtered.filter((r) => {
    const host = hostnameOf(r.url);
    if (!host) return false;
    return includeDomains.some((d) => matchesDomain(host, d));
  });
}
function dedupWithinEngine(results) {
  const seen = /* @__PURE__ */ new Set();
  const out = [];
  for (const r of results) {
    if (seen.has(r.url)) continue;
    seen.add(r.url);
    out.push(r);
  }
  return out;
}
async function runV1Search(input, opts = {}) {
  const query = typeof input.query === "string" ? input.query.trim() : "";
  if (query.length === 0) {
    log.warn("orchestrator received empty query");
    return {
      vertical: "general",
      results: [],
      enginesUsed: [],
      outcomes: [],
      degraded: true
    };
  }
  const engineQuery = input.exactMatch ? `"${query.replace(/^"|"$/g, "")}"` : query;
  const exactPhrase = input.exactMatch ? query.replace(/^"|"$/g, "").toLowerCase() : "";
  const timeRangeHint = resolveTimeRange(input.timeRange);
  const callerHasDateBound = !!(input.fromDate || input.toDate || timeRangeHint);
  const classification = classifyIntentDetailed(query, {
    hint: input.category,
    hasDateBound: callerHasDateBound
  });
  const vertical = classification.vertical;
  const dateHint = timeRangeHint ?? classification.dateHint;
  const effectiveFromDate = input.fromDate ?? dateHint?.fromDate;
  const effectiveToDate = input.toDate ?? dateHint?.toDate;
  const hasDateBound = !!(effectiveFromDate || effectiveToDate);
  const allEntries = getEntriesForVertical(vertical);
  let entries = allEntries;
  if (hasDateBound) {
    const dateAware = allEntries.filter((e) => e.supportsDateFilter === true);
    entries = dateAware.length > 0 ? dateAware : allEntries;
  }
  const options = {
    maxResults: input.maxResults ?? DEFAULT_MAX_RESULTS,
    timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
    language: input.language,
    includeDomains: input.includeDomains,
    excludeDomains: input.excludeDomains,
    fromDate: effectiveFromDate,
    toDate: effectiveToDate,
    country: input.country,
    timeRange: input.timeRange,
    category: vertical === "general" ? void 0 : vertical
  };
  log.info("orchestrator dispatching engines", {
    vertical,
    engineCount: entries.length,
    hasDateBound
  });
  const outcomes = await runEnginesParallel(entries, engineQuery, options);
  const wantsRecency = vertical === "news" || hasDateBound || hasTemporalIntent(query);
  const fused = /* @__PURE__ */ new Map();
  const urlToResult = /* @__PURE__ */ new Map();
  const urlPrimaryCount = /* @__PURE__ */ new Map();
  const urlSecondaryCount = /* @__PURE__ */ new Map();
  const urlExactMatchHit = /* @__PURE__ */ new Set();
  function canonKey(url) {
    try {
      return canonicalizeUrl(url);
    } catch {
      return url;
    }
  }
  for (let i = 0; i < outcomes.length; i++) {
    const outcome = outcomes[i];
    if (!outcome.ok || outcome.results.length === 0) continue;
    const dedupedResults = dedupWithinEngine(outcome.results);
    outcome.results = dedupedResults;
    const weight = resolveEngineWeight(
      entries[i].engine.name,
      entries[i].weight,
      entries[i].quality
    );
    const isSecondary = entries[i].secondary === true;
    for (let j = 0; j < dedupedResults.length; j++) {
      const r = dedupedResults[j];
      const rank = j + 1;
      const base = weight / (RRF_K + rank);
      const recMul = wantsRecency ? recencyMultiplier(r.published_date) : 1;
      const key = canonKey(r.url);
      fused.set(key, (fused.get(key) ?? 0) + base * recMul);
      if (!urlToResult.has(key)) {
        urlToResult.set(key, r);
      }
      if (isSecondary) {
        urlSecondaryCount.set(key, (urlSecondaryCount.get(key) ?? 0) + 1);
      } else {
        urlPrimaryCount.set(key, (urlPrimaryCount.get(key) ?? 0) + 1);
      }
      if (exactPhrase) {
        const hay = `${r.title} ${r.snippet}`.toLowerCase();
        if (hay.includes(exactPhrase)) urlExactMatchHit.add(key);
      }
    }
  }
  let merged = [...fused.entries()].sort((a, b) => b[1] - a[1]).map(([key, score]) => {
    const base = urlToResult.get(key);
    return base ? { ...base, relevance_score: score } : void 0;
  }).filter((r) => r !== void 0);
  merged = applyAuthorityBoost(query, merged);
  merged = applyBrandCollisionGuard(query, merged);
  const breakdowns = input.includeScoreBreakdown ? /* @__PURE__ */ new Map() : void 0;
  const evidenceScores = /* @__PURE__ */ new Map();
  merged = merged.map((r) => {
    const key = canonKey(r.url);
    const base = r.relevance_score;
    const dq = domainQualityScore(r.url, vertical, query);
    const la = lexicalAlignment(query, r.title, r.snippet);
    const primaryCount = urlPrimaryCount.get(key) ?? 0;
    const secondaryCount = urlSecondaryCount.get(key) ?? 0;
    const isSecondaryOnly = primaryCount === 0 && secondaryCount > 0;
    const secondaryPenalty = isSecondaryOnly && la < 0.5 ? 0.3 : 1;
    const recencyMul = wantsRecency ? recencyMultiplier(r.published_date) : 1;
    const final = base * dq * (0.5 + 0.5 * la) * secondaryPenalty;
    if (breakdowns) {
      breakdowns.set(r.url, {
        base,
        domain_quality: dq,
        lexical_alignment: la,
        final
      });
    }
    evidenceScores.set(r.url, {
      final,
      components: {
        base_rrf: base,
        context_cosine: 0,
        domain_quality: dq,
        lexical_alignment: la,
        recency_boost: recencyMul,
        engine_consensus: primaryCount + secondaryCount
      },
      explanation: explainEvidence({
        base,
        dq,
        la,
        recencyMul,
        engineConsensus: primaryCount + secondaryCount
      })
    });
    return { ...r, relevance_score: final };
  });
  merged.sort((a, b) => b.relevance_score - a.relevance_score);
  merged = applyDomainFilters(merged, input.includeDomains, input.excludeDomains);
  if (effectiveFromDate) {
    merged = merged.filter((r) => {
      if (!r.published_date) return true;
      return r.published_date >= effectiveFromDate;
    });
  }
  if (effectiveToDate) {
    merged = merged.filter((r) => {
      if (!r.published_date) return true;
      return r.published_date <= effectiveToDate;
    });
  }
  if (exactPhrase) {
    merged = merged.filter((r) => {
      if (urlExactMatchHit.has(canonKey(r.url))) return true;
      const hay = `${r.title} ${r.snippet}`.toLowerCase();
      return hay.includes(exactPhrase);
    });
  }
  const maxResults = input.maxResults ?? DEFAULT_MAX_RESULTS;
  let results = merged.slice(0, maxResults);
  if (results.length > 0) {
    const maxFinal = Math.max(...results.map((r) => r.relevance_score));
    if (maxFinal > 0) {
      results = results.map((r) => ({ ...r, relevance_score: r.relevance_score / maxFinal }));
    }
  }
  const threshold = getConfig().relevanceThreshold;
  if (threshold > 0) {
    results = results.filter((r) => r.relevance_score >= threshold);
  }
  if (breakdowns) {
    results = results.map((r) => {
      const bd = breakdowns.get(r.url);
      return bd ? { ...r, _score_breakdown: bd } : r;
    });
  }
  results = results.map((r) => {
    const ev = evidenceScores.get(r.url);
    if (!ev) return r;
    return { ...r, evidence_score: { ...ev, final: r.relevance_score } };
  });
  const enginesUsed = outcomes.filter((o) => o.ok && o.results.length > 0).map((o) => o.engine);
  const degraded = outcomes.every((o) => !o.ok) || results.length === 0;
  if (degraded) {
    log.warn("orchestrator returning degraded result", {
      vertical,
      attempted: outcomes.length,
      ok: outcomes.filter((o) => o.ok).length,
      resultCount: results.length
    });
  }
  if (degraded && vertical !== "general" && vertical !== "images" && !opts._isFallback) {
    log.info("vertical degraded, falling back to general", { from: vertical });
    return runV1Search({ ...input, category: "general" }, { _isFallback: true });
  }
  return {
    vertical,
    results,
    enginesUsed,
    outcomes,
    degraded
  };
}
function _resetOrchestratorVerticalsForTest() {
  _resetGeneralEnginesForTest();
  _resetNewsEnginesForTest();
  _resetCodeEnginesForTest();
  _resetDocsEnginesForTest();
  _resetPapersEnginesForTest();
  _resetImageEnginesForTest();
}
export {
  _resetOrchestratorVerticalsForTest,
  runV1Search
};
//# sourceMappingURL=orchestrator.js.map