///|
/// Operational counters for observing a search workload without exposing data.
pub(all) struct SearchTelemetry {
  queries : Int
  returned : Int
  filtered : Int
  empty_queries : Int
  average_results : Double
}

///|
pub impl Show for SearchTelemetry with fn output(self, logger) {
  logger.write_string(
    "SearchTelemetry{queries: " +
    self.queries.to_string() +
    ", returned: " +
    self.returned.to_string() +
    ", filtered: " +
    self.filtered.to_string() +
    ", empty_queries: " +
    self.empty_queries.to_string() +
    ", average_results: " +
    self.average_results.to_string() +
    "}",
  )
}

///|
/// Aggregate result lists into a privacy-preserving workload summary.
pub fn summarize_workload(
  batches : Array[Array[SearchResult]],
  requested_top_k : Int,
) -> SearchTelemetry {
  let mut returned = 0
  let mut empty_queries = 0
  for batch in batches {
    returned = returned + batch.length()
    if batch.length() == 0 {
      empty_queries = empty_queries + 1
    }
  }
  let queries = batches.length()
  let average_results = if queries == 0 {
    0.0
  } else {
    returned.to_double() / queries.to_double()
  }
  let filtered = if requested_top_k * queries > returned {
    requested_top_k * queries - returned
  } else {
    0
  }
  { queries, returned, filtered, empty_queries, average_results }
}

///|
/// Compare two workloads by average result count.
pub fn workload_delta(a : SearchTelemetry, b : SearchTelemetry) -> Double {
  b.average_results - a.average_results
}

///|
/// Return whether a workload has enough successful queries for a stable sample.
pub fn workload_is_stable(
  report : SearchTelemetry,
  minimum_queries : Int,
) -> Bool {
  report.queries >= minimum_queries &&
  report.queries > 0 &&
  report.empty_queries * 2 <= report.queries
}

///|
/// A deterministic report for evaluating filter selectivity.
pub(all) struct FilterReport {
  total : Int
  matched : Int
  selectivity : Double
}

///|
pub impl Show for FilterReport with fn output(self, logger) {
  logger.write_string(
    "FilterReport{total: " +
    self.total.to_string() +
    ", matched: " +
    self.matched.to_string() +
    ", selectivity: " +
    self.selectivity.to_string() +
    "}",
  )
}

///|
/// Measure how many documents survive a metadata filter.
pub fn measure_filter(
  docs : Array[Document],
  filters : Array[(String, String)],
) -> FilterReport {
  let matched = filter_documents(docs, filters).length()
  let selectivity = if docs.length() == 0 {
    0.0
  } else {
    matched.to_double() / docs.length().to_double()
  }
  { total: docs.length(), matched, selectivity }
}

///|
/// Return the fraction of approximate results that also satisfy an exact filter.
pub fn filtered_recall(
  approximate : Array[SearchResult],
  exact : Array[SearchResult],
  k : Int,
) -> Double {
  recall_at_k(approximate, exact, k)
}

///|
/// Determine whether an index appears useful for a requested recall target.
pub fn meets_recall_target(
  approximate : Array[Array[SearchResult]],
  exact : Array[Array[SearchResult]],
  k : Int,
  target : Double,
) -> Bool {
  mean_recall(approximate, exact, k) >= target
}