///|
/// High-level database facts used by the CLI's text and JSON renderers.
pub(all) struct DatabaseInspection {
  header : DatabaseHeader
  physical_page_count : UInt64
  file_length : Int
  page_one : BtreePageHeader
  page_one_layout : PageLayout
  schema_tree : BtreeTraversal
  freelist : FreelistReport
  diagnostics : Array[Diagnostic]
}

///|
pub fn inspect_database(data : Bytes) -> DatabaseInspection raise ParseError {
  let database = DatabaseImage::open(data)
  let page_one = parse_btree_page_header(database, 1UL)
  let page_one_layout = analyze_page_layout(database, page_one)
  let schema_tree = traverse_btree(database, 1UL)
  let freelist = analyze_freelist(database)
  let diagnostics = validate_database_header(database.header, data.length())
  append_diagnostics(diagnostics, validate_page_layout(database, page_one))
  append_diagnostics(diagnostics, schema_tree.diagnostics)
  append_diagnostics(diagnostics, freelist.diagnostics)
  {
    header: database.header,
    physical_page_count: database.page_count,
    file_length: data.length(),
    page_one,
    page_one_layout,
    schema_tree,
    freelist,
    diagnostics,
  }
}

///|
fn optional_int_text(value : Int?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "-"
  }
}

///|
fn diagnostic_text(diagnostic : Diagnostic) -> String {
  let mut location = ""
  if diagnostic.page_number is Some(page) {
    location = location + " page=" + page.to_string()
  }
  if diagnostic.offset is Some(offset) {
    location = location + " offset=" + offset.to_string()
  }
  "[" +
  diagnostic.severity.label() +
  "] " +
  diagnostic.code +
  location +
  ": " +
  diagnostic.message
}

///|
fn diagnostics_text(diagnostics : Array[Diagnostic]) -> String {
  if diagnostics.length() == 0 {
    return "Diagnostics: none"
  }
  let lines : Array[String] = ["Diagnostics:"]
  for diagnostic in diagnostics {
    lines.push("  " + diagnostic_text(diagnostic))
  }
  lines.join("\n")
}

///|
fn json_string(value : String) -> String {
  value.escape(quote=true)
}

///|
fn optional_int_json(value : Int?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "null"
  }
}

///|
fn optional_page_json(value : UInt64?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "null"
  }
}

///|
fn diagnostic_json(diagnostic : Diagnostic) -> String {
  "{" +
  "\"severity\":" +
  json_string(diagnostic.severity.label()) +
  ",\"code\":" +
  json_string(diagnostic.code) +
  ",\"message\":" +
  json_string(diagnostic.message) +
  ",\"offset\":" +
  optional_int_json(diagnostic.offset) +
  ",\"page_number\":" +
  optional_page_json(diagnostic.page_number) +
  "}"
}

///|
fn diagnostics_json(diagnostics : Array[Diagnostic]) -> String {
  let values : Array[String] = []
  for diagnostic in diagnostics {
    values.push(diagnostic_json(diagnostic))
  }
  "[" + values.join(",") + "]"
}

