///|
/// A validated package stored under the identifier used in dependency lists.
#derive(Eq, @debug.Debug)
pub(all) struct Package {
  id : String
  document : Document
}

///|
/// An in-memory package collection. File discovery belongs to adapters and the
/// CLI; the core resolver accepts parsed documents so it works on every target.
pub(all) struct PackageSet {
  packages : Map[String, Package]
  package_ids : Array[String]
}

///|
pub fn PackageSet::new() -> PackageSet {
  { packages: Map([]), package_ids: [], }
}

///|
/// Add one package after checking its identifier and required metadata.
/// Nothing is inserted when diagnostics are returned.
pub fn PackageSet::add(
  self : PackageSet,
  id : String,
  document : Document,
  source~ : String = "package.pc",
) -> Array[Diagnostic] {
  if id.is_empty() || id.iter().any(whitespace) {
    return [
      diag(
        "PC013",
        "Package identifier must not be empty or contain whitespace",
        { source, line: 1, column: 1, },
      ),
    ]
  }
  if self.packages.get(id) is Some(_) {
    return [
      diag("PC014", "Duplicate package identifier: " + id, {
        source,
        line: 1,
        column: 1,
      }),
    ]
  }
  let diagnostics = document.validate_metadata(source~)
  if !diagnostics.is_empty() {
    return diagnostics
  }
  self.packages[id] = { id, document, }
  self.package_ids.push(id)
  []
}

///|
pub fn PackageSet::get(self : PackageSet, id : String) -> Package? {
  self.packages.get(id)
}

///|
/// Select and validate one exact package or its first matching virtual provider.
/// Dependency and conflict fields are not traversed. Original document errors
/// and unavailable-package diagnostics retain their locations in a copied array.
/// `source` identifies missing roots or metadata with no remaining source entry.
pub fn PackageSet::select(
  self : PackageSet,
  root : String,
  source~ : String = "query",
  unavailable~ : Map[String, Array[Diagnostic]] = Map([]),
) -> Result[Package, Array[Diagnostic]] {
  let context = QueryContext::new(self, false, unavailable~)
  let selected = match context.root_id(root) {
    Some(id) => self.packages.get(id)
    None => None
  }
  guard selected is Some(pkg) else {
    if context.unavailable_diagnostics(root) is Some(problems) {
      return Err(problems.copy())
    }
    return Err([
      diag("PC015", "Required package not found: " + root, {
        source,
        line: 1,
        column: 1,
      }),
    ])
  }
  if !pkg.document.diagnostics.is_empty() {
    return Err(pkg.document.diagnostics.copy())
  }
  let metadata_source = if pkg.document.entries.is_empty() {
    source
  } else {
    pkg.document.entries[0].location.source
  }
  let diagnostics = pkg.document.validate_metadata(source=metadata_source)
  if diagnostics.is_empty() {
    Ok(pkg)
  } else {
    Err(diagnostics)
  }
}

///|
// Documents are caller-owned; validation at insertion cannot justify an unwrap
// after the caller changes entries or inserts a package directly.
fn Package::checked_version(self : Package) -> Result[String, Diagnostic] {
  match self.document.field("Version") {
    Some(entry) =>
      if entry.value.is_empty() {
        Err(diag("PC011", "Required field is empty: Version", entry.location))
      } else if entry.value.iter().any(whitespace) {
        Err(
          diag("PC012", "Version must not contain whitespace", entry.location),
        )
      } else {
        Ok(entry.value)
      }
    None => {
      let source = if self.document.entries.is_empty() {
        self.id + ".pc"
      } else {
        self.document.entries[0].location.source
      }
      Err(
        diag("PC010", "Missing required field: Version", {
          source,
          line: 1,
          column: 1,
        }),
      )
    }
  }
}

///|
/// Compare a requested virtual dependency with one `Provides` declaration.
///
/// This is not ordinary interval intersection. It mirrors the comparator-pair
/// table used by pkgconf 3.0.7 so range boundaries behave the same way.
fn virtual_requirement_matches(
  requirement : Requirement,
  provided : Requirement,
) -> Bool {
  if requirement.op == Any {
    return provided.op != Any
  }
  if provided.op == Any {
    return false
  }
  let order = compare_versions(requirement.version, provided.version)
  match requirement.op {
    Any => abort("handled above")
    Less =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order > 0
        Equal => order > 0
        NotEqual => order <= 0
      }
    Greater =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order < 0
        GreaterEqual => order >= 0
        Equal => order < 0
        NotEqual => order >= 0
      }
    LessEqual =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order >= 0
        Equal => order >= 0
        NotEqual => order < 0
      }
    GreaterEqual =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order >= 0
        Equal => order <= 0
        NotEqual => order > 0
      }
    Equal =>
      match provided.op {
        Any => false
        Less => order < 0
        Greater => order > 0
        LessEqual => order <= 0
        GreaterEqual => order >= 0
        Equal => order == 0
        NotEqual => order != 0
      }
    NotEqual =>
      match provided.op {
        Any => false
        Less => order >= 0
        Greater => order <= 0
        LessEqual => order > 0
        GreaterEqual => order < 0
        Equal => order != 0
        NotEqual => order == 0
      }
  }
}

///|
/// Check a direct dependency list against the packages currently in the set.
pub fn PackageSet::check_requirements(
  self : PackageSet,
  requirements : Array[Requirement],
  source~ : String = "Requires",
) -> Array[Diagnostic] {
  let diagnostics = DiagnosticCollector::new()
  let context = QueryContext::new(self, false)
  for requirement in requirements {
    match self.packages.get(requirement.name) {
      None =>
        match context.find_provider(requirement) {
          Some(id) => {
            guard self.packages.get(id) is Some(pkg) else { continue }
            if pkg.checked_version() is Err(problem) {
              append_unavailable([problem], diagnostics)
            }
          }
          None =>
            diagnostics.push(
              diag("PC015", "Required package not found: " + requirement.name, {
                source,
                line: 1,
                column: 1,
              }),
            )
        }
      Some(pkg) => {
        let version = match pkg.checked_version() {
          Ok(version) => version
          Err(problem) => {
            append_unavailable([problem], diagnostics)
            continue
          }
        }
        if !requirement.matches(version) {
          diagnostics.push(
            diag(
              "PC016",
              "Package " +
              requirement.name +
              " has version " +
              version +
              ", which does not satisfy " +
              requirement_text(requirement),
              { source, line: 1, column: 1, },
            ),
          )
        }
      }
    }
  }
  diagnostics.values
}

///|
fn requirement_text(requirement : Requirement) -> String {
  match requirement.op {
    Any => "(any)"
    Equal => "= " + requirement.version
    NotEqual => "!= " + requirement.version
    Less => "< " + requirement.version
    LessEqual => "<= " + requirement.version
    Greater => "> " + requirement.version
    GreaterEqual => ">= " + requirement.version
  }
}