///|
/// Report exporters are intentionally text-first. They work in WASM, command
/// line tools, and CI logs without needing filesystem or terminal extensions.

///|
pub(all) struct ReportOptions {
  include_manifest : Bool
  include_diagnostics : Bool
  include_suggestions : Bool
  include_unchanged_diff : Bool
  max_items : Int
} derive(Eq, Debug)

///|
pub fn default_report_options() -> ReportOptions {
  {
    include_manifest: true,
    include_diagnostics: true,
    include_suggestions: true,
    include_unchanged_diff: false,
    max_items: 50,
  }
}

///|
pub fn compact_report_options() -> ReportOptions {
  {
    include_manifest: false,
    include_diagnostics: true,
    include_suggestions: false,
    include_unchanged_diff: false,
    max_items: 20,
  }
}

///|
pub fn diagnostic_severity_name(severity : Severity) -> String {
  match severity {
    Warning => "warning"
    Error => "error"
  }
}

///|
pub fn diagnostic_to_markdown(diagnostic : Diagnostic) -> String {
  "- " +
  diagnostic_severity_name(diagnostic.severity) +
  " `" +
  diagnostic.code +
  "` line " +
  diagnostic.line.to_string() +
  ": " +
  diagnostic.message
}

///|
pub fn diagnostics_to_markdown(diagnostics : Array[Diagnostic]) -> String {
  if diagnostics.length() == 0 {
    return "- no diagnostics"
  }
  let lines : Array[String] = []
  for diagnostic in diagnostics {
    lines.push(diagnostic_to_markdown(diagnostic))
  }
  lines.join("\n")
}

///|
pub fn document_report_markdown(output : ParseOutput) -> String {
  let doc = output.document
  let buf = StringBuilder(size_hint=256)
  buf.write_string("## MoonFolio Document\n\n")
  buf.write_string("- format: `")
  buf.write_string(format_name(doc.format))
  buf.write_string("`\n")
  buf.write_string("- fields: ")
  buf.write_string(doc.fields.length().to_string())
  buf.write_string("\n")
  buf.write_string("- metadata lines: ")
  buf.write_string(document_metadata_line_count(doc).to_string())
  buf.write_string("\n")
  buf.write_string("- diagnostics: ")
  buf.write_string(output.diagnostics.length().to_string())
  buf.write_string("\n")
  if doc.order.length() > 0 {
    buf.write_string("\n### Fields\n\n")
    for key in doc.order {
      match doc.fields.get(key) {
        Some(value) => {
          buf.write_string("- `")
          buf.write_string(key)
          buf.write_string("`: ")
          buf.write_string(meta_value_display(value))
          buf.write_string("\n")
        }
        None => ()
      }
    }
  }
  buf.write_string("\n### Diagnostics\n\n")
  buf.write_string(diagnostics_to_markdown(output.diagnostics))
  buf.to_string()
}

///|
pub fn record_report_markdown(record : FolioRecord) -> String {
  let buf = StringBuilder(size_hint=256)
  buf.write_string("### ")
  buf.write_string(record.path)
  buf.write_string("\n\n")
  buf.write_string("- title: ")
  buf.write_string(record.folio.title)
  buf.write_string("\n")
  buf.write_string("- slug: `")
  buf.write_string(record.folio.slug)
  buf.write_string("`\n")
  buf.write_string("- permalink: `")
  buf.write_string(record.folio.permalink)
  buf.write_string("`\n")
  buf.write_string("- fingerprint: `")
  buf.write_string(record.folio.fingerprint)
  buf.write_string("`\n")
  buf.write_string("- status: ")
  buf.write_string(record_status(record))
  buf.write_string("\n")
  buf.write_string("- labels: ")
  buf.write_string(record_label_line(record))
  buf.write_string("\n")
  if record.diagnostics.length() > 0 {
    buf.write_string("\n")
    buf.write_string(diagnostics_to_markdown(record.diagnostics))
    buf.write_string("\n")
  }
  buf.to_string()
}

///|
pub fn registry_report_markdown(
  registry : FolioRegistry,
  options : ReportOptions,
) -> String {
  let summary = registry_summary(registry)
  let buf = StringBuilder(size_hint=512)
  buf.write_string("## MoonFolio Registry\n\n")
  buf.write_string("- total: ")
  buf.write_string(summary.total.to_string())
  buf.write_string("\n")
  buf.write_string("- valid: ")
  buf.write_string(summary.valid.to_string())
  buf.write_string("\n")
  buf.write_string("- invalid: ")
  buf.write_string(summary.invalid.to_string())
  buf.write_string("\n")
  buf.write_string("- warnings: ")
  buf.write_string(summary.warnings.to_string())
  buf.write_string("\n")
  buf.write_string("- drafts: ")
  buf.write_string(summary.drafts.to_string())
  buf.write_string("\n")
  buf.write_string("- health score: ")
  buf.write_string(summary.health_score.to_string())
  buf.write_string("\n")
  write_format_counts(buf, summary.formats)
  write_tag_counts(buf, summary.tags, options.max_items)
  if summary.duplicate_slugs.length() > 0 {
    write_duplicate_section(buf, "Duplicate Slugs", summary.duplicate_slugs)
  }
  if summary.duplicate_fingerprints.length() > 0 {
    write_duplicate_section(
      buf,
      "Duplicate Fingerprints",
      summary.duplicate_fingerprints,
    )
  }
  buf.write_string("\n### Records\n\n")
  let records = registry_sort_by_path(registry).records
  let limit = report_limit(records.length(), options.max_items)
  for i in 0.. limit {
    buf.write_string("- ")
    buf.write_string((records.length() - limit).to_string())
    buf.write_string(" records omitted by report options\n")
  }
  if options.include_manifest {
    buf.write_string("\n### Manifest\n\n```text\n")
    buf.write_string(registry_manifest_sorted(registry))
    buf.write_string("\n```\n")
  }
  buf.to_string()
}

