///|
/// Exact predicates over effective, inherited top-level fields.
pub struct MetadataQuery {
  priv present : Array[String]
  priv absent : Array[String]
  priv equals : Map[String, Array[Json]]
  priv fields : Array[String]
  priv numeric : NumericRules?
}

///|
fn MetadataQuery::query_value(self : MetadataQuery) -> Json {
  let equals : Map[String, Json] = Map([])
  for field in sorted_keys(self.equals) {
    equals[field] = Json::array(self.equals[field])
  }
  let document : Map[String, Json] = {
    "present": Json::array(self.present.map(Json::string)),
    "absent": Json::array(self.absent.map(Json::string)),
    "equals": Json::object(equals),
  }
  if self.numeric is Some(rules) {
    document["numeric"] = rules.rules_value()
  }
  Json::object(document)
}

///|
pub fn MetadataQuery::to_json(self : MetadataQuery) -> String {
  canonical_json(self.query_value())
}

///|
/// All controls mandatory; AND across fields, OR within exact value choices.
/// Equality requires presence. Null is present; unknown controls never vanish.
pub fn MetadataQuery::from_json(
  text : String,
) -> MetadataQuery raise TraceError {
  guard parse_source("", text, container_limit=67)
    is Object(input) else {
    reject(
      "invalid_metadata_query", "", "Expected present, absent and equals controls.",
    )
    abort("reject raises")
  }
  for key in sorted_keys(input) {
    if !["present", "absent", "equals", "numeric"].contains(key) {
      reject(
        "unknown_metadata_query_field", key, "Use only present, absent, equals and optional numeric controls.",
      )
    }
  }
  guard input.get("present") is Some(Array(present)) &&
    input.get("absent") is Some(Array(absent)) &&
    input.get("equals") is Some(Object(equals)) else {
    reject(
      "invalid_metadata_query", "", "All three controls are mandatory: arrays present/absent and object equals.",
    )
    abort("reject raises")
  }
  // Reuse the strict exact-value contract, not approximate core Json equality.
  let expected = MetadataExpectations::from_json(
    canonical_json(
      Json::object({
        "required": Json::array(present),
        "allowed": Json::object(equals),
      }),
    ),
  )
  let missing = absent.map(value => {
    guard value is String(field) else {
      reject(
        "invalid_metadata_query", "/absent", "Absent entries must be field names.",
      )
      abort("reject raises")
    }
    field
  })
  ignore(review_fields(missing))
  missing.sort_by(lexical_compare)
  let all : Map[String, Bool] = Map([])
  for field in expected.fields {
    all[field] = true
  }
  let numeric = input
    .get("numeric")
    .map(value => NumericRules::from_json(canonical_json(value)))
  if numeric is Some(rules) {
    for rule in rules.fields {
      all[rule.field] = true
    }
  }
  for field in missing {
    if all.contains(field) {
      reject(
        "conflicting_metadata_predicate", field, "A field cannot be both absent and present/equal/numeric; remove the contradictory predicate.",
      )
    }
    all[field] = true
  }
  let legacy_fields = expected.fields + missing
  legacy_fields.sort_by(lexical_compare)
  let query : MetadataQuery = {
    present: expected.required,
    absent: missing,
    equals: expected.allowed,
    fields: review_fields(legacy_fields),
    numeric,
  }
  ignore(review_fields(sorted_keys(all)))
  if query.to_json().length() > 2097152 {
    reject(
      "input_limit", "", "Canonical metadata query exceeds 2,097,152 UTF-16 units.",
    )
  }
  query
}

///|
/// Attach/replace numeric predicates. A field cannot be both absent and numeric.
pub fn MetadataQuery::with_numeric(
  self : MetadataQuery,
  rules : NumericRules,
) -> MetadataQuery raise TraceError {
  guard self.query_value() is Object(fields) else { abort("object invariant") }
  fields["numeric"] = rules.rules_value()
  MetadataQuery::from_json(canonical_json(Json::object(fields)))
}

