am {number} params.historyLimit
 * @returns {Promise<object[]>}
 */
async function searchBrowsingHistoryTimeRange({
  startTs,
  endTs,
  historyLimit,
}) {
  const semanticManager = lazy.getPlacesSemanticHistoryManager();
  const conn = await semanticManager.getConnection();

  const results = await conn.executeCached(
    `
      SELECT id,
             title,
             url,
             NULL AS distance,
             visit_count,
             frecency,
             last_visit_date,
             preview_image_url
      FROM moz_places
      WHERE frecency <> 0
      AND (:startTs IS NULL OR last_visit_date >= :startTs)
      AND (:endTs IS NULL OR last_visit_date <= :endTs)
      ORDER BY last_visit_date DESC, frecency DESC
      LIMIT :limit
    `,
    {
      startTs,
      endTs,
      limit: historyLimit,
    }
  );

  const rows = [];
  for (let row of results) {
    rows.push(await buildHistoryRow(row));
  }
  return rows;
}

/**
 * Normalize tensor/output format from the embedder into a single vector.
 *
 * @param {Array|object} tensor
 * @returns {Array|Float32Array}
 */
function extractVectorFromTensor(tensor) {
  if (!tensor) {
    throw new Error("Unexpected empty tensor");
  }

  // Case 1: { output: ... } or { metrics, output }
  if (tensor.output) {
    if (
      Array.isArray(tensor.output) &&
      (Array.isArray(tensor.output[0]) || ArrayBuffer.isView(tensor.output[0]))
    ) {
      // output is an array of vectors, return the first
      return tensor.output[0];
    }
    // output is already a single vector
    return tensor.output;
  }

  // Case 2: tensor is nested like [[...]]
  if (
    Array.isArray(tensor) &&
    tensor.length === 1 &&
    Array.isArray(tensor[0])
  ) {
    tensor = tensor[0];
  }

  // Then we check if it's an array of arrays or just a single value.
  if (
    Array.isArray(tensor) &&
    (Array.isArray(tensor[0]) || ArrayBuffer.isView(tensor[0]))
  ) {
    return tensor[0];
  }

  return tensor;
}

/**
 * Semantic browsing history search using embeddings.
 *
 * This performs a two-stage retrieval for performance:
 * 1. Coarse search: over the quantized embeddings (`embedding_coarse`) to
 *    quickly select up to 100 candidate rows. This hard limit keeps the
 *    expensive cosine-distance computation bounded.
 * 2. Refined search: computes the exact cosine distance for those candidates
 *    and applies the caller-provided `historyLimit` and `distanceThreshold`
 *    filters.
 *
 * @param {object} params
 * @param {string} params.searchTerm
 * @param {number|null} params.startTs
 * @param {number|null} params.endTs
 * @param {number} params.historyLimit
 * @param {number} params.distanceThreshold
 * @returns {Promise<object[]>}
 */
async function searchBrowsingHistorySemantic({
  searchTerm,
  startTs,
  endTs,
  historyLimit,
  distanceThreshold,
}) {
  const semanticManager = lazy.getPlacesSemanticHistoryManager();
  await semanticManager.embedder.ensureEngine();

  // Embed search term
  let tensor = await semanticManager.embedder.embed(searchTerm);
  const vec = extractVectorFromTensor(tensor);
  const vector = lazy.PlacesUtils.tensorToSQLBindable(vec);

  let conn = await semanticManager.getConnection();
  const results = await conn.executeCached(
    `
    WITH coarse_matches AS (
      SELECT rowid,
             embedding
      FROM vec_history
      WHERE embedding_coarse match vec_quantize_binary(:vector)
      ORDER BY distance
      LIMIT 100
    ),
    matches AS (
      SELECT url_hash, vec_distance_cosine(embedding, :vector) AS distance
      FROM vec_history_mapping
      JOIN coarse_matches USING (rowid)
      WHERE distance <= :distanceThreshold
      ORDER BY distance
      LIMIT :limit
    )
    SELECT id,
           title,
           url,
           distance,
           visit_count,
           frecency,
           last_visit_date,
           preview_image_url
    FROM moz_places
    JOIN matches USING (url_hash)
    WHERE frecency <> 0
    AND (:startTs IS NULL OR last_visit_date >= :startTs)
    AND (:endTs IS NULL OR last_visit_date <= :endTs)
    ORDER BY distance
    `,
    {
      vector,
      distanceThreshold,
      limit: historyLimit,
      startTs,
      endTs,
    }
  );

  const rows = [];
  for (let row of results) {
    rows.push(await buildHistoryRow(row));
  }

  // Domain fallback for general-category queries (games, movies, news, etc.)
  // Keep semantic ranking primary, only top-up if we have room.
  if (rows.length < historyLimit) {
    const domains =
      lazy.SearchBrowsingHistoryDomainBoost.matchDomains(searchTerm);
    if (domains?.length) {
      const domainRows =
        await lazy.SearchBrowsingHistoryDomainBoost.searchByDomains({
          conn,
          domains,
          startTs,
          endTs,
          historyLimit: Math.max(historyLimit * 2, 200), // extra for dedupe
          buildHistoryRow,
        });

      return lazy.SearchBrowsingHistoryDomainBoost.mergeDedupe(
        rows,
        domainRows,
        historyLimit
      );
    }
  }

  return rows;
}

