///|
/// A typed table used in engineering reports.
pub(all) struct ReportTable {
  title : String
  columns : Array[String]
  rows : Array[Array[String]]
  numeric_columns : Array[Bool]
} derive(Debug, Eq, ToJson)

///|
pub fn ReportTable::new(
  title~ : String,
  columns~ : ArrayView[String],
  numeric_columns? : ArrayView[Bool] = [],
) -> ReportTable raise @domain.ExtractionError {
  if title.trim().length() == 0 || columns.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="report table title and columns are required",
    )
  }
  if numeric_columns.length() != 0 &&
    numeric_columns.length() != columns.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="numeric-column flags do not match columns",
    )
  }
  let numeric = if numeric_columns.length() == 0 {
    [
      for _ in columns => false
    ]
  } else {
    numeric_columns.to_owned()
  }
  { title, columns: columns.to_owned(), rows: [], numeric_columns: numeric }
}

///|
pub fn ReportTable::add_row(
  self : ReportTable,
  row : ArrayView[String],
) -> ReportTable raise @domain.ExtractionError {
  if row.length() != self.columns.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="report row width does not match columns",
    )
  }
  let rows = self.rows.copy()
  rows.push(row.to_owned())
  {
    title: self.title,
    columns: self.columns,
    rows,
    numeric_columns: self.numeric_columns,
  }
}

///|
pub fn ReportTable::row_count(self : ReportTable) -> Int {
  self.rows.length()
}

///|
pub fn ReportTable::column_count(self : ReportTable) -> Int {
  self.columns.length()
}

///|
pub fn ReportTable::rows(self : ReportTable) -> Array[Array[String]] {
  [
    for row in self.rows => row.copy()
  ]
}

///|
pub fn ReportTable::to_markdown(self : ReportTable) -> String {
  let lines = [
    "### {self.title}",
    "",
    "| " + self.columns.join(" | ") + " |",
    "|" +
    [
      for numeric in self.numeric_columns => {
        if numeric {
          "---:"
        } else {
          "---"
        }
      }
    ].join("|") +
    "|",
  ]
  for row in self.rows {
    lines.push("| " + row.join(" | ") + " |")
  }
  lines.join("\n")
}

///|
fn csv_escape(value : String) -> String {
  if value.contains(",") || value.contains("\n") || value.contains("\"") {
    "\"" + value.replace(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
pub fn ReportTable::to_csv(self : ReportTable) -> String {
  let lines = [self.columns.map(csv_escape).join(",")]
  for row in self.rows {
    lines.push(row.map(csv_escape).join(","))
  }
  lines.join("\n")
}

///|
pub(all) enum ReportBlock {
  Heading(level~ : Int, text~ : String)
  Paragraph(text~ : String)
  Bullet(items~ : Array[String])
  Table(table~ : ReportTable)
  Finding(finding~ : Finding)
  Metric(name~ : String, value~ : Double, unit~ : String)
  Divider
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ReportDocument {
  title : String
  subtitle : String
  blocks : Array[ReportBlock]
  generated_by : String
} derive(Debug, Eq, ToJson)

///|
pub fn ReportDocument::new(
  title~ : String,
  subtitle? : String = "",
  generated_by? : String = "moonbit-extractor",
) -> ReportDocument raise @domain.ExtractionError {
  if title.trim().length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="report title cannot be blank",
    )
  }
  { title, subtitle, blocks: [], generated_by }
}

///|
pub fn ReportDocument::add(
  self : ReportDocument,
  block : ReportBlock,
) -> ReportDocument {
  let blocks = self.blocks.copy()
  blocks.push(block)
  {
    title: self.title,
    subtitle: self.subtitle,
    blocks,
    generated_by: self.generated_by,
  }
}

///|
pub fn ReportDocument::heading(
  self : ReportDocument,
  level~ : Int,
  text~ : String,
) -> ReportDocument raise @domain.ExtractionError {
  if level < 1 || level > 6 || text.trim().length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="report heading level or text is invalid",
    )
  }
  self.add(Heading(level~, text~))
}

///|
pub fn ReportDocument::paragraph(
  self : ReportDocument,
  text~ : String,
) -> ReportDocument {
  self.add(Paragraph(text~))
}

