///|
/// Host-computed identity of the exact analysed bytes, not a signature.
/// Callers must supply the options used to create the report.
pub(all) struct ReviewMetadata {
  input_sha256 : String
  input_byte_length : Int
  options : Options
  legacy_dn_spaces : Bool
  risk_policy : RiskPolicy
} derive(Eq)

///|
fn ReviewMetadata::json(self : ReviewMetadata) -> Json {
  {
    "allow_missing_version": self.options.allow_missing_version.to_json(),
    "deny_delete": self.options.deny_delete.to_json(),
    "deny_clear": self.risk_policy.deny_clear.to_json(),
    "deny_rename": self.risk_policy.deny_rename.to_json(),
    "legacy_dn_spaces": self.legacy_dn_spaces.to_json(),
  }
}

///|
/// Additive fields preserve the existing report JSON shape. Attribute bytes,
/// complete input text, filenames and host paths are intentionally omitted.
pub fn Report::review_json(self : Report, metadata : ReviewMetadata) -> Json {
  let result = self.to_json(include_review=true)
  if result is Object(fields) {
    fields["report_schema_version"] = (1).to_json()
    fields["source"] = {
      "sha256": metadata.input_sha256.to_json(),
      "byte_length": metadata.input_byte_length.to_json(),
      "identity_scope": "Exact analysed bytes; not a signature or authenticity proof",
    }
    fields["options"] = metadata.json()
    fields["diagnostics_truncated"] = self.diagnostics
      .any(d => d.code == "diagnostic-limit")
      .to_json()
  }
  result
}

///|
fn markdown_escape(text : String) -> String {
  let out = StringBuilder()
  for c in text.iter() {
    if c.to_int() < 32 || "&<>\\`*_{}[]()#+-.!|~=\"".contains(c.to_string()) {
      out.write_string("&#" + c.to_int().to_string() + ";")
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
pub fn Report::review_markdown(
  self : Report,
  metadata : ReviewMetadata,
) -> String {
  let review = self.review()
  let out = StringBuilder()
  out.write_string("# MoonLDIF operation review\n\n")
  out.write_string("Tool version: " + markdown_escape(version()) + "\n\n")
  out.write_string(
    "Input SHA-256: " + markdown_escape(metadata.input_sha256) + "\n\n",
  )
  out.write_string(
    "Input bytes: " + metadata.input_byte_length.to_string() + "\n\n",
  )
  out.write_string(
    "Status: " +
    self.status() +
    "; exit: " +
    self.exit_code().to_string() +
    "; records: " +
    self.document.records.length().to_string() +
    "\n\n",
  )
  out.write_string(
    "Options: " + markdown_escape(metadata.json().stringify()) + "\n\n",
  )
  out.write_string(
    "File intent only, not server import approval. Hash identifies analysed bytes, not authenticity. This report contains target DNs; do not share it as anonymised data.\n\n",
  )
  out.write_string(
    "Not checked: " + markdown_escape(self.not_checked().join(", ")) + "\n\n",
  )
  out.write_string(
    "Review items: " +
    review.total_items.to_string() +
    "; retained: " +
    review.items.length().to_string() +
    "; truncated: " +
    review.truncated.to_string() +
    "\n\n",
  )
  out.write_string(
    "Diagnostics truncated: " +
    self.diagnostics.any(d => d.code == "diagnostic-limit").to_string() +
    "\n\n",
  )
  if self.exit_code() == 2 {
    out.write_string(
      "**Partial or invalid analysis. Missing findings are not proof of no impact.**\n\n",
    )
  }
  out.write_string("## Diagnostics\n\n")
  if self.diagnostics.is_empty() {
    out.write_string("No diagnostics in the supported checks.\n\n")
  }
  for d in self.diagnostics {
    out.write_string(
      "- " +
      markdown_escape(d.code) +
      " / " +
      markdown_escape(d.severity) +
      " / lines " +
      d.span.line.to_string() +
      "–" +
      d.span.end_line.to_string() +
      ": " +
      markdown_escape(d.reason) +
      "\n",
    )
  }
  out.write_string("\n## Operations\n\n")
  for item in review.items {
    out.write_string(
      "### " +
      (item.record_index + 1).to_string() +
      ". " +
      markdown_escape(item.title) +
      "\n\n",
    )
    out.write_string(
      "- Code: " +
      markdown_escape(item.code) +
      "; level: " +
      markdown_escape(item.level) +
      "\n",
    )
    out.write_string("- Target DN: " + markdown_escape(item.dn) + "\n")
    out.write_string(
      "- Lines: " +
      item.span.line.to_string() +
      "–" +
      item.span.end_line.to_string() +
      "\n",
    )
    match item.attribute {
      Some(attribute) =>
        out.write_string(
          "- Attribute: " +
          markdown_escape(attribute) +
          "; supplied values: " +
          item.value_count.to_string() +
          "\n",
        )
      None => ()
    }
    out.write_string(
      "- Reason: " +
      markdown_escape(item.reason) +
      "\n- Review action: " +
      markdown_escape(item.action) +
      "\n\n",
    )
  }
  out.to_string()
}