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

///|
fn json_finding(finding : Finding) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"rule_id\":")
  out.write_string(json_quote(finding.rule_id))
  out.write_string(",\"severity\":")
  out.write_string(json_quote(severity_name(finding.severity)))
  out.write_string(",\"risk\":")
  out.write_string(json_quote(risk_name(finding.risk)))
  out.write_string(",\"message\":")
  out.write_string(json_quote(finding.message))
  out.write_string(",\"offset\":")
  out.write_string(finding.offset.to_string())
  out.write_string(",\"sequence\":")
  out.write_string(json_quote(finding.sequence))
  out.write_string(",\"decision\":")
  out.write_string(
    json_quote(
      match finding.decision {
        Keep => "keep"
        Drop => "drop"
        Mark => "mark"
      },
    ),
  )
  out.write_string("}")
  out.to_string()
}

///|
fn json_event(event : Event) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"kind\":")
  out.write_string(json_quote(event_kind_name(event.kind)))
  out.write_string(",\"offset\":")
  out.write_string(event.offset.to_string())
  out.write_string(",\"complete\":")
  out.write_string(json_bool(event.complete))
  out.write_string(",\"raw\":")
  out.write_string(json_quote(visible(event.raw)))
  out.write_string(",\"payload\":")
  out.write_string(json_quote(visible(event.payload)))
  match event.final_byte {
    Some(c) => {
      out.write_string(",\"final\":")
      out.write_string(json_quote(String::from_array([c])))
    }
    None => ()
  }
  out.write_string("}")
  out.to_string()
}

///|
fn json_stats(stats : Stats) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"input_chars\":\{stats.input_chars}")
  out.write_string(",\"text_chars\":\{stats.text_chars}")
  out.write_string(",\"control_events\":\{stats.control_events}")
  out.write_string(",\"info\":\{stats.info}")
  out.write_string(",\"warnings\":\{stats.warnings}")
  out.write_string(",\"dangerous\":\{stats.dangerous}")
  out.write_string(",\"critical\":\{stats.critical}")
  out.write_string(",\"dropped\":\{stats.dropped}")
  out.write_string(",\"marked\":\{stats.marked}")
  out.write_string("}")
  out.to_string()
}

///|
/// Deterministic JSON report with no dependency on a JSON serialization package.
pub fn report_json(
  result : AuditResult,
  include_events? : Bool = false,
) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"schema\":\"urn:termproof:report-v1\"")
  out.write_string(",\"failed\":")
  out.write_string(json_bool(result.failed))
  out.write_string(",\"plain_text\":")
  out.write_string(json_quote(result.plain_text))
  out.write_string(",\"stats\":")
  out.write_string(json_stats(result.stats))
  out.write_string(",\"findings\":[")
  let mut first = true
  for finding in result.findings {
    if !first {
      out.write_char(',')
    }
    first = false
    out.write_string(json_finding(finding))
  }
  out.write_string("]")
  if include_events {
    out.write_string(",\"events\":[")
    first = true
    for event in result.events {
      if !first {
        out.write_char(',')
      }
      first = false
      out.write_string(json_event(event))
    }
    out.write_string("]")
  }
  out.write_string("}")
  out.to_string()
}

///|
pub fn report_markdown(result : AuditResult) -> String {
  let out = StringBuilder::new()
  out.write_string("# TermProof audit report\n\n")
  out.write_string(
    if result.failed {
      "**Result: failed**\n\n"
    } else {
      "**Result: passed**\n\n"
    },
  )
  out.write_string("| Metric | Count |\n|---|---:|\n")
  out.write_string("| Input characters | \{result.stats.input_chars} |\n")
  out.write_string("| Control events | \{result.stats.control_events} |\n")
  out.write_string("| Warnings | \{result.stats.warnings} |\n")
  out.write_string("| Dangerous | \{result.stats.dangerous} |\n")
  out.write_string("| Critical | \{result.stats.critical} |\n\n")
  if result.findings.length() == 0 {
    out.write_string("No terminal-control findings.\n")
  } else {
    out.write_string("## Findings\n\n")
    out.write_string(
      "| Offset | Severity | Rule | Risk | Sequence | Description |\n",
    )
    out.write_string("|---:|---|---|---|---|---|\n")
    for finding in result.findings {
      out.write_string("| \{finding.offset} | ")
      out.write_string(severity_name(finding.severity))
      out.write_string(" | ")
      out.write_string(markdown_escape(finding.rule_id))
      out.write_string(" | ")
      out.write_string(markdown_escape(risk_name(finding.risk)))
      out.write_string(" | `")
      out.write_string(markdown_escape(finding.sequence))
      out.write_string("` | ")
      out.write_string(markdown_escape(finding.message))
      out.write_string(" |\n")
    }
  }
  out.to_string()
}

///|
pub fn report_github_annotations(
  result : AuditResult,
  file? : String = "terminal.log",
) -> String {
  let out = StringBuilder::new()
  for finding in result.findings {
    let command = match finding.severity {
      Info => "notice"
      Warning => "warning"
      Dangerous | Critical => "error"
    }
    out.write_string("::")
    out.write_string(command)
    out.write_string(" file=")
    out.write_string(github_property_escape(file))
    out.write_string(",title=")
    out.write_string(github_property_escape(finding.rule_id))
    out.write_string("::")
    out.write_string(
      github_message_escape(
        "\{finding.message} at character \{finding.offset}; sequence \{finding.sequence}",
      ),
    )
    out.write_char('\n')
  }
  out.to_string()
}

///|
pub fn summary_line(result : AuditResult) -> String {
  "TermProof: \{result.stats.control_events} controls, " +
  "\{result.stats.warnings} warning, \{result.stats.dangerous} dangerous, " +
  "\{result.stats.critical} critical; " +
  (if result.failed { "FAILED" } else { "PASSED" })
}