///|
/// A tolerance applies only to this exact RFC 6901 JSON Pointer.
pub(all) struct Tolerance {
  path : String
  absolute : Double
  relative : Double
} derive(Debug, Eq)

///|
pub(all) struct Rules {
  ignore_paths : Array[String]
  tolerances : Array[Tolerance]
} derive(Debug)

///|
pub fn Rules::new(
  ignore_paths? : Array[String] = [],
  tolerances? : Array[Tolerance] = [],
) -> Rules {
  { ignore_paths, tolerances, }
}

///|
pub(all) enum Reason {
  ValueMismatch
  TypeMismatch
  Missing
  Unexpected
  OutsideTolerance
  UnsupportedNumber
  DepthLimit
} derive(Debug, Eq)

///|
/// None represents an absent node; Some(Null) represents JSON null.
pub(all) struct Difference {
  path : String
  reason : Reason
  expected : Json?
  actual : Json?
} derive(Debug, Eq)

///|
pub suberror InvalidRule {
  InvalidRule(String)
} derive(Debug)

///|
pub suberror Mismatch {
  Mismatch(String)
} derive(Debug)

///|
fn valid_pointer(path : String) -> Bool {
  if path == "" {
    return true
  }
  let chars = path.iter().to_array()
  if chars[0] != '/' {
    return false
  }
  let mut i = 1
  while i < chars.length() {
    if chars[i] == '~' {
      if i + 1 >= chars.length() || (chars[i + 1] != '0' && chars[i + 1] != '1') {
        return false
      }
      i = i + 2
    } else {
      i = i + 1
    }
  }
  true
}

///|
fn validate(rules : Rules) -> Unit raise InvalidRule {
  for path in rules.ignore_paths {
    if !valid_pointer(path) {
      raise InvalidRule("Invalid JSON Pointer: \{path}")
    }
  }
  let seen : Map[String, Bool] = Map([])
  for rule in rules.tolerances {
    if !valid_pointer(rule.path) {
      raise InvalidRule("Invalid JSON Pointer: \{rule.path}")
    }
    for value in [rule.absolute, rule.relative] {
      if value < 0.0 || value.is_nan() || value.is_inf() {
        raise InvalidRule(
          "Tolerance must be finite and non-negative: \{rule.path}",
        )
      }
    }
    if seen.contains(rule.path) {
      raise InvalidRule("Duplicate tolerance path: \{rule.path}")
    }
    seen[rule.path] = true
  }
}

///|
fn child_path(path : String, key : String) -> String {
  let escaped = key
    .replace_all(old="~", new="~0")
    .replace_all(old="/", new="~1")
  "\{path}/\{escaped}"
}

///|
fn kind(value : Json) -> String {
  match value {
    Null => "null"
    True | False => "boolean"
    Number(_, ..) => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) => "object"
  }
}

///|
// Conservatively fail closed for unsafe magnitudes and parser underflow.
// Ordinary finite decimals still use Double semantics, not exact decimal math.
fn supported_number(value : Double, repr : String?) -> Bool {
  if value.is_nan() || value.is_inf() || value.abs() >= 9007199254740992.0 {
    return false
  }
  if value == 0.0 {
    if repr is Some(text) {
      for char in text {
        if char == 'e' || char == 'E' {
          break
        }
        if char >= '1' && char <= '9' {
          return false
        }
      }
    }
  }
  true
}