///|
pub fn ReportDocument::bullets(
  self : ReportDocument,
  items~ : Array[String],
) -> ReportDocument {
  self.add(Bullet(items~))
}

///|
pub fn ReportDocument::table(
  self : ReportDocument,
  table~ : ReportTable,
) -> ReportDocument {
  self.add(Table(table~))
}

///|
pub fn ReportDocument::metric(
  self : ReportDocument,
  name~ : String,
  value~ : Double,
  unit~ : String,
) -> ReportDocument raise @domain.ExtractionError {
  @domain.require_finite("report metric", value)
  self.add(Metric(name~, value~, unit~))
}

///|
pub fn ReportDocument::finding(
  self : ReportDocument,
  finding~ : Finding,
) -> ReportDocument {
  self.add(ReportBlock::Finding(finding~))
}

///|
pub fn ReportDocument::block_count(self : ReportDocument) -> Int {
  self.blocks.length()
}

///|
fn heading_prefix(level : Int) -> String {
  let mut result = ""
  for _ in 0.. String {
  let severity = match finding.severity {
    Pass => "PASS"
    Notice => "NOTICE"
    Fail => "FAIL"
  }
  "- **\{severity} \{finding.code}** \{finding.title}: \{finding.detail} (measured=\{finding.measured}, limit=\{finding.limit})"
}

///|
pub fn ReportDocument::to_markdown(self : ReportDocument) -> String {
  let lines = ["# \{self.title}", ""]
  if self.subtitle.trim() != "" {
    lines.push(self.subtitle)
    lines.push("")
  }
  for block in self.blocks {
    match block {
      Heading(level~, text~) => {
        lines.push(heading_prefix(level) + " " + text)
        lines.push("")
      }
      Paragraph(text~) => {
        lines.push(text)
        lines.push("")
      }
      Bullet(items~) => {
        for item in items {
          lines.push("- " + item)
        }
        lines.push("")
      }
      Table(table~) => {
        lines.push(table.to_markdown())
        lines.push("")
      }
      Finding(finding~) => {
        lines.push(finding_markdown(finding))
        lines.push("")
      }
      Metric(name~, value~, unit~) => {
        lines.push("**\{name}**: \{value} \{unit}")
        lines.push("")
      }
      Divider => {
        lines.push("---")
        lines.push("")
      }
    }
  }
  lines.push("Generated by \{self.generated_by}")
  lines.join("\n")
}

///|
pub fn ReportDocument::summary(self : ReportDocument) -> String {
  let mut headings = 0
  let mut tables = 0
  let mut findings = 0
  for block in self.blocks {
    match block {
      Heading(..) => headings += 1
      Table(_) => tables += 1
      Finding(_) => findings += 1
      _ => ()
    }
  }
  "title=\{self.title},blocks=\{self.blocks.length()},headings=\{headings},tables=\{tables},findings=\{findings}"
}

///|
pub fn report_from_audit(
  title~ : String,
  audit : AuditReport,
) -> ReportDocument raise @domain.ExtractionError {
  let document = ReportDocument::new(title~)
  let result_text = if audit.passed { "PASS" } else { "FAIL" }
  let with_summary = document.paragraph(
    text="Audit result: \{result_text}; score=\{audit.score}",
  )
  let mut result = with_summary
  for finding in audit.findings {
    result = result.finding(finding~)
  }
  result
}

///|
pub(all) struct ReportIndex {
  title : String
  section_count : Int
  metric_count : Int
  table_count : Int
  failure_count : Int
} derive(Debug, Eq, ToJson)

///|
pub fn ReportDocument::index(self : ReportDocument) -> ReportIndex {
  let mut metrics = 0
  let mut tables = 0
  let mut failures = 0
  for block in self.blocks {
    match block {
      Metric(_) => metrics += 1
      Table(_) => tables += 1
      Finding(finding~) =>
        match finding.severity {
          Fail => failures += 1
          _ => ()
        }
      _ => ()
    }
  }
  {
    title: self.title,
    section_count: self.blocks.length(),
    metric_count: metrics,
    table_count: tables,
    failure_count: failures,
  }
}