///|
pub fn profile_report_markdown(report : ProfileReport) -> String {
  let buf = StringBuilder(size_hint=256)
  buf.write_string("## MoonFolio Profile\n\n")
  buf.write_string("- ok: ")
  buf.write_string(report.ok.to_string())
  buf.write_string("\n")
  buf.write_string("- score: ")
  buf.write_string(report.score.to_string())
  buf.write_string("\n")
  buf.write_string("- checked: ")
  buf.write_string(report.checked.length().to_string())
  buf.write_string("\n")
  buf.write_string("- errors: ")
  buf.write_string(count_error_findings(report.findings).to_string())
  buf.write_string("\n")
  buf.write_string("- warnings: ")
  buf.write_string(count_warning_findings(report.findings).to_string())
  buf.write_string("\n")
  if report.findings.length() > 0 {
    buf.write_string("\n### Findings\n\n")
    for finding in report.findings {
      buf.write_string("- ")
      buf.write_string(diagnostic_severity_name(finding.severity))
      buf.write_string(" `")
      buf.write_string(profile_kind_code(finding.kind))
      buf.write_string("`: ")
      buf.write_string(finding.message)
      buf.write_string("\n")
    }
  }
  if report.suggestions.length() > 0 {
    buf.write_string("\n### Suggestions\n\n")
    for suggestion in report.suggestions {
      buf.write_string("- ")
      buf.write_string(suggestion)
      buf.write_string("\n")
    }
  }
  buf.to_string()
}

///|
pub fn diff_report_markdown(
  report : DiffReport,
  options : ReportOptions,
) -> String {
  let selected = if options.include_unchanged_diff {
    report
  } else {
    snapshot_without_unchanged(report)
  }
  let buf = StringBuilder(size_hint=256)
  buf.write_string("## MoonFolio Diff\n\n")
  buf.write_string("- ")
  buf.write_string(diff_summary_line(report))
  buf.write_string("\n")
  buf.write_string("- release safe: ")
  buf.write_string(diff_is_release_safe(report).to_string())
  buf.write_string("\n")
  if selected.changes.length() > 0 {
    buf.write_string("\n### Changes\n\n")
    let limit = report_limit(selected.changes.length(), options.max_items)
    for i in 0.. `")
        buf.write_string(change.after_fingerprint)
        buf.write_string("`")
      }
      buf.write_string("\n")
    }
  }
  buf.to_string()
}

///|
pub fn document_report_json(output : ParseOutput) -> String {
  let doc = output.document
  let buf = StringBuilder(size_hint=256)
  buf.write_string("{")
  json_pair_string(buf, "format", format_name(doc.format), first=true)
  json_pair_int(buf, "fields", doc.fields.length(), first=false)
  json_pair_int(buf, "diagnostics", output.diagnostics.length(), first=false)
  buf.write_string(",\"keys\":")
  json_string_array(buf, doc.order)
  buf.write_string(",\"diagnostic_items\":")
  diagnostics_json_array(buf, output.diagnostics)
  buf.write_string("}")
  buf.to_string()
}

///|
pub fn registry_report_json(registry : FolioRegistry) -> String {
  let summary = registry_summary(registry)
  let buf = StringBuilder(size_hint=512)
  buf.write_string("{")
  json_pair_int(buf, "total", summary.total, first=true)
  json_pair_int(buf, "valid", summary.valid, first=false)
  json_pair_int(buf, "invalid", summary.invalid, first=false)
  json_pair_int(buf, "errors", summary.errors, first=false)
  json_pair_int(buf, "warnings", summary.warnings, first=false)
  json_pair_int(buf, "drafts", summary.drafts, first=false)
  json_pair_int(buf, "health_score", summary.health_score, first=false)
  buf.write_string(",\"paths\":")
  json_string_array(buf, registry_paths(registry))
  buf.write_string(",\"slugs\":")
  json_string_array(buf, registry_slugs(registry))
  buf.write_string(",\"tags\":")
  json_string_array(buf, registry_unique_tags(registry))
  buf.write_string("}")
  buf.to_string()
}

