///|
fn json_hex_digit(n : Int) -> Char {
  "0123456789abcdef".to_array()[n]
}

///|
fn json_escape(value : String) -> String {
  let out = StringBuilder()
  out.write_char('"')
  for c in value {
    match c {
      '"' => out.write_string("\\\"")
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ => {
        let n = c.to_int()
        if n < 32 {
          out.write_string("\\u00")
          out.write_char(json_hex_digit(n / 16))
          out.write_char(json_hex_digit(n % 16))
        } else {
          out.write_char(c)
        }
      }
    }
  }
  out.write_char('"')
  out.to_string()
}

///|
fn json_bool(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn decision_name(kind : DecisionKind) -> String {
  match kind {
    Allowed => "allowed"
    Denied => "denied"
    Unknown => "unknown"
  }
}

///|
fn write_json_strings(out : StringBuilder, values : Array[String]) -> Unit {
  out.write_char('[')
  for index, value in values {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string(json_escape(value))
  }
  out.write_char(']')
}

///|
fn write_json_request(out : StringBuilder, request : AccessRequest) -> Unit {
  out.write_string("{\"principal\":")
  out.write_string(json_escape(request.principal_id))
  out.write_string(",\"action\":")
  out.write_string(json_escape(request.action))
  out.write_string(",\"resource\":")
  out.write_string(json_escape(request.resource_id))
  out.write_string(",\"attributes\":{")
  for index, pair in request.attributes {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string(json_escape(pair.0))
    out.write_char(':')
    out.write_string(json_escape(pair.1))
  }
  out.write_string("}}")
}

///|
fn write_json_decision(out : StringBuilder, decision : Decision) -> Unit {
  out.write_string("{\"kind\":")
  out.write_string(json_escape(decision_name(decision.kind)))
  out.write_string(",\"explanation\":")
  out.write_string(json_escape(decision.explanation))
  out.write_string(",\"decisive_rules\":")
  write_json_strings(out, decision.decisive_rules)
  out.write_string(",\"traces\":[")
  for index, trace in decision.traces {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("{\"rule\":")
    out.write_string(json_escape(trace.rule_id))
    out.write_string(",\"effect\":")
    out.write_string(
      json_escape(
        match trace.effect {
          Permit => "permit"
          Deny => "deny"
        },
      ),
    )
    out.write_string(",\"matched\":")
    out.write_string(json_bool(trace.matched))
    out.write_string(",\"reason\":")
    out.write_string(json_escape(trace.reason))
    out.write_char('}')
  }
  out.write_string("]}")
}

///|
pub fn render_report_json(report : AuditReport, gate : GateResult) -> String {
  let out = StringBuilder()
  out.write_string("{\"schema_version\":1,\"before\":")
  out.write_string(json_escape(report.before_name))
  out.write_string(",\"after\":")
  out.write_string(json_escape(report.after_name))
  out.write_string(",\"complete\":")
  out.write_string(json_bool(report.complete))
  out.write_string(",\"requests_considered\":\{report.requests_considered}")
  out.write_string(",\"gate\":")
  out.write_string(json_escape(verdict_name(gate.verdict)))
  out.write_string(",\"new_grants\":\{count_changes(report, NewGrant)}")
  out.write_string(",\"revocations\":\{count_changes(report, RevokedGrant)}")
  out.write_string(",\"diagnostics\":[")
  for index, issue in report.diagnostics {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("{\"code\":")
    out.write_string(json_escape(issue.code))
    out.write_string(",\"severity\":")
    out.write_string(
      json_escape(
        match issue.severity {
          Info => "info"
          Warning => "warning"
          Error => "error"
        },
      ),
    )
    out.write_string(",\"message\":")
    out.write_string(json_escape(issue.message))
    out.write_char('}')
  }
  out.write_string("],\"changes\":[")
  for index, change in report.changes {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("{\"kind\":")
    out.write_string(json_escape(change_name(change.kind)))
    out.write_string(",\"request\":")
    write_json_request(out, change.request)
    out.write_string(",\"before\":")
    write_json_decision(out, change.before)
    out.write_string(",\"after\":")
    write_json_decision(out, change.after)
    out.write_char('}')
  }
  out.write_string("],\"findings\":[")
  for index, finding in gate.findings {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("{\"code\":")
    out.write_string(json_escape(finding.code))
    out.write_string(",\"message\":")
    out.write_string(json_escape(finding.message))
    out.write_string(",\"request\":")
    write_json_request(out, finding.request)
    out.write_char('}')
  }
  out.write_string("]}")
  out.to_string()
}