///|
/// A value-level change between two merged views.
pub struct DiffItem {
  path : String
  before : String?
  after : String?
  kind : String
} derive(@debug.Debug, Eq)

///|
/// Computes added, removed, and changed keys between two views.
pub fn diff_views(before : ConfigView, after : ConfigView) -> Array[DiffItem] {
  let result : Array[DiffItem] = []
  for old_value in before.values {
    let path = entry_path(old_value.section, old_value.key)
    match after.get_path(path) {
      None =>
        result.push({
          path,
          before: Some(old_value.value),
          after: None,
          kind: "removed",
        })
      Some(new_value) =>
        if new_value != old_value.value {
          result.push({
            path,
            before: Some(old_value.value),
            after: Some(new_value),
            kind: "changed",
          })
        }
    }
  }
  for new_value in after.values {
    let path = entry_path(new_value.section, new_value.key)
    if before.get_path(path) == None {
      result.push({
        path,
        before: None,
        after: Some(new_value.value),
        kind: "added",
      })
    }
  }
  result
}

///|
/// Redacts a value when the path looks sensitive.
pub fn redact_value(path : String, value : String) -> String {
  if is_sensitive_path(path) {
    if value.length() == 0 {
      ""
    } else {
      "********"
    }
  } else {
    value
  }
}

///|
/// Returns true for common secret-bearing key names.
pub fn is_sensitive_path(path : String) -> Bool {
  let lower = ascii_lower_path(path)
  lower.contains("password") ||
  lower.contains("passwd") ||
  lower.contains("secret") ||
  lower.contains("token") ||
  lower.contains("apikey") ||
  lower.contains("api_key") ||
  lower.contains("private_key")
}

///|
fn ascii_lower_path(text : String) -> String {
  let builder = StringBuilder()
  let mut index = 0
  while index < text.length() {
    let ch = text[index]
    if ch >= 'A' && ch <= 'Z' {
      builder.write_char((ch.to_int() + 32).unsafe_to_char())
    } else {
      builder.write_char(ch.to_int().unsafe_to_char())
    }
    index = index + 1
  }
  builder.to_string()
}

///|
/// Renders a Markdown report for humans and CI summaries.
pub fn render_markdown_report(
  view : ConfigView,
  validation_diagnostics : Array[Diagnostic],
) -> String {
  let builder = StringBuilder()
  builder.write_string("# MoonConfigKit Report\n\n")
  builder.write_string("## Summary\n\n")
  builder.write_string(
    "- Effective keys: " + view.values.length().to_string() + "\n",
  )
  builder.write_string(
    "- Merge diagnostics: " + view.diagnostics.length().to_string() + "\n",
  )
  builder.write_string(
    "- Validation diagnostics: " +
    validation_diagnostics.length().to_string() +
    "\n",
  )
  builder.write_string(
    "- Errors: " + error_count(validation_diagnostics).to_string() + "\n\n",
  )
  builder.write_string("## Effective Values\n\n")
  builder.write_string("| Path | Value | Layer | Source |\n")
  builder.write_string("| --- | --- | --- | --- |\n")
  for item in view.values {
    let path = entry_path(item.section, item.key)
    builder.write_string(
      "| " +
      escape_markdown_cell(path) +
      " | " +
      escape_markdown_cell(redact_value(path, item.value)) +
      " | " +
      escape_markdown_cell(item.source_layer) +
      " | " +
      escape_markdown_cell(item.source + ":" + item.span.line.to_string()) +
      " |\n",
    )
  }
  builder.write_string("\n## Diagnostics\n\n")
  if validation_diagnostics.length() == 0 {
    builder.write_string("No validation diagnostics.\n")
  } else {
    for item in validation_diagnostics {
      builder.write_string(
        "- " +
        severity_label(item.severity) +
        " `" +
        item.code +
        "`: " +
        item.message,
      )
      match item.hint {
        Some(hint) => builder.write_string(" Hint: " + hint)
        None => ()
      }
      builder.write_string("\n")
    }
  }
  builder.to_string()
}

///|
fn severity_label(severity : Severity) -> String {
  match severity {
    Error => "ERROR"
    Warning => "WARN"
    Info => "INFO"
  }
}

///|
/// Renders a compact JSON-like report without depending on a JSON package.
pub fn render_json_like_report(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
) -> String {
  let builder = StringBuilder()
  builder.write_string("{\n")
  builder.write_string(
    "  \"keys\": " + view.values.length().to_string() + ",\n",
  )
  builder.write_string(
    "  \"errors\": " + error_count(diagnostics).to_string() + ",\n",
  )
  builder.write_string("  \"values\": [\n")
  let mut index = 0
  for item in view.values {
    let path = entry_path(item.section, item.key)
    builder.write_string(
      "    {\"path\": \"" +
      escape_json(path) +
      "\", \"value\": \"" +
      escape_json(redact_value(path, item.value)) +
      "\", \"layer\": \"" +
      escape_json(item.source_layer) +
      "\"}",
    )
    index = index + 1
    if index < view.values.length() {
      builder.write_string(",")
    }
    builder.write_string("\n")
  }
  builder.write_string("  ]\n")
  builder.write_string("}\n")
  builder.to_string()
}

///|
fn escape_json(text : String) -> String {
  text
  .replace(old="\\", new="\\\\")
  .replace(old="\"", new="\\\"")
  .replace(old="\r", new="\\r")
  .replace(old="\n", new="\\n")
  .replace(old="\t", new="\\t")
}

///|
/// Renders changes as a Markdown table.
pub fn render_diff_markdown(items : Array[DiffItem]) -> String {
  let builder = StringBuilder()
  builder.write_string("| Path | Kind | Before | After |\n")
  builder.write_string("| --- | --- | --- | --- |\n")
  for item in items {
    builder.write_string(
      "| " +
      escape_markdown_cell(item.path) +
      " | " +
      escape_markdown_cell(item.kind) +
      " | " +
      escape_markdown_cell(option_text(item.path, item.before)) +
      " | " +
      escape_markdown_cell(option_text(item.path, item.after)) +
      " |\n",
    )
  }
  builder.to_string()
}

///|
fn option_text(path : String, value : String?) -> String {
  match value {
    None => ""
    Some(text) => redact_value(path, text)
  }
}

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