///|
/// Stable, machine-readable exports for reports and engineering results.
pub(all) struct KeyValue {
  key : String
  value : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ExportManifest {
  format : String
  title : String
  record_count : Int
  generated_by : String
  fields : Array[String]
} derive(Debug, Eq, ToJson)

///|
pub fn escape_json_text(value : String) -> String {
  value
  .replace(old="\\", new="\\\\")
  .replace(old="\"", new="\\\"")
  .replace(old="\n", new="\\n")
  .replace(old="\r", new="\\r")
}

///|
pub fn key_value_json(values : ArrayView[KeyValue]) -> String {
  let fields = []
  for value in values {
    fields.push(
      "\"" +
      escape_json_text(value.key) +
      "\":\"" +
      escape_json_text(value.value) +
      "\"",
    )
  }
  "{" + fields.join(",") + "}"
}

///|
pub fn manifest_json(manifest : ExportManifest) -> String {
  let fields = manifest.fields.map(field => {
    "\"" + escape_json_text(field) + "\""
  })
  "{\"format\":\"" +
  escape_json_text(manifest.format) +
  "\",\"title\":\"" +
  escape_json_text(manifest.title) +
  "\",\"record_count\":" +
  manifest.record_count.to_string() +
  ",\"generated_by\":\"" +
  escape_json_text(manifest.generated_by) +
  "\",\"fields\":[" +
  fields.join(",") +
  "]}"
}

///|
pub fn document_manifest(
  document : ReportDocument,
  format : String,
) -> ExportManifest {
  let fields = ["title", "subtitle", "blocks", "generated_by"]
  {
    format,
    title: document.title,
    record_count: document.blocks.length(),
    generated_by: document.generated_by,
    fields,
  }
}

///|
pub fn document_outline(document : ReportDocument) -> Array[String] {
  let outline = []
  for block in document.blocks {
    match block {
      Heading(level~, text~) => outline.push("#".repeat(level) + " " + text)
      Paragraph(text~) => outline.push(text)
      Bullet(items~) => outline.push(items.join("; "))
      Table(table~) => outline.push(table.title)
      Finding(finding~) => outline.push(finding.title)
      Metric(name~, value~, unit~) =>
        outline.push(name + "=" + value.to_string() + unit)
      Divider => outline.push("---")
    }
  }
  outline
}

///|
pub fn document_plain_text(document : ReportDocument) -> String {
  let lines = [document.title]
  if document.subtitle.length() > 0 {
    lines.push(document.subtitle)
  }
  for line in document_outline(document) {
    lines.push(line)
  }
  lines.join("\n")
}

///|
pub fn document_csv(document : ReportDocument) -> String {
  let lines = ["kind,name,value,unit,details"]
  for block in document.blocks {
    match block {
      Heading(level~, text~) =>
        lines.push("heading," + level.to_string() + ",," + csv_text(text))
      Paragraph(text~) => lines.push("paragraph,," + csv_text(text) + ",,")
      Bullet(items~) =>
        lines.push("bullet,," + csv_text(items.join("; ")) + ",,")
      Table(table~) => {
        lines.push(
          "table," +
          csv_text(table.title) +
          "," +
          table.row_count().to_string() +
          ",,",
        )
        for row in table.rows() {
          lines.push("table-row,," + csv_text(row.join(" | ")) + ",,")
        }
      }
      Finding(finding~) =>
        lines.push(
          "finding," +
          csv_text(finding.title) +
          "," +
          csv_text(finding.detail) +
          ",,",
        )
      Metric(name~, value~, unit~) =>
        lines.push(
          "metric," +
          csv_text(name) +
          "," +
          value.to_string() +
          "," +
          csv_text(unit) +
          ",",
        )
      Divider => lines.push("divider,,,,")
    }
  }
  lines.join("\n")
}

///|
fn csv_text(value : String) -> String {
  "\"" + value.replace(old="\"", new="\"\"") + "\""
}

///|
fn severity_text(severity : Severity) -> String {
  match severity {
    Pass => "pass"
    Notice => "notice"
    Fail => "fail"
  }
}

///|
pub fn document_json_lines(document : ReportDocument) -> String {
  let lines = []
  for block in document.blocks {
    let record = match block {
      Heading(level~, text~) =>
        "{\"kind\":\"heading\",\"level\":" +
        level.to_string() +
        ",\"text\":\"" +
        escape_json_text(text) +
        "\"}"
      Paragraph(text~) =>
        "{\"kind\":\"paragraph\",\"text\":\"" + escape_json_text(text) + "\"}"
      Bullet(items~) =>
        "{\"kind\":\"bullet\",\"items\":[\"" +
        items.map(escape_json_text).join("\",\"") +
        "\"]}"
      Table(table~) =>
        "{\"kind\":\"table\",\"title\":\"" +
        escape_json_text(table.title) +
        "\",\"rows\":" +
        table.row_count().to_string() +
        "}"
      Finding(finding~) =>
        "{\"kind\":\"finding\",\"title\":\"" +
        escape_json_text(finding.title) +
        "\",\"severity\":\"" +
        severity_text(finding.severity) +
        "\"}"
      Metric(name~, value~, unit~) =>
        "{\"kind\":\"metric\",\"name\":\"" +
        escape_json_text(name) +
        "\",\"value\":" +
        value.to_string() +
        ",\"unit\":\"" +
        escape_json_text(unit) +
        "\"}"
      Divider => "{\"kind\":\"divider\"}"
    }
    lines.push(record)
  }
  lines.join("\n")
}

///|
pub fn table_to_tsv(table : ReportTable) -> String {
  let lines = [table.columns.join("\t")]
  for row in table.rows() {
    lines.push(row.map(value => value.replace(old="\t", new=" ")).join("\t"))
  }
  lines.join("\n")
}

///|
pub fn table_to_html(table : ReportTable) -> String {
  let lines = [
    "",
    "",
    "",
  ]
  for column in table.columns {
    lines.push("")
  }
  lines.push("")
  for row in table.rows() {
    let cells = row.map(value => "")
    lines.push("" + cells.join("") + "")
  }
  lines.push("
" + table.title + "
" + column + "
" + value + "
") lines.join("\n") } ///| pub fn document_html(document : ReportDocument) -> String { let lines = [ "", "" + document.title + "", ] lines.push("

" + document.title + "

") if document.subtitle.length() > 0 { lines.push("

" + document.subtitle + "

") } for block in document.blocks { match block { Heading(level~, text~) => lines.push( "" + text + "", ) Paragraph(text~) => lines.push("

" + text + "

") Bullet(items~) => lines.push("") Table(table~) => lines.push(table_to_html(table)) Finding(finding~) => lines.push( "
" + finding.title + "

" + finding.detail + "

" + severity_text(finding.severity) + "
", ) Metric(name~, value~, unit~) => lines.push( "
" + name + "" + value.to_string() + "" + unit + "
", ) Divider => lines.push("
") } } lines.push("") lines.join("\n") } ///| pub fn export_checksum(text : String) -> String { let mut first = 216613626 for index in 0.. String { manifest.format + ": " + manifest.title + ", " + manifest.record_count.to_string() + " records" } ///| pub fn report_export_formats() -> Array[String] { ["markdown", "csv", "tsv", "jsonl", "html"] } ///| pub fn report_has_format(format : String) -> Bool { for available in report_export_formats() { if available == format { return true } } false } ///| pub fn report_export( document : ReportDocument, format : String, ) -> String raise @domain.ExtractionError { match format { "markdown" => document.to_markdown() "csv" => document_csv(document) "jsonl" => document_json_lines(document) "html" => document_html(document) _ => raise @domain.ExtractionError::InvalidArgument( message="unsupported report format", ) } }