///|
/// Formatting options for INI and properties output.
pub struct FormatOptions {
  redact_secrets : Bool
  include_source_comments : Bool
  blank_line_between_sections : Bool
  quote_values : Bool
} derive(@debug.Debug, Eq)

///|
pub fn default_format_options() -> FormatOptions {
  {
    redact_secrets: false,
    include_source_comments: true,
    blank_line_between_sections: true,
    quote_values: false,
  }
}

///|
pub fn safe_format_options() -> FormatOptions {
  {
    redact_secrets: true,
    include_source_comments: false,
    blank_line_between_sections: true,
    quote_values: false,
  }
}

///|
/// Renders a merged view as INI text.
pub fn render_ini(
  view : ConfigView,
  options? : FormatOptions = default_format_options(),
) -> String {
  let builder = StringBuilder()
  let sections = collect_sections(view)
  render_global_entries(builder, view, options)
  let mut wrote_any_section = has_global_entries(view)
  for section_name in sections {
    if section_name != "" {
      if wrote_any_section && options.blank_line_between_sections {
        builder.write_string("\n")
      }
      builder.write_string("[" + section_name + "]\n")
      for item in view.values {
        if item.section == section_name {
          render_entry_line(builder, item, options)
        }
      }
      wrote_any_section = true
    }
  }
  builder.to_string()
}

///|
fn has_global_entries(view : ConfigView) -> Bool {
  for item in view.values {
    if item.section == "" {
      return true
    }
  }
  false
}

///|
fn render_global_entries(
  builder : StringBuilder,
  view : ConfigView,
  options : FormatOptions,
) -> Unit {
  for item in view.values {
    if item.section == "" {
      render_entry_line(builder, item, options)
    }
  }
}

///|
fn render_entry_line(
  builder : StringBuilder,
  item : ResolvedValue,
  options : FormatOptions,
) -> Unit {
  let path = entry_path(item.section, item.key)
  if options.include_source_comments {
    builder.write_string(
      "; from " +
      item.source_layer +
      " " +
      item.source +
      ":" +
      item.span.line.to_string() +
      "\n",
    )
  }
  let value = if options.redact_secrets {
    redact_value(path, item.value)
  } else {
    item.value
  }
  builder.write_string(
    item.key + " = " + format_value(value, options.quote_values) + "\n",
  )
}

///|
fn format_value(value : String, quote : Bool) -> String {
  if quote ||
    value.contains("#") ||
    value.contains(";") ||
    value.contains("\n") ||
    value.contains("\r") ||
    value.contains("\t") ||
    value.has_prefix(" ") ||
    value.has_suffix(" ") {
    "\"" +
    value
    .replace(old="\\", new="\\\\")
    .replace(old="\"", new="\\\"")
    .replace(old="\r", new="\\r")
    .replace(old="\n", new="\\n")
    .replace(old="\t", new="\\t") +
    "\""
  } else {
    value
  }
}

///|
fn collect_sections(view : ConfigView) -> Array[String] {
  let sections : Array[String] = []
  for item in view.values {
    if item.section != "" && !contains_section(sections, item.section) {
      sections.push(item.section)
    }
  }
  sections
}

///|
fn contains_section(sections : Array[String], name : String) -> Bool {
  for section_name in sections {
    if section_name == name {
      return true
    }
  }
  false
}

///|
/// Renders a merged view as Java-style properties text.
pub fn render_properties(
  view : ConfigView,
  options? : FormatOptions = default_format_options(),
) -> String {
  let builder = StringBuilder()
  for item in view.values {
    let path = entry_path(item.section, item.key)
    if options.include_source_comments {
      builder.write_string(
        "# from " +
        item.source_layer +
        " " +
        item.source +
        ":" +
        item.span.line.to_string() +
        "\n",
      )
    }
    let value = if options.redact_secrets {
      redact_value(path, item.value)
    } else {
      item.value
    }
    builder.write_string(
      escape_properties_key(path) + "=" + escape_properties_value(value) + "\n",
    )
  }
  builder.to_string()
}

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

///|
/// Returns a document preview with defaults from schema filled into missing keys.
pub fn render_defaults_preview(
  schema : ConfigSchema,
  view : ConfigView,
) -> String {
  let builder = StringBuilder()
  for schema_section in schema.sections {
    builder.write_string("[" + schema_section.name + "]\n")
    for schema_field in schema_section.fields {
      let key = field_key(schema_field.path)
      match view.get_path(schema_field.path) {
        Some(value) =>
          builder.write_string(
            key + " = " + redact_value(schema_field.path, value) + "\n",
          )
        None =>
          match schema_field.default_value {
            Some(default_value) =>
              builder.write_string(
                key +
                " = " +
                redact_value(schema_field.path, default_value) +
                " ; default\n",
              )
            None =>
              if schema_field.required {
                builder.write_string("; " + key + " = \n")
              }
          }
      }
    }
    builder.write_string("\n")
  }
  builder.to_string()
}

///|
fn field_key(path : String) -> String {
  let parts = path.split(".").map(part => part.to_owned()).collect()
  if parts.length() == 0 {
    path
  } else {
    parts[parts.length() - 1]
  }
}

///|
/// Produces a plain key inventory grouped by section.
pub fn render_inventory(view : ConfigView) -> String {
  let builder = StringBuilder()
  builder.write_string("Configuration Inventory\n")
  builder.write_string("=======================\n\n")
  for item in view.values {
    let path = entry_path(item.section, item.key)
    builder.write_string(
      "- " +
      path +
      " (" +
      item.source_layer +
      ", " +
      item.source +
      ":" +
      item.span.line.to_string() +
      ")\n",
    )
  }
  builder.to_string()
}