///|
pub struct MetadataSelection {
  priv selected : Array[String]
  priv retained : AuditReport
  priv errors : Int32
  priv document : Json
}

///|
/// Only proven matches; unresolved entries are accounted separately, never excluded.
pub fn MetadataSelection::paths(self : MetadataSelection) -> Array[String] {
  self.selected.copy()
}

///|
pub fn MetadataSelection::error_count(self : MetadataSelection) -> Int32 {
  self.errors
}

///|
/// Matches plus unresolved entries, in input order, so downstream gates see errors.
pub fn MetadataSelection::audit(self : MetadataSelection) -> AuditReport {
  self.retained
}

///|
pub fn MetadataSelection::to_json(self : MetadataSelection) -> String {
  canonical_json(self.document)
}

///|
pub fn MetadataSelection::to_json_value(self : MetadataSelection) -> Json {
  copy_json(self.document)
}

///|
/// Every input occurrence has a classification and complete resolution evidence.
/// Failed queries remain in audit(); they are not guessed to be nonmatches.
pub fn AuditReport::filter_metadata(
  self : AuditReport,
  query : MetadataQuery,
) -> MetadataSelection {
  let selected : Array[String] = []
  let retained : Array[AuditEntry] = []
  let mut errors = 0
  let mut excluded = 0
  let entries = self.entries.mapi((i, entry) => {
    let item : Map[String, Json] = {
      "query_index": Json::number(i.to_double()),
      "data_path": Json::string(entry.path),
    }
    match (entry.result, entry.error) {
      (None, Some(error)) => {
        errors += 1
        retained.push(entry)
        item["status"] = Json::string("error")
        item["diagnostic"] = error.to_json()
      }
      (Some(result), None) => {
        let mut matches = true
        let checks = query.fields.map(field => {
          let actual = result.values.get(field)
          let absent = query.absent.contains(field)
          let choices = query.equals.get(field)
          let matched = if absent {
            actual is None
          } else {
            match (actual, choices) {
              (Some(value), Some(values)) =>
                values.any(v => canonical_json(v) == canonical_json(value))
              (Some(_), None) => true
              _ => false
            }
          }
          if !matched {
            matches = false
          }
          let check : Map[String, Json] = {
            "field": Json::string(field),
            "predicate": Json::string(
              if absent {
                "absent"
              } else if choices is Some(_) {
                "equals"
              } else {
                "present"
              },
            ),
            "present": Json::boolean(actual is Some(_)),
            "matched": Json::boolean(matched),
          }
          if actual is Some(value) {
            check["actual"] = value
            check["trace"] = history_value(result.origins[field])
          }
          Json::object(check)
        })
        if query.numeric is Some(rules) {
          let (numeric_checks, failed) = rules.evaluate(result)
          item["numeric_checks"] = Json::array(numeric_checks)
          if failed > 0 {
            matches = false
          }
        }
        if matches {
          selected.push(entry.path)
          retained.push(entry)
        } else {
          excluded += 1
        }
        item["status"] = Json::string(
          if matches {
            "matched"
          } else {
            "excluded"
          },
        )
        item["checks"] = Json::array(checks)
        item["result"] = result.report_value()
      }
      _ => abort("AuditEntry invariant")
    }
    Json::object(item)
  })
  {
    selected,
    retained: { entries: retained, },
    errors,
    document: Json::object({
      "query": query.query_value(),
      "total_count": Json::number(entries.length().to_double()),
      "matched_count": Json::number(selected.length().to_double()),
      "excluded_count": Json::number(excluded.to_double()),
      "error_count": Json::number(errors.to_double()),
      "entries": Json::array(entries),
    }),
  }
}

///|
pub fn DatasetIndex::select_metadata(
  self : DatasetIndex,
  query : MetadataQuery,
) -> MetadataSelection {
  self.audit_all().filter_metadata(query)
}