///|
// Caches live for one public operation. PackageSet and its documents remain
// caller-owned and may change between operations.
priv struct QueryContext {
  packages : PackageSet
  include_private : Bool
  mut providers : Map[String, Array[(String, Requirement)]]?
  steps : Map[String, Array[ResolveStep]]
  unavailable : Map[String, Array[Diagnostic]]
}

///|
fn QueryContext::new(
  packages : PackageSet,
  include_private : Bool,
  unavailable? : Map[String, Array[Diagnostic]] = Map([]),
) -> QueryContext {
  { packages, include_private, providers: None, steps: Map([]), unavailable, }
}

///|
// Only excluded identifiers carry load failures. A valid exact package wins;
// empty error arrays do not block virtual providers.
fn QueryContext::unavailable_diagnostics(
  self : QueryContext,
  id : String,
) -> Array[Diagnostic]? {
  if self.packages.packages.contains(id) {
    return None
  }
  match self.unavailable.get(id) {
    Some(problems) if !problems.is_empty() => Some(problems)
    _ => None
  }
}

///|
fn QueryContext::root_id(self : QueryContext, root : String) -> String? {
  if self.packages.packages.contains(root) {
    Some(root)
  } else if self.unavailable_diagnostics(root) is Some(_) {
    None
  } else {
    self.find_provider({ name: root, op: Any, version: "", })
  }
}

///|
// Index candidates by virtual name while preserving package insertion order
// and declaration order. Exact package identifiers are handled by callers.
fn QueryContext::find_provider(
  self : QueryContext,
  requirement : Requirement,
) -> String? {
  let providers = match self.providers {
    Some(index) => index
    None => {
      let index : Map[String, Array[(String, Requirement)]] = Map([])
      for id in self.packages.package_ids {
        let pkg = self.packages.packages.get(id).unwrap()
        guard pkg.document.field("Provides") is Some(field) else { continue }
        guard parse_requirements(field.value, source=field.location.source)
          is Ok(provides) else {
          continue
        }
        for provided in provides {
          match index.get(provided.name) {
            Some(candidates) => candidates.push((pkg.id, provided))
            None => index[provided.name] = [(pkg.id, provided)]
          }
        }
      }
      self.providers = Some(index)
      index
    }
  }
  guard providers.get(requirement.name) is Some(candidates) else { return None }
  for (id, provided) in candidates {
    if virtual_requirement_matches(requirement, provided) {
      return Some(id)
    }
  }
  None
}

///|
fn QueryContext::resolve_steps(
  self : QueryContext,
  id : String,
) -> Array[ResolveStep] {
  match self.steps.get(id) {
    Some(steps) => steps
    None => {
      let steps : Array[ResolveStep] = []
      let pkg = self.packages.packages.get(id).unwrap()
      append_resolve_steps(self, pkg, "Requires", steps)
      if self.include_private {
        append_resolve_steps(self, pkg, "Requires.private", steps)
      }
      self.steps[id] = steps
      steps
    }
  }
}