///| 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("| " + path + " | " + redact_value(path, item.value) + " | " + item.source_layer + " | " + 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="\n", new="\\n")
}

///| 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("| " + item.path + " | " + item.kind + " | " + option_text(item.before) + " | " + option_text(item.after) + " |\n")
  }
  builder.to_string()
}

fn option_text(value : String?) -> String {
  match value {
    None => ""
    Some(text) => text
  }
}