///|
/// A JSON Pointer at the use site and at the resolved definition.
pub(all) struct Location {
  pointer : String
  definition_pointer : String
} derive(Eq, ToJson)

///|
/// A supported breaking change, attributed to an affected operation.
pub(all) struct Finding {
  rule_id : String
  http_method : String
  path : String
  old_location : Location?
  new_location : Location?
  reason : String
} derive(Eq, ToJson)

///|
/// An input error or a reason why analysis could not be completed.
pub(all) struct Diagnostic {
  code : String
  severity : String
  side : String
  http_method : String
  path : String
  location : Location
  reason : String
} derive(Eq, ToJson)

///|
/// status is complete, incomplete, or invalid; it does not mean full OAS compatibility.
pub(all) struct Report {
  status : String
  findings : Array[Finding]
  diagnostics : Array[Diagnostic]
} derive(Eq, ToJson)

///|
/// Exit code 2 takes precedence over breaking changes (1).
pub fn Report::exit_code(self : Report) -> Int {
  if self.status != "complete" {
    2
  } else if self.findings.is_empty() {
    0
  } else {
    1
  }
}

///|
priv struct Node {
  raw : Json
  location : Location
  refs : Array[String]
}

///|
priv struct Context {
  root : Json
  side : String
  http_method : String
  path : String
  diagnostics : Array[Diagnostic]
  budget : WorkBudget
}

///|
priv struct WorkBudget {
  mut remaining : Int
  mut exhausted : Bool
}

///|
fn new_budget() -> WorkBudget {
  { remaining: 20000, exhausted: false, }
}

///|
fn Context::visit(self : Context, value : Node) -> Bool {
  if self.budget.remaining > 0 {
    self.budget.remaining -= 1
    true
  } else {
    if !self.budget.exhausted {
      self.budget.exhausted = true
      self.diagnose(
        "analysis-work-limit", "unsupported", value, "Analysis exceeds the shared 20000-node traversal budget; manual review is required.",
      )
    }
    false
  }
}

///|
fn Context::diagnose(
  self : Context,
  code : String,
  severity : String,
  node : Node,
  reason : String,
) -> Unit {
  self.diagnostics.push({
    code,
    severity,
    side: self.side,
    http_method: self.http_method,
    path: self.path,
    location: node.location,
    reason,
  })
}

///|
fn node(raw : Json, pointer : String) -> Node {
  { raw, location: { pointer, definition_pointer: pointer, }, refs: [], }
}

///|
fn field(value : Json, key : String) -> Json? {
  match value {
    Object(fields) => fields.get(key)
    _ => None
  }
}

///|
fn string_value(value : Json?) -> String? {
  match value {
    Some(String(s)) => Some(s)
    _ => None
  }
}

///|
fn pointer_token(value : String) -> String {
  value.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
}

///|
fn Node::child(self : Node, key : String, value : Json) -> Node {
  let token = "/" + pointer_token(key)
  {
    raw: value,
    location: {
      pointer: self.location.pointer + token,
      definition_pointer: self.location.definition_pointer + token,
    },
    refs: self.refs,
  }
}

///|
fn keys(value : Json) -> Array[String] {
  let result : Array[String] = []
  if value is Object(fields) {
    for key, _ in fields {
      result.push(key)
    }
  }
  result.sort()
  result
}

///|
fn is_true(value : Json?) -> Bool {
  match value {
    Some(True) => true
    _ => false
  }
}