///|
/// Metadata editing helpers make MoonFolio usable inside automation. A CI job
/// can inspect a content folio, normalize its field order, and apply safe
/// metadata changes without touching the Markdown body.

///|
pub(all) enum EditAction {
  SetMeta
  RemoveMeta
  RenameMeta
  AppendListItem
  RemoveListItem
  NormalizeOrder
  EnsureSlug
  EnsurePermalink
  EnsureFingerprint
} derive(Eq, Debug)

///|
pub(all) struct MetadataEdit {
  action : EditAction
  key : String
  new_key : String
  value : MetaValue
  list_item : String
  preferred_order : Array[String]
  base_path : String
} derive(Eq, Debug)

///|
pub(all) struct EditPreview {
  changed : Bool
  before_fingerprint : String
  after_fingerprint : String
  before_yaml : String
  after_yaml : String
  document : Document
} derive(Eq, Debug)

///|
pub fn metadata_set_text(key : String, value : String) -> MetadataEdit {
  {
    action: SetMeta,
    key,
    new_key: "",
    value: Text(value),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_set_bool(key : String, value : Bool) -> MetadataEdit {
  {
    action: SetMeta,
    key,
    new_key: "",
    value: Bool(value),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_set_int(key : String, value : Int) -> MetadataEdit {
  {
    action: SetMeta,
    key,
    new_key: "",
    value: Int(value),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_set_list(key : String, values : Array[String]) -> MetadataEdit {
  {
    action: SetMeta,
    key,
    new_key: "",
    value: List(copy_strings(values)),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_remove(key : String) -> MetadataEdit {
  {
    action: RemoveMeta,
    key,
    new_key: "",
    value: Text(""),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_rename(key : String, new_key : String) -> MetadataEdit {
  {
    action: RenameMeta,
    key,
    new_key,
    value: Text(""),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_append_list_item(key : String, item : String) -> MetadataEdit {
  {
    action: AppendListItem,
    key,
    new_key: "",
    value: Text(""),
    list_item: item,
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_remove_list_item(key : String, item : String) -> MetadataEdit {
  {
    action: RemoveListItem,
    key,
    new_key: "",
    value: Text(""),
    list_item: item,
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_normalize_order(order : Array[String]) -> MetadataEdit {
  {
    action: NormalizeOrder,
    key: "",
    new_key: "",
    value: Text(""),
    list_item: "",
    preferred_order: copy_strings(order),
    base_path: "",
  }
}

///|
pub fn metadata_ensure_slug() -> MetadataEdit {
  {
    action: EnsureSlug,
    key: "slug",
    new_key: "",
    value: Text(""),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn metadata_ensure_permalink(base_path : String) -> MetadataEdit {
  {
    action: EnsurePermalink,
    key: "permalink",
    new_key: "",
    value: Text(""),
    list_item: "",
    preferred_order: [],
    base_path,
  }
}

///|
pub fn metadata_ensure_fingerprint() -> MetadataEdit {
  {
    action: EnsureFingerprint,
    key: "fingerprint",
    new_key: "",
    value: Text(""),
    list_item: "",
    preferred_order: [],
    base_path: "",
  }
}

///|
pub fn document_set_field(
  doc : Document,
  key : String,
  value : MetaValue,
) -> Document {
  let fields = copy_field_map(doc)
  let order = copy_strings(doc.order)
  if !fields.contains(key) {
    order.push(key)
  }
  fields.set(key, value)
  rebuild_document(doc, fields, order)
}

///|
pub fn document_set_text(
  doc : Document,
  key : String,
  value : String,
) -> Document {
  document_set_field(doc, key, Text(value))
}

///|
pub fn document_set_bool(
  doc : Document,
  key : String,
  value : Bool,
) -> Document {
  document_set_field(doc, key, Bool(value))
}

///|
pub fn document_set_int(doc : Document, key : String, value : Int) -> Document {
  document_set_field(doc, key, Int(value))
}

///|
pub fn document_set_list(
  doc : Document,
  key : String,
  values : Array[String],
) -> Document {
  document_set_field(doc, key, List(copy_strings(values)))
}

///|
pub fn document_remove_field(doc : Document, key : String) -> Document {
  let fields : Map[String, MetaValue] = Map([])
  let order : Array[String] = []
  for existing in doc.order {
    if existing != key {
      match doc.fields.get(existing) {
        Some(value) => {
          fields.set(existing, value)
          order.push(existing)
        }
        None => ()
      }
    }
  }
  rebuild_document(doc, fields, order)
}

///|
pub fn document_rename_field(
  doc : Document,
  key : String,
  new_key : String,
) -> Document {
  if key == new_key {
    return doc
  }
  match doc.fields.get(key) {
    None => doc
    Some(value) => {
      let fields : Map[String, MetaValue] = Map([])
      let order : Array[String] = []
      let mut inserted = false
      for existing in doc.order {
        if existing == key {
          if !fields.contains(new_key) {
            fields.set(new_key, value)
            order.push(new_key)
            inserted = true
          }
        } else if existing != new_key {
          match doc.fields.get(existing) {
            Some(existing_value) => {
              fields.set(existing, existing_value)
              order.push(existing)
            }
            None => ()
          }
        }
      }
      if !inserted {
        fields.set(new_key, value)
        order.push(new_key)
      }
      rebuild_document(doc, fields, order)
    }
  }
}

///|
pub fn document_append_list_item(
  doc : Document,
  key : String,
  item : String,
) -> Document {
  let values = match get_list(doc, key) {
    Some(existing) => copy_strings(existing)
    None => []
  }
  if !contains_string(values, item) {
    values.push(item)
  }
  document_set_list(doc, key, values)
}

///|
pub fn document_remove_list_item(
  doc : Document,
  key : String,
  item : String,
) -> Document {
  let values : Array[String] = []
  match get_list(doc, key) {
    Some(existing) =>
      for value in existing {
        if value != item {
          values.push(value)
        }
      }
    None => ()
  }
  document_set_list(doc, key, values)
}

///|
pub fn document_normalize_order(
  doc : Document,
  preferred_order : Array[String],
) -> Document {
  let fields = copy_field_map(doc)
  let order : Array[String] = []
  for key in preferred_order {
    if fields.contains(key) && !contains_string(order, key) {
      order.push(key)
    }
  }
  for key in doc.order {
    if fields.contains(key) && !contains_string(order, key) {
      order.push(key)
    }
  }
  rebuild_document(doc, fields, order)
}

///|
pub fn document_sort_fields(doc : Document) -> Document {
  document_normalize_order(doc, sorted_strings(doc.order))
}

///|
pub fn document_ensure_slug(doc : Document) -> Document {
  match get_text(doc, "slug") {
    Some(value) =>
      if value.trim().is_empty() {
        document_set_text(doc, "slug", slugify(document_title_for_slug(doc)))
      } else {
        document_set_text(doc, "slug", slugify(value))
      }
    None =>
      document_set_text(doc, "slug", slugify(document_title_for_slug(doc)))
  }
}

///|
pub fn document_ensure_permalink(
  doc : Document,
  base_path : String,
) -> Document {
  let with_slug = document_ensure_slug(doc)
  let slug = get_text(with_slug, "slug").unwrap_or("untitled")
  let base = base_path.trim_end(chars="/").to_owned()
  let permalink = if base.is_empty() { "/" + slug } else { base + "/" + slug }
  document_set_text(with_slug, "permalink", permalink)
}

///|
pub fn document_ensure_fingerprint(doc : Document) -> Document {
  document_set_text(doc, "fingerprint", fingerprint_document(doc))
}

///|
pub fn apply_metadata_edit(doc : Document, edit : MetadataEdit) -> Document {
  match edit.action {
    SetMeta => document_set_field(doc, edit.key, edit.value)
    RemoveMeta => document_remove_field(doc, edit.key)
    RenameMeta => document_rename_field(doc, edit.key, edit.new_key)
    AppendListItem => document_append_list_item(doc, edit.key, edit.list_item)
    RemoveListItem => document_remove_list_item(doc, edit.key, edit.list_item)
    NormalizeOrder => document_normalize_order(doc, edit.preferred_order)
    EnsureSlug => document_ensure_slug(doc)
    EnsurePermalink => document_ensure_permalink(doc, edit.base_path)
    EnsureFingerprint => document_ensure_fingerprint(doc)
  }
}

///|
pub fn apply_metadata_edits(
  doc : Document,
  edits : Array[MetadataEdit],
) -> Document {
  let mut current = doc
  for edit in edits {
    current = apply_metadata_edit(current, edit)
  }
  current
}

///|
pub fn preview_metadata_edit(
  doc : Document,
  edit : MetadataEdit,
) -> EditPreview {
  preview_metadata_edits(doc, [edit])
}

///|
pub fn preview_metadata_edits(
  doc : Document,
  edits : Array[MetadataEdit],
) -> EditPreview {
  let before_yaml = to_yaml(doc)
  let before_fingerprint = fingerprint_document(doc)
  let edited = apply_metadata_edits(doc, edits)
  let after_yaml = to_yaml(edited)
  let after_fingerprint = fingerprint_document(edited)
  {
    changed: before_fingerprint != after_fingerprint,
    before_fingerprint,
    after_fingerprint,
    before_yaml,
    after_yaml,
    document: edited,
  }
}

///|
pub fn render_document(doc : Document, format : MatterFormat) -> String {
  match format {
    None => doc.body
    Yaml => to_yaml(document_with_format(doc, Yaml))
    Toml => to_toml(document_with_format(doc, Toml))
    Json => to_json_front_matter(document_with_format(doc, Json))
  }
}

///|
pub fn document_with_format(doc : Document, format : MatterFormat) -> Document {
  {
    format,
    fields: copy_field_map(doc),
    order: copy_strings(doc.order),
    raw: doc.raw,
    body: doc.body,
    start_line: doc.start_line,
    end_line: doc.end_line,
  }
}

///|
pub fn to_json_front_matter(doc : Document) -> String {
  if doc.format == None {
    doc.body
  } else {
    let buf = StringBuilder(size_hint=doc.raw.length() + doc.body.length() + 16)
    buf.write_string(";;;\n{")
    for i in 0.. {
          if i > 0 {
            buf.write_string(",")
          }
          buf.write_string("\n  \"")
          buf.write_string(escape_string(key))
          buf.write_string("\": ")
          buf.write_string(format_json_meta_value(value))
        }
        None => ()
      }
    }
    if doc.order.length() > 0 {
      buf.write_string("\n")
    }
    buf.write_string("}\n;;;")
    if !doc.body.is_empty() {
      buf.write_string("\n")
      buf.write_string(doc.body)
    }
    buf.to_string()
  }
}

///|
pub fn canonicalize_document(doc : Document) -> Document {
  let ordered = document_normalize_order(doc, [
    "title", "slug", "summary", "date", "draft", "tags", "permalink", "fingerprint",
  ])
  let with_slug = document_ensure_slug(ordered)
  document_ensure_fingerprint(with_slug)
}

///|
pub fn canonical_yaml(doc : Document) -> String {
  to_yaml(canonicalize_document(document_with_format(doc, Yaml)))
}

///|
pub fn edit_action_name(action : EditAction) -> String {
  match action {
    SetMeta => "set"
    RemoveMeta => "remove"
    RenameMeta => "rename"
    AppendListItem => "append-list-item"
    RemoveListItem => "remove-list-item"
    NormalizeOrder => "normalize-order"
    EnsureSlug => "ensure-slug"
    EnsurePermalink => "ensure-permalink"
    EnsureFingerprint => "ensure-fingerprint"
  }
}

///|
pub fn edit_plan_summary(edits : Array[MetadataEdit]) -> String {
  let parts : Array[String] = []
  for edit in edits {
    let detail = if edit.key.is_empty() {
      edit_action_name(edit.action)
    } else if edit.new_key.is_empty() {
      edit_action_name(edit.action) + ":" + edit.key
    } else {
      edit_action_name(edit.action) + ":" + edit.key + "->" + edit.new_key
    }
    parts.push(detail)
  }
  parts.join(", ")
}

///|
pub fn document_field_count(doc : Document) -> Int {
  doc.order.length()
}

///|
pub fn document_missing_fields(
  doc : Document,
  keys : Array[String],
) -> Array[String] {
  let missing : Array[String] = []
  for key in keys {
    if !doc.fields.contains(key) {
      missing.push(key)
    }
  }
  missing
}

///|
pub fn document_present_fields(
  doc : Document,
  keys : Array[String],
) -> Array[String] {
  let present : Array[String] = []
  for key in keys {
    if doc.fields.contains(key) {
      present.push(key)
    }
  }
  present
}

///|
pub fn document_has_blank_text(doc : Document, key : String) -> Bool {
  match get_text(doc, key) {
    Some(value) => value.trim().is_empty()
    None => false
  }
}

///|
pub fn document_metadata_line_count(doc : Document) -> Int {
  if doc.raw.is_empty() {
    0
  } else {
    doc.raw.split("\n").to_array().length()
  }
}

///|
fn rebuild_document(
  doc : Document,
  fields : Map[String, MetaValue],
  order : Array[String],
) -> Document {
  {
    format: doc.format,
    fields,
    order,
    raw: doc.raw,
    body: doc.body,
    start_line: doc.start_line,
    end_line: doc.end_line,
  }
}

///|
fn copy_field_map(doc : Document) -> Map[String, MetaValue] {
  let fields : Map[String, MetaValue] = Map([])
  for key in doc.order {
    match doc.fields.get(key) {
      Some(value) => fields.set(key, value)
      None => ()
    }
  }
  fields
}

///|
fn document_title_for_slug(doc : Document) -> String {
  get_text(doc, "title").unwrap_or("untitled")
}

///|
fn format_json_meta_value(value : MetaValue) -> String {
  match value {
    Text(text) => "\"" + escape_string(text) + "\""
    Bool(flag) => flag.to_string()
    Int(number) => number.to_string()
    List(items) => {
      let parts : Array[String] = []
      for item in items {
        parts.push("\"" + escape_string(item) + "\"")
      }
      "[" + parts.join(", ") + "]"
    }
  }
}