///|
pub fn render_database_text(inspection : DatabaseInspection) -> String {
  let header = inspection.header
  let tree = summarize_btree(inspection.schema_tree)
  let lines : Array[String] = [
    "PageLens database inspection",
    "File length: " + inspection.file_length.to_string() + " bytes",
    "Physical pages: " + inspection.physical_page_count.to_string(),
    "Page size: " + header.page_size.to_string(),
    "Usable page size: " + header.usable_page_size().to_string(),
    "Write version: " + header.write_version.to_string(),
    "Read version: " + header.read_version.to_string(),
    "Reserved bytes: " + header.reserved_bytes.to_string(),
    "Payload fractions: " +
    header.max_embedded_payload_fraction.to_string() +
    "/" +
    header.min_embedded_payload_fraction.to_string() +
    "/" +
    header.leaf_payload_fraction.to_string(),
    "File change counter: " + header.file_change_counter.to_string(),
    "Header database pages: " + header.database_size_pages.to_string(),
    "First freelist trunk: " + header.first_freelist_trunk_page.to_string(),
    "Total freelist pages: " + header.total_freelist_pages.to_string(),
    "Schema cookie: " + header.schema_cookie.to_string(),
    "Schema format: " + header.schema_format.to_string(),
    "Default cache size: " + header.default_page_cache_size.to_string(),
    "Largest root B-tree page: " + header.largest_root_btree_page.to_string(),
    "Text encoding: " + header.text_encoding.label(),
    "User version: " + header.user_version.to_string(),
    "Incremental vacuum: " + header.incremental_vacuum.to_string(),
    "Application ID: " + header.application_id.to_string(),
    "Version valid for: " + header.version_valid_for.to_string(),
    "SQLite version number: " + header.sqlite_version_number.to_string(),
    "",
    "Page 1 type: " + inspection.page_one.page_type.label(),
    "Page 1 cells: " + inspection.page_one.cell_count.to_string(),
    "Page 1 free bytes: " +
    inspection.page_one_layout.total_free_bytes().to_string(),
    "Schema B-tree pages: " + inspection.schema_tree.visits.length().to_string(),
    "Schema B-tree interior pages: " + tree.interior_pages.to_string(),
    "Schema B-tree leaf pages: " + tree.leaf_pages.to_string(),
    "Schema B-tree cells: " + tree.total_cells.to_string(),
    "Schema B-tree maximum depth: " + tree.maximum_depth.to_string(),
    "Observed freelist pages: " +
    inspection.freelist.observed_page_count().to_string(),
    "",
    diagnostics_text(inspection.diagnostics),
  ]
  lines.join("\n")
}

///|
pub fn render_database_json(inspection : DatabaseInspection) -> String {
  let header = inspection.header
  let tree = summarize_btree(inspection.schema_tree)
  "{" +
  "\"kind\":\"database\"," +
  "\"file_length\":" +
  inspection.file_length.to_string() +
  ",\"physical_page_count\":" +
  inspection.physical_page_count.to_string() +
  ",\"header\":{" +
  "\"page_size\":" +
  header.page_size.to_string() +
  ",\"usable_page_size\":" +
  header.usable_page_size().to_string() +
  ",\"file_format_write_version\":" +
  header.write_version.to_string() +
  ",\"file_format_read_version\":" +
  header.read_version.to_string() +
  ",\"reserved_bytes\":" +
  header.reserved_bytes.to_string() +
  ",\"maximum_embedded_payload_fraction\":" +
  header.max_embedded_payload_fraction.to_string() +
  ",\"minimum_embedded_payload_fraction\":" +
  header.min_embedded_payload_fraction.to_string() +
  ",\"leaf_payload_fraction\":" +
  header.leaf_payload_fraction.to_string() +
  ",\"file_change_counter\":" +
  header.file_change_counter.to_string() +
  ",\"database_size_pages\":" +
  header.database_size_pages.to_string() +
  ",\"first_freelist_trunk_page\":" +
  header.first_freelist_trunk_page.to_string() +
  ",\"total_freelist_pages\":" +
  header.total_freelist_pages.to_string() +
  ",\"schema_cookie\":" +
  header.schema_cookie.to_string() +
  ",\"schema_format\":" +
  header.schema_format.to_string() +
  ",\"default_page_cache_size\":" +
  header.default_page_cache_size.to_string() +
  ",\"largest_root_btree_page\":" +
  header.largest_root_btree_page.to_string() +
  ",\"text_encoding\":" +
  json_string(header.text_encoding.label()) +
  ",\"user_version\":" +
  header.user_version.to_string() +
  ",\"incremental_vacuum\":" +
  header.incremental_vacuum.to_string() +
  ",\"application_id\":" +
  header.application_id.to_string() +
  ",\"version_valid_for\":" +
  header.version_valid_for.to_string() +
  ",\"sqlite_version_number\":" +
  header.sqlite_version_number.to_string() +
  "},\"page_one\":{" +
  "\"type\":" +
  json_string(inspection.page_one.page_type.label()) +
  ",\"cell_count\":" +
  inspection.page_one.cell_count.to_string() +
  ",\"free_bytes\":" +
  inspection.page_one_layout.total_free_bytes().to_string() +
  "},\"schema_btree_pages\":" +
  inspection.schema_tree.visits.length().to_string() +
  ",\"schema_btree\":{\"interior_pages\":" +
  tree.interior_pages.to_string() +
  ",\"leaf_pages\":" +
  tree.leaf_pages.to_string() +
  ",\"total_cells\":" +
  tree.total_cells.to_string() +
  ",\"maximum_depth\":" +
  tree.maximum_depth.to_string() +
  "}" +
  ",\"observed_freelist_pages\":" +
  inspection.freelist.observed_page_count().to_string() +
  ",\"diagnostics\":" +
  diagnostics_json(inspection.diagnostics) +
  "}"
}

