///|
/// Metadata predicates used by application-facing retrieval APIs.
pub(all) enum MetadataFilter {
  MetadataEquals(String, String)
  MetadataNotEquals(String, String)
  MetadataPrefix(String, String)
  MetadataContains(String, String)
  MetadataIn(String, Array[String])
}

///|
/// A small composable expression for common metadata policies.
pub(all) enum FilterExpression {
  MatchAll
  MatchNone
  AllOf(Array[MetadataFilter])
  AnyOf(Array[MetadataFilter])
  Not(MetadataFilter)
}

///|
/// Construct an equality predicate.
pub fn metadata_equals(key : String, value : String) -> MetadataFilter {
  MetadataEquals(key, value)
}

///|
/// Construct a negative equality predicate.
pub fn metadata_not_equals(key : String, value : String) -> MetadataFilter {
  MetadataNotEquals(key, value)
}

///|
/// Construct a string-prefix predicate.
pub fn metadata_prefix(key : String, prefix : String) -> MetadataFilter {
  MetadataPrefix(key, prefix)
}

///|
/// Construct a substring predicate.
pub fn metadata_contains(key : String, fragment : String) -> MetadataFilter {
  MetadataContains(key, fragment)
}

///|
/// Construct a membership predicate.
pub fn metadata_in(key : String, values : Array[String]) -> MetadataFilter {
  MetadataIn(key, values)
}

///|
/// Return whether one metadata predicate matches a metadata array.
pub fn matches_metadata_filter(
  metadata : Array[(String, String)],
  filter : MetadataFilter,
) -> Bool {
  match filter {
    MetadataEquals(key, value) => has_metadata_pair(metadata, key, value)
    MetadataNotEquals(key, value) => !has_metadata_pair(metadata, key, value)
    MetadataPrefix(key, prefix) => {
      for pair in metadata {
        if pair.0 == key && pair.1.has_prefix(prefix) {
          return true
        }
      }
      false
    }
    MetadataContains(key, fragment) => {
      for pair in metadata {
        if pair.0 == key && pair.1.contains(fragment) {
          return true
        }
      }
      false
    }
    MetadataIn(key, values) => {
      for pair in metadata {
        if pair.0 == key && string_array_contains(values, pair.1) {
          return true
        }
      }
      false
    }
  }
}

///|
/// Evaluate a composed metadata expression.
pub fn matches_filter_expression(
  metadata : Array[(String, String)],
  expression : FilterExpression,
) -> Bool {
  match expression {
    MatchAll => true
    MatchNone => false
    AllOf(filters) => {
      for filter in filters {
        if !matches_metadata_filter(metadata, filter) {
          return false
        }
      }
      true
    }
    AnyOf(filters) => {
      for filter in filters {
        if matches_metadata_filter(metadata, filter) {
          return true
        }
      }
      false
    }
    Not(filter) => !matches_metadata_filter(metadata, filter)
  }
}

///|
/// Filter documents without changing the input order or records.
pub fn filter_documents_expression(
  docs : Array[Document],
  expression : FilterExpression,
) -> Array[Document] {
  let result = []
  for doc in docs {
    if matches_filter_expression(doc.metadata, expression) {
      result.push(doc)
    }
  }
  result
}

///|
/// Filter ranked results after retrieval while preserving ranking order.
pub fn filter_results_expression(
  results : Array[SearchResult],
  expression : FilterExpression,
) -> Array[SearchResult] {
  let result = []
  for item in results {
    if matches_filter_expression(item.metadata, expression) {
      result.push(item)
    }
  }
  result
}

///|
/// Count how many records satisfy an expression.
pub fn count_matching_documents(
  docs : Array[Document],
  expression : FilterExpression,
) -> Int {
  filter_documents_expression(docs, expression).length()
}

///|
/// Produce a stable human-readable description for audit logs.
pub fn describe_filter(filter : MetadataFilter) -> String {
  match filter {
    MetadataEquals(key, value) => key + " == " + value
    MetadataNotEquals(key, value) => key + " != " + value
    MetadataPrefix(key, value) => key + " starts_with " + value
    MetadataContains(key, value) => key + " contains " + value
    MetadataIn(key, values) => key + " in [" + values.join(", ") + "]"
  }
}

///|
/// Produce a stable human-readable description for an expression.
pub fn describe_filter_expression(expression : FilterExpression) -> String {
  match expression {
    MatchAll => "match_all"
    MatchNone => "match_none"
    AllOf(filters) => describe_filter_array(filters, " AND ")
    AnyOf(filters) => describe_filter_array(filters, " OR ")
    Not(filter) => "NOT (" + describe_filter(filter) + ")"
  }
}

///|
fn describe_filter_array(
  filters : Array[MetadataFilter],
  separator : String,
) -> String {
  let descriptions = []
  for filter in filters {
    descriptions.push(describe_filter(filter))
  }
  "(" + descriptions.join(separator) + ")"
}

///|
fn has_metadata_pair(
  metadata : Array[(String, String)],
  key : String,
  value : String,
) -> Bool {
  for pair in metadata {
    if pair.0 == key && pair.1 == value {
      return true
    }
  }
  false
}

///|
fn string_array_contains(values : Array[String], value : String) -> Bool {
  for candidate in values {
    if candidate == value {
      return true
    }
  }
  false
}