///|
/// A page of ranked search results for API pagination.
pub(all) struct ResultPage {
  results : Array[SearchResult]
  offset : Int
  limit : Int
  total : Int
  has_more : Bool
}

///|
pub impl Show for ResultPage with fn output(self, logger) {
  logger.write_string(
    "ResultPage{offset: " +
    self.offset.to_string() +
    ", limit: " +
    self.limit.to_string() +
    ", total: " +
    self.total.to_string() +
    ", has_more: " +
    self.has_more.to_string() +
    "}",
  )
}

///|
/// Convert a ranked result list into a bounded page.
pub fn paginate_results(
  results : Array[SearchResult],
  offset : Int,
  limit : Int,
) -> ResultPage {
  let safe_offset = if offset < 0 { 0 } else { offset }
  let safe_limit = if limit < 0 { 0 } else { limit }
  let total = results.length()
  let start = if safe_offset < total { safe_offset } else { total }
  let end_candidate = start + safe_limit
  let end = if end_candidate < total { end_candidate } else { total }
  let page = []
  for i = start; i < end; i = i + 1 {
    page.push(results[i])
  }
  {
    results: page,
    offset: start,
    limit: safe_limit,
    total,
    has_more: end < total,
  }
}

///|
/// Merge result lists by id, retaining the best score for each document.
pub fn merge_results(
  lists : Array[Array[SearchResult]],
  metric : DistanceMetric,
) -> Array[SearchResult] {
  let merged = Map([])
  for list in lists {
    for result in list {
      match merged.get(result.id) {
        None => merged.set(result.id, result)
        Some(previous) => {
          let keep = match metric {
            Euclidean | Manhattan => result.score < previous.score
            Cosine | DotProduct => result.score > previous.score
          }
          if keep {
            merged.set(result.id, result)
          }
        }
      }
    }
  }
  let result = []
  for entry in merged {
    result.push(entry.1)
  }
  let ascending = match metric {
    Euclidean | Manhattan => true
    _ => false
  }
  sort_results(result, ascending)
  result
}

///|
/// Remove duplicate ids while preserving the first occurrence.
pub fn unique_results(results : Array[SearchResult]) -> Array[SearchResult] {
  let seen = Map([])
  let unique = []
  for result in results {
    if !seen.contains(result.id) {
      seen.set(result.id, true)
      unique.push(result)
    }
  }
  unique
}

///|
/// Keep at most one result per metadata value, useful for diversified feeds.
pub fn diversify_results(
  results : Array[SearchResult],
  metadata_key : String,
  per_value : Int,
) -> Array[SearchResult] {
  if per_value <= 0 {
    return []
  }
  let counts = Map([])
  let selected = []
  for result in results {
    let value = metadata_value(result.metadata, metadata_key)
    let current = match counts.get(value) {
      Some(count) => count
      None => 0
    }
    if current < per_value {
      counts.set(value, current + 1)
      selected.push(result)
    }
  }
  selected
}

///|
fn metadata_value(metadata : Array[(String, String)], key : String) -> String {
  for pair in metadata {
    if pair.0 == key {
      return pair.1
    }
  }
  ""
}

///|
/// Return a score-normalized copy in the range [0, 1].
pub fn normalize_scores(
  results : Array[SearchResult],
  metric : DistanceMetric,
) -> Array[SearchResult] {
  if results.length() == 0 {
    return []
  }
  let mut min_score = results[0].score
  let mut max_score = results[0].score
  for result in results {
    if result.score < min_score {
      min_score = result.score
    }
    if result.score > max_score {
      max_score = result.score
    }
  }
  let span = max_score - min_score
  let normalized = []
  for result in results {
    let score = if span == 0.0 {
      1.0
    } else if (match metric {
        Euclidean | Manhattan => true
        _ => false
      }) {
      (max_score - result.score) / span
    } else {
      (result.score - min_score) / span
    }
    normalized.push({ id: result.id, score, metadata: result.metadata })
  }
  normalized
}

///|
/// Select the highest-scoring result from a non-empty result list.
pub fn first_result(results : Array[SearchResult]) -> SearchResult? {
  results.get(0)
}

///|
/// Return result ids in ranking order.
pub fn result_ids(results : Array[SearchResult]) -> Array[String] {
  let ids = []
  for result in results {
    ids.push(result.id)
  }
  ids
}