const { BingRssSearchAdapter } = require("./adapters/bing_rss_search");
const { GenericHtmlSearchAdapter } = require("./adapters/generic_html_search");
const { sanitizeText } = require("./html_extractor");
const { canonicalUrl } = require("./url_policy");

class LumiSearchBroker {
  constructor(options = {}) {
    this.fetcher = options.fetcher;
    this.adapters = options.adapters || [
      new GenericHtmlSearchAdapter({ fetcher: this.fetcher }),
      new BingRssSearchAdapter({ fetcher: this.fetcher })
    ];
  }

  async search(query, options = {}) {
    const errors = [];
    const rows = [];
    for (const adapter of this.adapters) {
      try {
        const result = await adapter.search(query, options);
        rows.push(...result);
        if (rows.length >= options.settings.max_results) break;
      } catch (error) {
        errors.push({ adapter: adapter.id, reason: safeAdapterError(error) });
      }
    }
    return {
      provider: "lumi_search_broker",
      results: normalizeBrokerResults(rows),
      warnings: errors.length ? errors.map((entry) => `${entry.adapter}: ${entry.reason}`) : [],
      adapter_errors: errors
    };
  }
}

function normalizeBrokerResults(rows) {
  const output = [];
  const seenUrls = new Set();
  const seenTitles = new Set();
  for (const row of rows) {
    let url;
    try {
      url = stripTracking(canonicalUrl(new URL(row.url)));
    } catch {
      continue;
    }
    const title = sanitizeText(row.title || "Untitled result", 240);
    const titleKey = title.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
    if (seenUrls.has(url) || (titleKey && seenTitles.has(titleKey))) continue;
    seenUrls.add(url);
    if (titleKey) seenTitles.add(titleKey);
    output.push({
      title,
      url,
      domain: new URL(url).hostname,
      snippet: sanitizeText(row.snippet, 900),
      date: normalizeDate(row.date),
      rank: Number(row.rank) || output.length + 1,
      source: sanitizeText(row.source || "web", 80),
      raw_source_id: sanitizeText(row.raw_source_id || "", 120) || null,
      relevance_score: Number.isFinite(Number(row.relevance_score))
        ? Number(row.relevance_score)
        : Math.max(0.1, 1 - output.length * 0.08)
    });
  }
  return output;
}

function stripTracking(value) {
  const url = new URL(value);
  for (const key of [...url.searchParams.keys()]) {
    if (/^(utm_.+|fbclid|gclid|mc_[ce]id|ref|source)$/i.test(key)) url.searchParams.delete(key);
  }
  return url.href;
}

function normalizeDate(value) {
  if (!value) return null;
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
}

function safeAdapterError(error) {
  if (error?.name === "AbortError" || error?.code === "timeout") return "timed out";
  return String(error?.message || "unavailable").replace(/https?:\/\/\S+/g, "[url]").slice(0, 180);
}

module.exports = {
  LumiSearchBroker,
  normalizeBrokerResults,
  safeAdapterError,
  stripTracking
};