///|
pub fn render_wal_text(wal : WalFile) -> String {
  let histories = wal.page_histories()
  let lines : Array[String] = [
    "PageLens WAL inspection",
    "Magic: " + wal.header.magic.to_string(),
    "Format version: " + wal.header.format_version.to_string(),
    "Page size: " + wal.header.page_size.to_string(),
    "Checkpoint sequence: " + wal.header.checkpoint_sequence.to_string(),
    "Header checksum valid: " + wal.header.checksum_valid.to_string(),
    "Frames: " + wal.frames.length().to_string(),
    "Valid frames: " + wal.valid_frame_count().to_string(),
    "Transactions: " + wal.transactions.length().to_string(),
    "Committed transactions: " + wal.committed_transaction_count().to_string(),
    "Distinct modified pages: " + histories.length().to_string(),
    "Uncommitted valid frames: " + wal.uncommitted_frame_count().to_string(),
    "Last commit frame: " + optional_int_text(wal.last_commit_frame),
    "Trailing bytes: " + wal.trailing_bytes.to_string(),
    "",
    "Frames:",
  ]
  for frame in wal.frames {
    lines.push(
      "  #" +
      frame.index.to_string() +
      " page=" +
      frame.page_number.to_string() +
      " commit_size=" +
      frame.database_size_after_commit.to_string() +
      " salt=" +
      frame.salt_valid.to_string() +
      " checksum=" +
      frame.checksum_valid.to_string(),
    )
  }
  lines.push("")
  lines.push(diagnostics_text(wal.diagnostics))
  lines.join("\n")
}

///|
pub fn render_wal_json(wal : WalFile) -> String {
  let frames : Array[String] = []
  let histories = wal.page_histories()
  for frame in wal.frames {
    frames.push(
      "{\"index\":" +
      frame.index.to_string() +
      ",\"page_number\":" +
      frame.page_number.to_string() +
      ",\"database_size_after_commit\":" +
      frame.database_size_after_commit.to_string() +
      ",\"salt_valid\":" +
      frame.salt_valid.to_string() +
      ",\"checksum_valid\":" +
      frame.checksum_valid.to_string() +
      "}",
    )
  }
  "{" +
  "\"kind\":\"wal\"," +
  "\"magic\":" +
  wal.header.magic.to_string() +
  ",\"format_version\":" +
  wal.header.format_version.to_string() +
  ",\"page_size\":" +
  wal.header.page_size.to_string() +
  ",\"checkpoint_sequence\":" +
  wal.header.checkpoint_sequence.to_string() +
  ",\"header_checksum_valid\":" +
  wal.header.checksum_valid.to_string() +
  ",\"last_commit_frame\":" +
  optional_int_json(wal.last_commit_frame) +
  ",\"committed_transactions\":" +
  wal.committed_transaction_count().to_string() +
  ",\"distinct_modified_pages\":" +
  histories.length().to_string() +
  ",\"uncommitted_valid_frames\":" +
  wal.uncommitted_frame_count().to_string() +
  ",\"trailing_bytes\":" +
  wal.trailing_bytes.to_string() +
  ",\"frames\":[" +
  frames.join(",") +
  "],\"diagnostics\":" +
  diagnostics_json(wal.diagnostics) +
  "}"
}

