///|
/// Semantic keymap diffing for review and migration planning.
pub(all) enum ChangeKind {
  Added
  Removed
  Modified
  Unchanged
} derive(Eq, @debug.Debug)

///|
pub(all) struct BindingChange {
  kind : ChangeKind
  id : String
  before : Binding?
  after : Binding?
  summary : String
  risk : Severity
} derive(Eq, @debug.Debug)

///|
pub(all) struct KeymapDiff {
  from_name : String
  to_name : String
  changes : Array[BindingChange]
  added : Int
  removed : Int
  modified : Int
  unchanged : Int
  context_added : Int
  context_removed : Int
  reserved_added : Int
  reserved_removed : Int
} derive(Eq, @debug.Debug)

///|
fn binding_equal(left : Binding, right : Binding) -> Bool {
  left.command == right.command &&
  left.keys.canonical == right.keys.canonical &&
  left.context == right.context &&
  lower_ascii(left.platform) == lower_ascii(right.platform) &&
  left.priority == right.priority &&
  left.enabled == right.enabled &&
  left.description == right.description
}

///|
fn binding_change(
  id : String,
  before : Binding,
  after : Binding,
) -> BindingChange {
  let key_changed = before.keys.canonical != after.keys.canonical
  let context_changed = before.context != after.context
  let command_changed = before.command != after.command
  let summary = if command_changed {
    "command " + before.command + " → " + after.command
  } else if key_changed {
    "shortcut " + before.keys.canonical + " → " + after.keys.canonical
  } else if context_changed {
    "context " + before.context + " → " + after.context
  } else {
    "metadata or priority changed"
  }
  let risk = if command_changed || key_changed { Error } else { Warning }
  {
    kind: Modified,
    id,
    before: Some(before),
    after: Some(after),
    summary,
    risk,
  }
}

///|
fn context_names(keymap : Keymap) -> Array[String] {
  keymap.contexts.map(context => context.name)
}

///|
fn marker_count_difference(
  before : Array[String],
  after : Array[String],
) -> (Int, Int) {
  let mut added = 0
  let mut removed = 0
  for marker in after {
    if !array_contains(before, marker) {
      added += 1
    }
  }
  for marker in before {
    if !array_contains(after, marker) {
      removed += 1
    }
  }
  (added, removed)
}

///|
/// Compare declarations by id and retain unchanged entries for auditability.
pub fn diff_keymaps(before : Keymap, after : Keymap) -> KeymapDiff {
  let changes : Array[BindingChange] = []
  let mut added = 0
  let mut removed = 0
  let mut modified = 0
  let mut unchanged = 0
  for old_binding in before.bindings {
    match find_binding(after, old_binding.id) {
      None => {
        removed += 1
        changes.push({
          kind: Removed,
          id: old_binding.id,
          before: Some(old_binding),
          after: None,
          summary: "binding removed",
          risk: Warning,
        })
      }
      Some(new_binding) =>
        if binding_equal(old_binding, new_binding) {
          unchanged += 1
          changes.push({
            kind: Unchanged,
            id: old_binding.id,
            before: Some(old_binding),
            after: Some(new_binding),
            summary: "unchanged",
            risk: Info,
          })
        } else {
          modified += 1
          changes.push(binding_change(old_binding.id, old_binding, new_binding))
        }
    }
  }
  for new_binding in after.bindings {
    if find_binding(before, new_binding.id) is None {
      added += 1
      changes.push({
        kind: Added,
        id: new_binding.id,
        before: None,
        after: Some(new_binding),
        summary: "binding added",
        risk: Info,
      })
    }
  }
  let (context_added, context_removed) = marker_count_difference(
    context_names(before),
    context_names(after),
  )
  let (reserved_added, reserved_removed) = marker_count_difference(
    before.reserved,
    after.reserved,
  )
  {
    from_name: before.name,
    to_name: after.name,
    changes,
    added,
    removed,
    modified,
    unchanged,
    context_added,
    context_removed,
    reserved_added,
    reserved_removed,
  }
}

///|
fn change_kind_name(kind : ChangeKind) -> String {
  match kind {
    Added => "added"
    Removed => "removed"
    Modified => "modified"
    Unchanged => "unchanged"
  }
}

///|
/// Return only changes that may alter runtime behavior.
pub fn behavioral_changes(diff : KeymapDiff) -> Array[BindingChange] {
  diff.changes.filter(change => change.kind != Unchanged && change.risk != Info)
}

///|
pub fn keymap_diff_to_json(diff : KeymapDiff) -> String {
  let rows : Array[String] = []
  for change in diff.changes {
    rows.push(
      "{\"kind\":" +
      json_string(change_kind_name(change.kind)) +
      ",\"id\":" +
      json_string(change.id) +
      ",\"summary\":" +
      json_string(change.summary) +
      ",\"risk\":" +
      json_string(change.risk.name()) +
      "}",
    )
  }
  "{\"from\":" +
  json_string(diff.from_name) +
  ",\"to\":" +
  json_string(diff.to_name) +
  ",\"added\":" +
  diff.added.to_string() +
  ",\"removed\":" +
  diff.removed.to_string() +
  ",\"modified\":" +
  diff.modified.to_string() +
  ",\"unchanged\":" +
  diff.unchanged.to_string() +
  ",\"changes\":[" +
  rows.join(",") +
  "]}"
}

///|
/// Render a migration plan that can be copied into a release checklist.
pub fn migration_plan(diff : KeymapDiff) -> String {
  let lines : Array[String] = [
    "# Keymap migration plan",
    "",
    "From: `" + diff.from_name + "` → `" + diff.to_name + "`",
    "",
    "- added bindings: " + diff.added.to_string(),
    "- removed bindings: " + diff.removed.to_string(),
    "- modified bindings: " + diff.modified.to_string(),
    "- changed contexts: +" +
    diff.context_added.to_string() +
    " / -" +
    diff.context_removed.to_string(),
    "- changed reserved shortcuts: +" +
    diff.reserved_added.to_string() +
    " / -" +
    diff.reserved_removed.to_string(),
    "",
    "## Actions",
  ]
  for change in behavioral_changes(diff) {
    let marker = match change.kind {
      Added => "[ ] add"
      Removed => "[ ] remove"
      Modified => "[ ] review"
      Unchanged => "[x] keep"
    }
    lines.push("- " + marker + " `" + change.id + "`: " + change.summary)
  }
  if behavioral_changes(diff).length() == 0 {
    lines.push("- [x] no behavioral migration required")
  }
  lines.join("\n")
}

///|
/// Apply a diff's additions and modifications to a base keymap.
pub fn apply_keymap_diff(
  base : Keymap,
  target : Keymap,
  include_removed? : Bool = false,
) -> Keymap {
  let diff = diff_keymaps(base, target)
  let result = base.bindings.copy()
  for change in diff.changes {
    match change.kind {
      Added =>
        match change.after {
          Some(binding) => result.push(binding)
          None => ()
        }
      Modified =>
        match change.after {
          Some(binding) =>
            match binding_index({ ..base, bindings: result }, binding.id) {
              Some(index) => result[index] = binding
              None => result.push(binding)
            }
          None => ()
        }
      Removed => if include_removed { () }
      Unchanged => ()
    }
  }
  { ..target, bindings: result }
}