///|
pub fn profile_report_json(report : ProfileReport) -> String {
  let buf = StringBuilder(size_hint=256)
  buf.write_string("{")
  json_pair_bool(buf, "ok", report.ok, first=true)
  json_pair_int(buf, "score", report.score, first=false)
  json_pair_int(buf, "findings", report.findings.length(), first=false)
  json_pair_int(
    buf,
    "errors",
    count_error_findings(report.findings),
    first=false,
  )
  json_pair_int(
    buf,
    "warnings",
    count_warning_findings(report.findings),
    first=false,
  )
  buf.write_string(",\"suggestions\":")
  json_string_array(buf, report.suggestions)
  buf.write_string("}")
  buf.to_string()
}

///|
pub fn diff_report_json(report : DiffReport) -> String {
  let buf = StringBuilder(size_hint=256)
  buf.write_string("{")
  json_pair_bool(buf, "has_changes", report.has_changes, first=true)
  json_pair_int(buf, "added", report.added, first=false)
  json_pair_int(buf, "removed", report.removed, first=false)
  json_pair_int(buf, "changed", report.changed, first=false)
  json_pair_int(buf, "renamed", report.renamed, first=false)
  json_pair_int(buf, "unchanged", report.unchanged, first=false)
  buf.write_string(",\"paths\":")
  json_string_array(buf, diff_paths(report))
  buf.write_string("}")
  buf.to_string()
}

///|
pub fn meta_value_display(value : MetaValue) -> String {
  match value {
    Text(text) => text
    Bool(flag) => flag.to_string()
    Int(number) => number.to_string()
    List(items) => "[" + items.join(", ") + "]"
  }
}

///|
pub fn record_status(record : FolioRecord) -> String {
  if record_has_errors(record) {
    "error"
  } else if record_has_warnings(record) {
    "warning"
  } else if record_is_draft(record) {
    "draft"
  } else {
    "ok"
  }
}

///|
fn write_format_counts(
  buf : StringBuilder,
  counts : Array[FormatCount],
) -> Unit {
  if counts.length() == 0 {
    return
  }
  buf.write_string("\n### Formats\n\n")
  for item in counts {
    buf.write_string("- `")
    buf.write_string(format_name(item.format))
    buf.write_string("`: ")
    buf.write_string(item.count.to_string())
    buf.write_string("\n")
  }
}

///|
fn write_tag_counts(
  buf : StringBuilder,
  counts : Array[TagCount],
  max_items : Int,
) -> Unit {
  if counts.length() == 0 {
    return
  }
  buf.write_string("\n### Tags\n\n")
  let limit = report_limit(counts.length(), max_items)
  for i in 0.. Unit {
  buf.write_string("\n### ")
  buf.write_string(title)
  buf.write_string("\n\n")
  for duplicate in duplicates {
    buf.write_string("- `")
    buf.write_string(duplicate.value)
    buf.write_string("`: ")
    buf.write_string(duplicate.paths.join(", "))
    buf.write_string("\n")
  }
}

///|
fn report_limit(length : Int, max_items : Int) -> Int {
  if max_items <= 0 || max_items > length {
    length
  } else {
    max_items
  }
}

///|
fn json_pair_string(
  buf : StringBuilder,
  key : String,
  value : String,
  first~ : Bool,
) -> Unit {
  if !first {
    buf.write_string(",")
  }
  buf.write_string("\"")
  buf.write_string(escape_string(key))
  buf.write_string("\":\"")
  buf.write_string(escape_string(value))
  buf.write_string("\"")
}

///|
fn json_pair_int(
  buf : StringBuilder,
  key : String,
  value : Int,
  first~ : Bool,
) -> Unit {
  if !first {
    buf.write_string(",")
  }
  buf.write_string("\"")
  buf.write_string(escape_string(key))
  buf.write_string("\":")
  buf.write_string(value.to_string())
}

///|
fn json_pair_bool(
  buf : StringBuilder,
  key : String,
  value : Bool,
  first~ : Bool,
) -> Unit {
  if !first {
    buf.write_string(",")
  }
  buf.write_string("\"")
  buf.write_string(escape_string(key))
  buf.write_string("\":")
  buf.write_string(value.to_string())
}

///|
fn json_string_array(buf : StringBuilder, values : Array[String]) -> Unit {
  buf.write_string("[")
  for i in 0.. 0 {
      buf.write_string(",")
    }
    buf.write_string("\"")
    buf.write_string(escape_string(values[i]))
    buf.write_string("\"")
  }
  buf.write_string("]")
}

///|
fn diagnostics_json_array(
  buf : StringBuilder,
  diagnostics : Array[Diagnostic],
) -> Unit {
  buf.write_string("[")
  for i in 0.. 0 {
      buf.write_string(",")
    }
    let diagnostic = diagnostics[i]
    buf.write_string("{")
    json_pair_string(
      buf,
      "severity",
      diagnostic_severity_name(diagnostic.severity),
      first=true,
    )
    json_pair_string(buf, "code", diagnostic.code, first=false)
    json_pair_int(buf, "line", diagnostic.line, first=false)
    json_pair_string(buf, "message", diagnostic.message, first=false)
    buf.write_string("}")
  }
  buf.write_string("]")
}