// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// The four output formats of §12: `flag`, `basic`, `detailed` and `verbose`.
//
// A validation collects `OutputUnit`s as it walks: one per keyword, with the
// reasons that keyword had to give as that unit's children. Three of the four
// formats are then one reading of that tree — flattened, pruned, or whole — so
// that the four cannot disagree about what happened, only about how much of it
// they say.

///|
/// One result of one keyword: the output unit of §12.3.
///
/// `keyword_at` is the *evaluation path*, which is a JSON Pointer that goes
/// through `$ref`; `absolute_at` is the *schema location*, which is the
/// resource's own URI and a pointer into it and never goes through one. The two
/// differ exactly when a reference was crossed, and a reader that has to fetch
/// the keyword needs the second.
pub(all) struct OutputUnit {
  /// Whether this result is a pass.
  valid : Bool
  /// Where in the schema, following the walk that arrived.
  keyword_at : String
  /// The resource's canonical URI and a pointer into it, when it can be said.
  absolute_at : String?
  /// Where in the instance, as a JSON Pointer.
  at : String
  /// What went wrong, in one clause. A branch that only has children under it
  /// has nothing of its own to say.
  error : String?
  /// What the keyword contributed, when it produced an annotation.
  annotation : Json?
  /// The results this one encloses.
  children : Array[OutputUnit]
} derive(Eq, Debug)

///|
pub extend OutputUnit with Eq::{equal, not_equal}

///|
pub extend OutputUnit with Debug::{to_repr}

///|
/// What a validation came to, in the shape the caller asked for.
///
/// `units` is the whole of it: empty for `flag`, flat for `basic`, a pruned tree
/// for `detailed`, and the tree the walk produced for `verbose`. [`Report::to_json`]
/// writes it as §12.4's object, which is what a caller sends over a wire.
pub(all) struct Report {
  /// Whether the instance conformed.
  valid : Bool
  /// The results, nested as the format asks.
  units : Array[OutputUnit]
} derive(Eq, Debug)

///|
pub extend Report with Eq::{equal, not_equal}

///|
pub extend Report with Debug::{to_repr}

///|
/// What a keyword contributes when it passes, which is its own value for the
/// keywords that §10 calls annotations and nothing for the rest.
fn annotation_of(keyword : String, value : Json) -> Json? {
  match keyword {
    "title"
    | "description"
    | "default"
    | "deprecated"
    | "readOnly"
    | "writeOnly"
    | "examples"
    | "format"
    | "contentEncoding"
    | "contentMediaType"
    | "contentSchema" => Some(value)
    _ => None
  }
}

///|
/// Validate `instance` and answer in the format `output` names.
///
/// ```moonbit nocheck
/// let schema = @moonschema.Schema::new(
///   @json.parse("{ \"type\": \"integer\", \"minimum\": 1 }"),
/// )
/// let report = schema.report(@json.parse("0"), output=@moonschema.Output::Basic)
/// println(report.to_json().stringify(indent=2))
/// ```
pub fn Schema::report(
  self : Schema,
  instance : Json,
  output~ : Output = output,
) -> Report {
  let collected : Array[OutputUnit] = []
  self.scope.clear()
  self.scope.push(self.base)
  let valid = self.check(
    self.root,
    self.base,
    instance,
    "",
    "",
    Some(collected),
    Marks::new(),
  )
  let placed = order(self.place(collected))
  let units = match output {
    Flag => []
    Basic =>
      if valid {
        // A validation that passed has annotations and no errors, and the flat
        // format lists whichever it has.
        annotated(placed)
      } else {
        flat(prune(placed))
      }
    Detailed => prune(placed)
    Verbose => placed
  }
  { valid, units, }
}

///|
/// The units in the order a reader follows them: by evaluation path, and by
/// instance location where two share one.
///
/// A schema is a `Map` here, so the order a walk visits its keywords in is the
/// order the map keeps them, which is neither the document's nor anyone's. Two
/// runs of one validation should read the same, so the output is put in an order
/// that depends on nothing but the paths.
fn order(units : Array[OutputUnit]) -> Array[OutputUnit] {
  let sorted = units.map(fn(unit) {
    { ..unit, children: order(unit.children), }
  })
  // An insertion sort, because the result has to be the same on every backend
  // and a comparison of two pointers is all this is.
  for i = 1; i < sorted.length(); i = i + 1 {
    let held = sorted[i]
    let mut j = i
    while j > 0 && after(sorted[j - 1], held) {
      sorted[j] = sorted[j - 1]
      j = j - 1
    }
    sorted[j] = held
  }
  sorted
}

///|
/// Whether `left` belongs after `right` in the output.
fn after(left : OutputUnit, right : OutputUnit) -> Bool {
  let by_keyword = lexical(left.keyword_at, right.keyword_at)
  if by_keyword != 0 {
    by_keyword > 0
  } else {
    lexical(left.at, right.at) > 0
  }
}

///|
/// Two strings by the code point at a time, which is what an order of pointers
/// means: `String::compare` is the library's own and is not this order.
fn lexical(left : String, right : String) -> Int32 {
  let a = left[:]
  let b = right[:]
  let shorter = if a.length() < b.length() { a.length() } else { b.length() }
  let mut i = 0
  while i < shorter {
    let here = a[i].to_int()
    let there = b[i].to_int()
    if here != there {
      if here < there {
        return -1
      } else {
        return 1
      }
    }
    i = i + 1
  }
  if a.length() == b.length() {
    0
  } else if a.length() < b.length() {
    -1
  } else {
    1
  }
}

