///|
fn value_json(value : Value) -> Json {
  match value {
    Inline(b) => {
      // Base64 is authoritative; text is only a convenience representation.
      let text : Json = @utf8.decode(b[:]).to_json() catch { _ => Json::null() }
      {
        "kind": "inline",
        "base64": @base64.encode(b[:]).to_json(),
        "text": text,
      }
    }
    External(url) => { "kind": "external", "url": url.to_json() }
  }
}

///|
fn attr_json(a : Attribute) -> Json {
  { "name": a.name.to_json(), "value": value_json(a.value) }
}

///|
fn record_json(r : Record) -> Json {
  let body : Json = match r.body {
    Entry(a) => { "type": "entry", "attributes": a.map(attr_json).to_json() }
    Add(a) => { "type": "add", "attributes": a.map(attr_json).to_json() }
    Delete => { "type": "delete" }
    Modify(m) =>
      {
        "type": "modify",
        "modifications": m
        .map(v => {
          "operation": v.operation.to_json(),
          "attribute": v.attribute.to_json(),
          "values": v.values.map(attr_json).to_json(),
        })
        .to_json(),
      }
    Rename(rdn, old, superior) =>
      {
        "type": "moddn",
        "newrdn": rdn.to_json(),
        "deleteoldrdn": old.to_json(),
        "newsuperior": match superior {
          Some(s) => s.to_json()
          None => Json::null()
        },
      }
  }
  let controls = r.controls.map(c => {
    "oid": c.oid.to_json(),
    "critical": c.critical.to_json(),
    "value": match c.value {
      Some(v) => value_json(v)
      None => Json::null()
    },
  })
  { "dn": r.dn.to_json(), "controls": controls.to_json(), "body": body }
}

///|
/// Semantic JSON omits source locations, but preserves record/operation/value
/// order and the distinction between absent and empty control values.
pub fn document_json(document : Document) -> Json {
  {
    "mode": document.mode.to_json(),
    "records": document.records.map(record_json).to_json(),
  }
}

///|
pub fn Report::to_json(
  self : Report,
  include_records? : Bool = false,
  include_review? : Bool = false,
) -> Json {
  let locations = if include_records {
    self.document.records.map(r => {
      let attrs : Array[Attribute] = match r.body {
        Entry(a) | Add(a) => a
        Modify(m) => m.iter().flat_map(v => v.values.iter()).to_array()
        _ => []
      }
      {
        "record": ToJson::to_json(r.span),
        "attributes": attrs
        .map(a => { "name": a.name.to_json(), "span": ToJson::to_json(a.span) })
        .to_json(),
      }
    })
  } else {
    []
  }
  {
    "tool": "MoonLDIF",
    "version": version().to_json(),
    "status": self.status().to_json(),
    "exit_code": self.exit_code().to_json(),
    "mode": self.document.mode.to_json(),
    "record_count": self.document.records.length().to_json(),
    "scope": if self.names_checked {
      "LDIF v1 supported structure and DN/RDN string syntax; not server import approval"
    } else {
      "LDIF v1 supported structural profile; not server import approval"
    },
    "names_checked": self.names_checked.to_json(),
    "name_profile": if !self.names_checked {
      "not_checked"
    } else if self.diagnostics.any(d => d.code == "legacy-name-separator-spaces") {
      "RFC 4514 with explicitly enabled legacy separator spaces"
    } else {
      "RFC 4514 string syntax"
    },
    "not_checked": self.not_checked().to_json(),
    "diagnostics": self.diagnostics.to_json(),
    "review": if include_review {
      self.review().to_json()
    } else {
      Json::null()
    },
    "document": if include_records {
      document_json(self.document)
    } else {
      Json::null()
    },
    "locations": if include_records {
      locations.to_json()
    } else {
      Json::null()
    },
  }
}

///|
pub fn Report::to_text(
  self : Report,
  include_records? : Bool = false,
) -> String {
  let out = StringBuilder()
  out.write_string("MoonLDIF " + version() + "\n")
  out.write_string(
    "Status: " +
    self.status() +
    "; exit=" +
    self.exit_code().to_string() +
    "; records=" +
    self.document.records.length().to_string() +
    "; mode=" +
    self.document.mode +
    "\n",
  )
  out.write_string(
    if self.names_checked {
      "Scope: supported LDIF structure and DN/RDN string syntax.\n"
    } else {
      "Scope: supported LDIF structure; DN/RDN syntax was not requested.\n"
    },
  )
  out.write_string("Not checked: " + self.not_checked().join(", ") + ".\n")
  for d in self.diagnostics {
    out.write_string(
      d.severity +
      " " +
      d.code +
      " line " +
      d.span.line.to_string() +
      "-" +
      d.span.end_line.to_string() +
      ": " +
      d.reason +
      "\n",
    )
  }
  if include_records {
    out.write_string(document_json(self.document).stringify(indent=2))
    out.write_string("\n")
  }
  out.to_string()
}

///|
pub fn Report::not_checked(self : Report) -> Array[String] {
  let items = [
    "DN equality and schema matching", "BER-hex assertion semantics", "directory schema",
    "server state and ACL", "control semantics", "external value contents",
  ]
  if !self.names_checked {
    items.push("DN/RDN string syntax")
  }
  items
}