///|
pub(all) struct SearchHit {
  index : Int
  resource : Json
  score : Int
  matched_fields : Array[String]
} derive(Debug, Eq, ToJson)

///|
fn search_text_fields(value : Json) -> Array[(String, String)] {
  let output : Array[(String, String)] = []
  for
    path in [
      "resourceType", "id", "status", "name[0].text", "code.text", "title", "description.text",
      "subject.reference", "patient.reference",
    ] {
    let text = query_text(value, path, fallback="")
    if text != "" {
      output.push((path, text))
    }
  }
  output
}

///|
fn search_hit(value : Json, index : Int, query : String) -> SearchHit {
  let fields : Array[String] = []
  let lower_query = query.to_lower()
  for _, pair in search_text_fields(value) {
    let path = pair.0
    let text = pair.1
    if text.to_lower().contains(lower_query) {
      fields.push(path)
    }
  }
  { index, resource: value, score: fields.length(), matched_fields: fields }
}

///|
pub fn search_resource(value : Json, query : String) -> Bool {
  json_contains_text(value, query)
}

///|
pub fn search_resources(values : Array[Json], query : String) -> Array[Json] {
  if query == "" {
    values
  } else {
    values.filter(fn(value) { search_resource(value, query) })
  }
}

///|
pub fn search_hits(values : Array[Json], query : String) -> Array[SearchHit] {
  let hits = values.mapi(fn(index, value) { search_hit(value, index, query) })
  hits.filter(fn(hit) { hit.score > 0 })
}

///|
pub fn search_by_id(values : Array[Json], id : String) -> Array[Json] {
  values.filter(fn(value) { field_string(value, "id") == Some(id) })
}

///|
pub fn search_by_type(values : Array[Json], kind : String) -> Array[Json] {
  values.filter(fn(value) { resource_type(value) == Some(kind) })
}

///|
pub fn search_by_status(values : Array[Json], status : String) -> Array[Json] {
  values.filter(fn(value) { resource_status(value) == Some(status) })
}

///|
pub fn search_by_type_and_status(
  values : Array[Json],
  kind : String,
  status : String,
) -> Array[Json] {
  values.filter(fn(value) {
    resource_type(value) == Some(kind) && resource_status(value) == Some(status)
  })
}

///|
pub fn search_by_field(
  values : Array[Json],
  path : String,
  query : String,
) -> Array[Json] {
  values.filter(fn(value) {
    query_strings(value, path).any(fn(text) { text.contains(query) })
  })
}

///|
pub fn search_by_reference(
  values : Array[Json],
  reference : String,
) -> Array[Json] {
  values.filter(fn(value) {
    query_resource_references(value).contains(reference)
  })
}

///|
pub fn resource_matches_all(value : Json, expressions : Array[String]) -> Bool {
  expressions.all(fn(expression) { query_exists(value, expression) })
}

///|
pub fn resource_match_count(value : Json, expressions : Array[String]) -> Int {
  expressions.count_if(fn(expression) { query_exists(value, expression) })
}

///|
pub fn select_resources(
  values : Array[Json],
  expressions : Array[String],
) -> Array[Json] {
  values.filter(fn(value) { resource_matches_all(value, expressions) })
}

///|
pub fn find_first_resource(
  values : Array[Json],
  kind : String,
  id : String,
) -> Json? {
  search_by_id(search_by_type(values, kind), id).get(0)
}

///|
pub fn search_summary(values : Array[Json], query : String) -> String {
  let hits = search_hits(values, query)
  let counts = group_resources_by_type(hits.map(fn(hit) { hit.resource }))
  "\{hits.length()} hit(s) across \{counts.length()} resource type(s)"
}