///|
/// Every result in the tree, in the order the walk produced them.
fn flat(units : Array[OutputUnit]) -> Array[OutputUnit] {
  let out : Array[OutputUnit] = []
  for unit in units {
    out.push({ ..unit, children: [], error: message_of(unit), })
    for child in flat(unit.children) {
      out.push(child)
    }
  }
  out
}

///|
/// What a flat entry says, which for a branch is that something under it failed.
fn message_of(unit : OutputUnit) -> String? {
  match unit.error {
    Some(message) => Some(message)
    None => if unit.valid { None } else { Some("a subschema had errors") }
  }
}

///|
/// What passed and said something: the annotations of a validation that held.
fn annotated(units : Array[OutputUnit]) -> Array[OutputUnit] {
  let out : Array[OutputUnit] = []
  for unit in units {
    for child in annotated(unit.children) {
      out.push(child)
    }
    if unit.annotation is Some(_) {
      out.push({ ..unit, children: [], })
    }
  }
  out
}

///|
/// The tree with everything that explains nothing taken out.
///
/// A pass explains nothing about a failure. A branch with one reason under it
/// is that reason, one level up: the hierarchy exists to show where several
/// reasons share a place, and a chain of single reasons shows nothing.
fn prune(units : Array[OutputUnit]) -> Array[OutputUnit] {
  let out : Array[OutputUnit] = []
  for unit in units {
    if unit.valid {
      continue
    }
    let children = prune(unit.children)
    if children.length() == 1 && unit.error is None {
      out.push(children[0])
      continue
    }
    if children.length() == 0 && unit.error is None {
      continue
    }
    out.push({ ..unit, error: message_of(unit), children, })
  }
  out
}

///|
/// Every unit with its schema location filled in.
///
/// The walk knows the evaluation path and not the resource it passes through, so
/// the location is worked out afterwards by walking the same path through the
/// document: a pointer step stays in the resource, and a `$ref` step moves to
/// the one it names.
fn Schema::place(self : Schema, units : Array[OutputUnit]) -> Array[OutputUnit] {
  let out : Array[OutputUnit] = []
  for unit in units {
    out.push({
      ..unit,
      absolute_at: self.absolute_of(unit.keyword_at),
      children: self.place(unit.children),
    })
  }
  out
}

///|
/// The schema location of the keyword at `path`, or `None` when the walk went
/// through an anchor and no pointer says where it landed.
fn Schema::absolute_of(self : Schema, path : String) -> String? {
  // A document that names no `$id` has no location to point at, and §12.3.2
  // lets the answer be nothing.
  if self.base == "" {
    return None
  }
  let text = path[:]
  let mut base = self.base
  let mut location = ""
  let mut node = self.root
  let mut i = 0
  while i < text.length() {
    let start = i
    let mut end = i + 1
    while end < text.length() && text[end] != '/' {
      end = end + 1
    }
    // Kept as it was written: a JSON Pointer escapes `~` and `/`, and a path
    // that has been through `step` is already escaped.
    let raw = text[start + 1:end].to_owned()
    let token = unescape(raw)
    let reference = if (token == "$ref" || token == "$dynamicRef") &&
      node is Object(members) {
      match members.get(token) {
        Some(String(found)) => Some(found)
        _ => None
      }
    } else {
      None
    }
    match reference {
      Some(found) => {
        let (document, fragment) = split_fragment(resolve(base, found))
        // An anchor names a place without a pointer, and no pointer can say
        // where it was.
        if !fragment.has_prefix("/") {
          return None
        }
        let resolved_base = if document == "" { base } else { document }
        let root = if resolved_base == self.base || resolved_base == "" {
          Some(self.root)
        } else {
          self.ids.get(resolved_base)
        }
        match root {
          Some(document_root) =>
            match pointer(document_root, fragment) {
              Some(target) => node = target
              None => return None
            }
          None => return None
        }
        base = resolved_base
        location = fragment
      }
      None => {
        location = location + "/" + raw
        node = match node {
          Object(members) =>
            match members.get(token) {
              Some(next) => next
              None => return None
            }
          Array(items) => {
            let index = index_of(token)
            if index < 0 || index >= items.length() {
              return None
            }
            items[index]
          }
          _ => return None
        }
      }
    }
    i = end
  }
  Some(base + "#" + location)
}

///|
/// The report as the object §12.4 describes.
pub impl ToJson for Report with fn to_json(self) {
  let out : Map[String, Json] = Map::Map([])
  out["valid"] = Json::boolean(self.valid)
  // Nested results are `errors` when the validation failed and `annotations`
  // when it held: the plural, because a keyword may have both a result of its
  // own and results under it.
  if self.units.length() > 0 {
    let key = if self.valid { "annotations" } else { "errors" }
    out[key] = Json::array(self.units.map(fn(unit) { ToJson::to_json(unit) }))
  }
  Json::object(out)
}

///|
pub impl ToJson for OutputUnit with fn to_json(self) {
  let out : Map[String, Json] = Map::Map([])
  out["valid"] = Json::boolean(self.valid)
  out["keywordLocation"] = Json::string(self.keyword_at)
  match self.absolute_at {
    Some(location) => out["absoluteKeywordLocation"] = Json::string(location)
    None => ()
  }
  out["instanceLocation"] = Json::string(self.at)
  match self.error {
    Some(message) => out["error"] = Json::string(message)
    None => ()
  }
  match self.annotation {
    Some(value) => out["annotation"] = value
    None => ()
  }
  if self.children.length() > 0 {
    let key = if self.valid { "annotations" } else { "errors" }
    out[key] = Json::array(
      self.children.map(fn(child) { ToJson::to_json(child) }),
    )
  }
  Json::object(out)
}

///|
pub extend Report with ToJson::{to_json}

///|
pub extend OutputUnit with ToJson::{to_json}