/**
 * Browsing history search using the default history search.
 *
 * @param {object} params
 * @param {string} params.searchTerm
 * @param {number} params.historyLimit
 * @returns {Promise<object[]>}
 */
async function searchBrowsingHistoryBasic({ searchTerm, historyLimit }) {
  let root;
  let openedRoot = false;

  try {
    const currentHistory = lazy.PlacesUtils.history;
    const query = currentHistory.getNewQuery();
    const opts = currentHistory.getNewQueryOptions();

    // Use Places' built-in text filtering
    query.searchTerms = searchTerm;

    // Simple URI results, ranked by frecency
    opts.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_URI;
    opts.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING;
    opts.maxResults = historyLimit;
    opts.excludeQueries = false;
    opts.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;

    const result = currentHistory.executeQuery(query, opts);
    root = result.root;

    if (!root.containerOpen) {
      root.containerOpen = true;
      openedRoot = true;
    }

    const rows = [];
    for (let i = 0; i < root.childCount && rows.length < historyLimit; i++) {
      const node = root.getChild(i);
      rows.push(await buildHistoryRow(node, true));
    }
    return rows;
  } catch (error) {
    console.error("Error searching browser history:", error);
    return [];
  } finally {
    if (root && openedRoot) {
      root.containerOpen = false;
    }
  }
}

/**
 * Searches browser history using semantic search when possible, otherwise basic
 * text search or time-range filtering.
 *
 * Rules:
 *   - Empty searchTerm: time-range search (if start/end given) or recent history.
 *   - Non-empty searchTerm: semantic search when available, otherwise basic text
 *     search (ignore time filtering).
 *
 * @param {object} params
 *  The search parameters.
 * @param {string} params.searchTerm
 *  The search string. If null or empty, semantic search is skipped and
 *  results are filtered by time range and sorted by last_visit_date and frecency.
 * @param {string|null} params.startTs
 *  Optional local ISO-8601 start timestamp (e.g. "2025-11-07T09:00:00").
 * @param {string|null} params.endTs
 *  Optional local ISO-8601 end timestamp (e.g. "2025-11-07T09:00:00").
 * @param {number} params.historyLimit
 *  Maximum number of history results to return.
 * @returns {Promise<object>}
 *  A promise resolving to an object with the search term and history results.
 *  Includes `count` when matches exist, a `message` when none are found, or an
 *  `error` string on failure.
 */
export async function searchBrowsingHistory({
  searchTerm = "",
  startTs = null,
  endTs = null,
  historyLimit = 15,
}) {
  let rows = [];

  try {
    // Convert ISO timestamp strings to microseconds to match the format used in moz_places
    const startUs = isoToMicroseconds(startTs);
    const endUs = isoToMicroseconds(endTs);

    const distanceThreshold = Services.prefs.getFloatPref(
      "places.semanticHistory.distanceThreshold",
      0.6
    );

    const semanticManager = lazy.getPlacesSemanticHistoryManager();

    // If semantic search cannot be used or we don't have enough entries, always
    // fall back to plain time-range search.
    const canUseSemantic =
      semanticManager.canUseSemanticSearch &&
      (await semanticManager.hasSufficientEntriesForSearching());

    if (!searchTerm?.trim()) {
      // Plain time-range search (no searchTerm)
      rows = await searchBrowsingHistoryTimeRange({
        startTs: startUs,
        endTs: endUs,
        historyLimit,
      });
    } else if (canUseSemantic) {
      // Semantic search
      rows = await searchBrowsingHistorySemantic({
        searchTerm,
        startTs: startUs,
        endTs: endUs,
        historyLimit,
        distanceThreshold,
      });
    } else {
      // Fallback to basic search without time window if semantic search not enable or insufficient records.
      rows = await searchBrowsingHistoryBasic({
        searchTerm,
        historyLimit,
      });
    }

    if (rows.length === 0) {
      return JSON.stringify({
        searchTerm,
        results: [],
        message: searchTerm
          ? `No browser history found for "${searchTerm}".`
          : "No browser history found in the requested time range.",
      });
    }

    // Return as JSON string with metadata
    return JSON.stringify({
      searchTerm,
      count: rows.length,
      results: rows,
    });
  } catch (error) {
    console.error("Error searching browser history:", error);
    return JSON.stringify({
      searchTerm,
      error: `Error searching browser history: ${error.message}`,
      results: [],
    });
  }
}
PK