///|
fn rank(severity : String) -> Int32 {
  match severity {
    "ERROR" => 4
    "DANGER" => 3
    "WARNING" => 2
    "NOTE" => 1
    _ => 0
  }
}

///|
fn member_target_changes(
  d : Differences,
  events : Array[ValidationEvent],
  context : ComparisonContext,
) -> Unit {
  for change in d.changed {
    if change.old_shape.body is Member(target=old_id) &&
      change.new_shape.body is Member(target=new_id) &&
      old_id != new_id {
      events.push(member_target_event(d, change, Set([]), context))
    }
  }
}

///|
fn member_target_event(
  d : Differences,
  change : ShapeChange,
  active : Set[(@smithy.ShapeId, @smithy.ShapeId)],
  context : ComparisonContext,
) -> ValidationEvent {
  let a = change.old_shape
  let b = change.new_shape
  let old_target = target(d.old_model, a)
  let new_target = target(d.new_model, b)
  let old_id = match a.body {
    Member(target~) => target
    _ => a.id
  }
  let new_id = match b.body {
    Member(target~) => target
    _ => b.id
  }
  let (severity, explanation) = target_evaluation(
    d, old_target, new_target, active, context,
  )
  {
    id: "ChangedMemberTarget",
    severity,
    shape: Some(b.id),
    source: Some(b.source),
    suppression_reason: None,
    message: "The shape targeted by the member " +
    tick(a.id.to_string()) +
    " changed from " +
    tick(old_id.to_string()) +
    " (" +
    old_target.map(kind).unwrap_or("unknown") +
    ") to " +
    tick(new_id.to_string()) +
    " (" +
    new_target.map(kind).unwrap_or("unknown") +
    "). " +
    explanation,
  }
}

///|
fn target_evaluation(
  d : Differences,
  old_shape : @smithy.Shape?,
  new_shape : @smithy.Shape?,
  active : Set[(@smithy.ShapeId, @smithy.ShapeId)],
  context : ComparisonContext,
) -> (String, String) {
  guard old_shape is Some(a) && new_shape is Some(b) else {
    return ("WARNING", "This was determined backward compatible.")
  }
  if kind(a) != kind(b) {
    return (
      "ERROR",
      "The type of the targeted shape changed from " +
      kind(a) +
      " to " +
      kind(b) +
      ".",
    )
  }
  match a.body {
    Simple(_) | List(_) | Map(..) => ()
    _ => return ("ERROR", "The name of a " + kind(a) + " is significant.")
  }
  if has(a, "enum") {
    return (
      "ERROR", "The `smithy.api#enum` trait was found on the target, so the name of the targeted shape matters for codegen.",
    )
  }
  // Assembled Smithy models cannot have a cycle made solely from collections.
  // Keep a pair guard for callers that construct models directly.
  let pair = (a.id, b.id)
  if active.contains(pair) {
    return ("WARNING", "This was determined backward compatible.")
  }
  active.add(pair)
  let changes = [shape_change(a, b)]
  let old_members = members(a)
  let new_members = members(b)
  for i = 0; i < old_members.length() && i < new_members.length(); i = i + 1 {
    if d.old_model.shapes.get(old_members[i]) is Some(old_member) &&
      d.new_model.shapes.get(new_members[i]) is Some(new_member) {
      changes.push(shape_change(old_member, new_member))
    }
  }
  let synthetic = {
    old_model: d.old_model,
    new_model: d.new_model,
    added: [],
    removed: [],
    changed: changes,
    metadata: [],
  }
  let nested = evaluate(synthetic, context, include_targets=false)
  for change in changes {
    if change.old_shape.body is Member(target=old_id) &&
      change.new_shape.body is Member(target=new_id) &&
      old_id != new_id {
      nested.push(member_target_event(d, change, active, context))
    }
  }
  active.remove(pair)
  let decorated = decorate(synthetic, nested, context)
  if decorated.is_empty() {
    return ("WARNING", "This was determined backward compatible.")
  }
  let mut severity = "WARNING"
  for event in decorated {
    if rank(event.severity) > rank(severity) {
      severity = event.severity
    }
  }
  let details = decorated
    .map(event => {
      "- [" +
      event.severity +
      "] " +
      event.message.replace_all(old="\n", new="\n  ")
    })
    .join("\n")
    .trim_end()
    .to_owned()
  (
    severity,
    (if rank(severity) <= 2 {
      "This was determined backward compatible. "
    } else {
      ""
    }) +
    "This will result in the following effective differences:\n\n" +
    details,
  )
}