///|
/// A versioned compatibility check described by a JSON manifest.
///
/// The manifest contains one candidate snapshot, at least one historical
/// baseline snapshot, and an optional release gate. Paths are kept as supplied
/// so callers can resolve them relative to the manifest file.
pub struct ConfigCompatibilityContract {
  candidate_path : String
  baseline_paths : Array[String]
  policy : ConfigCompatibilityPolicy
} derive(Eq, Debug)

///|
/// Parse a compatibility contract from a JSON-compatible value.
///
/// The accepted shape contains string field `candidate`, a non-empty string
/// array field `baselines`, and an optional `fail_on` field.
/// `fail_on` defaults to `breaking`. Unknown object fields are ignored so
/// the manifest can grow without breaking older clients.
pub fn ConfigCompatibilityContract::from_value(
  value : ConfigValue,
) -> Result[ConfigCompatibilityContract, String] {
  if value.kind() != Object {
    return Err("contract must be a JSON object")
  }
  let candidate_path = match value.field("candidate") {
    Some(candidate_value) =>
      match candidate_value.as_string() {
        Some(path) =>
          if path == "" {
            return Err("contract field 'candidate' must be a non-empty string")
          } else {
            path
          }
        None =>
          return Err("contract field 'candidate' must be a non-empty string")
      }
    None => return Err("contract is missing required field 'candidate'")
  }
  let baselines_value = match value.field("baselines") {
    Some(baselines_value) => baselines_value
    None => return Err("contract is missing required field 'baselines'")
  }
  if baselines_value.kind() != Array {
    return Err("contract field 'baselines' must be a non-empty array")
  }
  let baseline_count = baselines_value.length().unwrap()
  if baseline_count == 0 {
    return Err("contract field 'baselines' must be a non-empty array")
  }
  let baseline_paths : Array[String] = []
  let seen : Map[String, Bool] = Map([])
  for index in 0.. baseline_value
      None =>
        return Err(
          "contract field 'baselines' contains an invalid item at index " +
          index.to_string(),
        )
    }
    let path = match baseline_value.as_string() {
      Some(path) =>
        if path == "" {
          return Err(
            "contract baseline at index " +
            index.to_string() +
            " must be a non-empty string",
          )
        } else {
          path
        }
      None =>
        return Err(
          "contract baseline at index " +
          index.to_string() +
          " must be a non-empty string",
        )
    }
    if seen.contains(path) {
      return Err(
        "contract field 'baselines' contains duplicate path '" + path + "'",
      )
    }
    seen[path] = true
    baseline_paths.push(path)
  }
  let policy = match value.field("fail_on") {
    None => BreakingOnly
    Some(policy_value) =>
      match policy_value.as_string() {
        Some("breaking") => BreakingOnly
        Some("behavioral") => NoBehavioralChanges
        Some("any") => NoChanges
        _ =>
          return Err(
            "contract field 'fail_on' must be one of breaking, behavioral, or any",
          )
      }
  }
  { candidate_path, baseline_paths, policy } |> Ok
}

///|
/// Return the candidate snapshot path from the manifest.
pub fn ConfigCompatibilityContract::candidate_path(
  self : ConfigCompatibilityContract,
) -> String {
  self.candidate_path
}

///|
/// Return baseline snapshot paths in manifest order.
pub fn ConfigCompatibilityContract::baseline_paths(
  self : ConfigCompatibilityContract,
) -> Array[String] {
  self.baseline_paths.copy()
}

///|
/// Return the release gate selected by the manifest.
pub fn ConfigCompatibilityContract::policy(
  self : ConfigCompatibilityContract,
) -> ConfigCompatibilityPolicy {
  self.policy
}