///|
pub fn render_snapshot_text(snapshot : SnapshotView) -> String raise ParseError {
  let differences = snapshot.differences()
  let statistics = summarize_snapshot(snapshot)
  let lines : Array[String] = [
    "PageLens committed snapshot",
    "Commit frame: " + snapshot.max_frame.to_string(),
    "Logical database pages: " + snapshot.logical_page_count.to_string(),
    "Changed pages: " + differences.length().to_string(),
    "Changed bytes: " + statistics.changed_bytes.to_string(),
    "Changed ranges: " + statistics.changed_ranges.to_string(),
    "Pages added by WAL: " + statistics.pages_added_by_wal.to_string(),
    "",
    "Changes:",
  ]
  for difference in differences {
    lines.push(
      "  page=" +
      difference.page_number.to_string() +
      " frame=" +
      difference.frame_index.to_string() +
      " changed_bytes=" +
      difference.changed_bytes.to_string() +
      " first=" +
      optional_int_text(difference.first_changed_offset) +
      " last=" +
      optional_int_text(difference.last_changed_offset),
    )
  }
  lines.join("\n")
}

///|
pub fn render_snapshot_json(snapshot : SnapshotView) -> String raise ParseError {
  let changes : Array[String] = []
  let statistics = summarize_snapshot(snapshot)
  for difference in snapshot.differences() {
    changes.push(
      "{\"page_number\":" +
      difference.page_number.to_string() +
      ",\"frame_index\":" +
      difference.frame_index.to_string() +
      ",\"changed_bytes\":" +
      difference.changed_bytes.to_string() +
      ",\"first_changed_offset\":" +
      optional_int_json(difference.first_changed_offset) +
      ",\"last_changed_offset\":" +
      optional_int_json(difference.last_changed_offset) +
      "}",
    )
  }
  "{" +
  "\"kind\":\"snapshot\"," +
  "\"commit_frame\":" +
  snapshot.max_frame.to_string() +
  ",\"logical_page_count\":" +
  snapshot.logical_page_count.to_string() +
  ",\"changed_pages\":" +
  statistics.changed_pages.to_string() +
  ",\"changed_bytes\":" +
  statistics.changed_bytes.to_string() +
  ",\"changed_ranges\":" +
  statistics.changed_ranges.to_string() +
  ",\"pages_added_by_wal\":" +
  statistics.pages_added_by_wal.to_string() +
  ",\"largest_page_change\":" +
  statistics.largest_page_change.to_string() +
  ",\"changes\":[" +
  changes.join(",") +
  "]}"
}

///|
pub fn render_integrity_text(report : IntegrityReport) -> String {
  let lines : Array[String] = [
    "PageLens consistency report",
    "Result: " + (if report.is_ok() { "PASS" } else { "FAIL" }),
    "Errors: " + report.error_count().to_string(),
    "Warnings: " + report.warning_count().to_string(),
    "Info: " + report.info_count().to_string(),
    "Pages examined: " + report.pages_examined.to_string(),
    "B-tree pages: " + report.btree_pages.to_string(),
    "B-tree cells: " + report.btree_cells.to_string(),
    "Records decoded: " + report.records_decoded.to_string(),
    "Overflow pages: " + report.overflow_pages.to_string(),
    "Freelist pages: " + report.freelist_pages.to_string(),
    "WAL frames: " + report.wal_frames.to_string(),
    "",
    diagnostics_text(report.diagnostics),
  ]
  lines.join("\n")
}

///|
pub fn render_integrity_json(report : IntegrityReport) -> String {
  "{" +
  "\"kind\":\"consistency\"," +
  "\"ok\":" +
  report.is_ok().to_string() +
  ",\"errors\":" +
  report.error_count().to_string() +
  ",\"warnings\":" +
  report.warning_count().to_string() +
  ",\"info\":" +
  report.info_count().to_string() +
  ",\"pages_examined\":" +
  report.pages_examined.to_string() +
  ",\"btree_pages\":" +
  report.btree_pages.to_string() +
  ",\"btree_cells\":" +
  report.btree_cells.to_string() +
  ",\"records_decoded\":" +
  report.records_decoded.to_string() +
  ",\"overflow_pages\":" +
  report.overflow_pages.to_string() +
  ",\"freelist_pages\":" +
  report.freelist_pages.to_string() +
  ",\"wal_frames\":" +
  report.wal_frames.to_string() +
  ",\"diagnostics\":" +
  diagnostics_json(report.diagnostics) +
  "}"
}