///|
fn walk(
  expected : Json?,
  actual : Json?,
  path : String,
  depth : Int,
  rules : Rules,
  output : Array[Difference],
) -> Unit {
  if rules.ignore_paths.contains(path) {
    return
  }
  let reason : Reason = match (expected, actual) {
    (None, None) => return
    (Some(_), None) => Missing
    (None, Some(_)) => Unexpected
    (Some(left), Some(right)) =>
      if depth > 128 {
        DepthLimit
      } else {
        match (left, right) {
          (Object(a), Object(b)) => {
            let keys = a.keys().to_array()
            for key in b.keys() {
              if !a.contains(key) {
                keys.push(key)
              }
            }
            keys.sort_by(fn(a, b) { a.lexical_compare(b) })
            for key in keys {
              walk(
                a.get(key),
                b.get(key),
                child_path(path, key),
                depth + 1,
                rules,
                output,
              )
            }
            return
          }
          (Array(a), Array(b)) => {
            let length = if a.length() > b.length() {
              a.length()
            } else {
              b.length()
            }
            for i = 0; i < length; i = i + 1 {
              walk(a.get(i), b.get(i), "\{path}/\{i}", depth + 1, rules, output)
            }
            return
          }
          (Number(a, repr=ar), Number(b, repr=br)) =>
            if !supported_number(a, ar) || !supported_number(b, br) {
              UnsupportedNumber
            } else {
              let mut tolerance : Tolerance? = None
              for rule in rules.tolerances {
                if rule.path == path {
                  tolerance = Some(rule)
                }
              }
              match tolerance {
                None => if a == b { return } else { ValueMismatch }
                Some(rule) => {
                  // Divide by scale to avoid overflow in relative * scale.
                  let delta = (a - b).abs()
                  let scale = if a.abs() > b.abs() { a.abs() } else { b.abs() }
                  if delta <= rule.absolute ||
                    (scale > 0.0 && delta / scale <= rule.relative) {
                    return
                  }
                  OutsideTolerance
                }
              }
            }
          _ =>
            if kind(left) != kind(right) {
              TypeMismatch
            } else if left == right {
              return
            } else {
              ValueMismatch
            }
        }
      }
  }
  output.push({ path, reason, expected, actual, })
}

///|
/// Compare JSON with exact path rules. Empty output means a match.
/// Invalid rules raise an error even when the inputs are equal or ignored.
/// Results are sorted lexicographically by encoded JSON Pointer.
pub fn compare(
  expected : Json,
  actual : Json,
  rules? : Rules = Rules::new(),
) -> Array[Difference] raise InvalidRule {
  validate(rules)
  let output = []
  walk(Some(expected), Some(actual), "", 0, rules, output)
  output.sort_by(fn(a, b) { a.path.lexical_compare(b.path) })
  output
}

///|
fn render_value(value : Json, depth : Int) -> String {
  if depth > 128 {
    return ""
  }
  match value {
    Object(fields) => {
      let keys = fields.keys().to_array()
      keys.sort_by(fn(a, b) { a.lexical_compare(b) })
      let pairs = keys.map(fn(key) {
        let field = match fields.get(key) {
          Some(v) => v
          None => Json::null()
        }
        "\{Json::string(key).stringify()}:\{render_value(field, depth + 1)}"
      })
      "{" + pairs.join(",") + "}"
    }
    Array(values) =>
      "[" + values.map(fn(v) { render_value(v, depth + 1) }).join(",") + "]"
    Number(n, ..) if n.is_nan() => ""
    Number(n, ..) if n.is_inf() => ""
    _ => value.stringify()
  }
}

///|
fn render_optional(value : Json?) -> String {
  match value {
    None => ""
    Some(v) => render_value(v, 0)
  }
}

///|
/// Render paths as quoted pointers so root, empty keys and newlines are clear.
pub fn format_differences(items : Array[Difference]) -> String {
  if items.is_empty() {
    return "JSON matched"
  }
  items
  .map(fn(item) {
    let path = Json::string(item.path).stringify()
    "\{path}: \{item.reason.label()}; expected=\{render_optional(item.expected)}; actual=\{render_optional(item.actual)}"
  })
  .join("\n")
}

///|
fn Reason::label(self : Reason) -> String {
  match self {
    ValueMismatch => "ValueMismatch"
    TypeMismatch => "TypeMismatch"
    Missing => "Missing"
    Unexpected => "Unexpected"
    OutsideTolerance => "OutsideTolerance"
    UnsupportedNumber => "UnsupportedNumber"
    DepthLimit => "DepthLimit"
  }
}

///|
/// Raise Mismatch with a readable report; configuration errors remain distinct.
pub fn assert_matches(
  expected : Json,
  actual : Json,
  rules? : Rules = Rules::new(),
) -> Unit raise {
  let differences = compare(expected, actual, rules~)
  if !differences.is_empty() {
    raise Mismatch(format_differences(differences))
  }
}