///|
fn revision_value_equal(left : Value?, right : Value?) -> Bool {
  match (left, right) {
    (None, None) => true
    (Some(a), Some(b)) => a == b
    _ => false
  }
}

///|
fn revision_field_changes_for_entity(
  before : Entity?,
  after : Entity?,
) -> Array[EntityFieldChange] {
  let entity_id = match (before, after) {
    (Some(entity), _) => entity.id
    (_, Some(entity)) => entity.id
    _ => 0
  }
  let before_length = before
    .map(entity => entity.arguments.length())
    .unwrap_or(0)
  let after_length = after.map(entity => entity.arguments.length()).unwrap_or(0)
  let count = if before_length > after_length {
    before_length
  } else {
    after_length
  }
  let result : Array[EntityFieldChange] = []
  for index in 0.. Added
        (Some(_), None) => Removed
        _ => Modified
      }
      result.push({
        entity_id,
        attribute: index,
        before: old_value,
        after: new_value,
        kind,
      })
    }
  }
  result
}

///|
fn revision_entity_value(entity : Entity?, index : Int) -> Value? {
  match entity {
    Some(value) => value.arguments.get(index)
    None => None
  }
}

///|
fn revision_collect_field_changes(diff : ModelDiff) -> Array[EntityFieldChange] {
  diff.changes.fold(init=Array::new(), (result, change) => {
    result.append(
      revision_field_changes_for_entity(change.before, change.after),
    )
    result
  })
}

///|
fn revision_changed_ids(diff : ModelDiff) -> Array[Int] {
  diff.changed_ids()
}

///|
fn revision_products_for_ids(
  model : Model,
  affected : Array[Int],
) -> Array[Int] {
  model
  .products()
  .fold(init=Array::new(), (result, product) => {
    if affected.contains(product.id) {
      result.push(product.id)
    }
    result
  })
}

///|
fn revision_affected_spatial_nodes(
  model : Model,
  affected : Array[Int],
) -> Array[Int] {
  let result : Array[Int] = []
  for root in spatial_roots(model.index()) {
    for node in flatten_spatial(root) {
      if affected.contains(node.id) ||
        node.products.any(id => affected.contains(id)) {
        result.push(node.id)
      }
    }
  }
  result
}

///|
fn revision_risk(diff : ModelDiff, impact : RevisionImpact) -> String {
  if diff.removed > 0 {
    "high"
  } else if impact.affected_products.length() > 10 || diff.modified > 20 {
    "medium"
  } else if diff.changes.length() > 0 {
    "low"
  } else {
    "none"
  }
}

///|
/// Calculate dependency-aware impact for a model revision.
pub fn revision_impact(before : Model, after : Model) -> RevisionImpact {
  let diff = diff_models(before, after)
  let changed_ids = revision_changed_ids(diff)
  let after_graph = entity_graph(after)
  let affected_ids = after_graph.dependents(changed_ids)
  let affected_products = revision_products_for_ids(after, affected_ids)
  let affected_spatial_nodes = revision_affected_spatial_nodes(
    after, affected_ids,
  )
  let field_changes = revision_collect_field_changes(diff)
  let preliminary = {
    changed_ids,
    affected_ids,
    affected_products,
    affected_spatial_nodes,
    field_changes,
    risk: "none",
  }
  { ..preliminary, risk: revision_risk(diff, preliminary) }
}

///|
/// Combine an entity diff with product-level and spatial dependency impact.
pub fn revision_report(before : Model, after : Model) -> RevisionReport {
  let diff = diff_models(before, after)
  let impact = revision_impact(before, after)
  let before_products = before.products()
  let after_products = after.products()
  let added_products : Array[Int] = []
  let removed_products : Array[Int] = []
  let modified_products : Array[Int] = []
  for product in after_products {
    match product_by_id(before_products, product.id) {
      None => added_products.push(product.id)
      Some(old) if old.ifc_type != product.ifc_type ||
        old.name != product.name ||
        old.representation != product.representation =>
        modified_products.push(product.id)
      _ => ()
    }
  }
  for product in before_products {
    if product_by_id(after_products, product.id) is None {
      removed_products.push(product.id)
    }
  }
  { diff, impact, added_products, removed_products, modified_products }
}

///|
pub fn compare_revision(before : Model, after : Model) -> RevisionReport {
  revision_report(before, after)
}

///|
pub fn RevisionImpact::is_empty(self : RevisionImpact) -> Bool {
  self.changed_ids.length() == 0
}

///|
pub fn RevisionImpact::affected_count(self : RevisionImpact) -> Int {
  self.affected_ids.length()
}

///|
pub fn RevisionImpact::field_change_count(self : RevisionImpact) -> Int {
  self.field_changes.length()
}

///|
pub fn RevisionImpact::contains_product(
  self : RevisionImpact,
  id : Int,
) -> Bool {
  self.affected_products.contains(id)
}

///|
pub fn RevisionImpact::headline(self : RevisionImpact) -> String {
  "changed=" +
  self.changed_ids.length().to_string() +
  " affected=" +
  self.affected_ids.length().to_string() +
  " products=" +
  self.affected_products.length().to_string() +
  " spatial_nodes=" +
  self.affected_spatial_nodes.length().to_string() +
  " fields=" +
  self.field_changes.length().to_string() +
  " risk=" +
  self.risk
}

///|
pub fn EntityFieldChange::headline(self : EntityFieldChange) -> String {
  let kind = match self.kind {
    Added => "added"
    Removed => "removed"
    Modified => "modified"
  }
  "#" +
  self.entity_id.to_string() +
  "[" +
  self.attribute.to_string() +
  "] " +
  kind
}

///|
pub fn RevisionReport::is_empty(self : RevisionReport) -> Bool {
  self.diff.is_empty()
}

///|
pub fn RevisionReport::change_count(self : RevisionReport) -> Int {
  self.diff.changes.length()
}

///|
pub fn RevisionReport::impact_count(self : RevisionReport) -> Int {
  self.impact.affected_ids.length()
}

///|
pub fn RevisionReport::risk(self : RevisionReport) -> String {
  self.impact.risk
}

///|
pub fn RevisionReport::headline(self : RevisionReport) -> String {
  "changes=" +
  self.change_count().to_string() +
  " added_products=" +
  self.added_products.length().to_string() +
  " removed_products=" +
  self.removed_products.length().to_string() +
  " modified_products=" +
  self.modified_products.length().to_string() +
  " affected=" +
  self.impact_count().to_string() +
  " risk=" +
  self.risk()
}

///|
pub fn RevisionReport::changed_product_ids(self : RevisionReport) -> Array[Int] {
  let result = self.added_products.copy()
  for id in self.removed_products + self.modified_products {
    if !result.contains(id) {
      result.push(id)
    }
  }
  result
}

///|
pub fn RevisionReport::field_changes_for(
  self : RevisionReport,
  id : Int,
) -> Array[EntityFieldChange] {
  self.impact.field_changes.filter(change => change.entity_id == id)
}

///|
pub fn RevisionReport::to_json(self : RevisionReport) -> String {
  let fields = self.impact.field_changes
    .map(change => {
      let kind = match change.kind {
        Added => "added"
        Removed => "removed"
        Modified => "modified"
      }
      "{\"entity_id\":" +
      change.entity_id.to_string() +
      ",\"attribute\":" +
      change.attribute.to_string() +
      ",\"kind\":\"" +
      kind +
      "\"}"
    })
    .join(",")
  "{\"risk\":\"" +
  self.risk() +
  "\",\"added\":[" +
  self.added_products.map(id => id.to_string()).join(",") +
  "],\"removed\":[" +
  self.removed_products.map(id => id.to_string()).join(",") +
  "],\"modified\":[" +
  self.modified_products.map(id => id.to_string()).join(",") +
  "],\"affected\":[" +
  self.impact.affected_ids.map(id => id.to_string()).join(",") +
  "],\"field_changes\":[" +
  fields +
  "]}"
}

///|
pub fn RevisionReport::to_markdown(self : RevisionReport) -> String {
  let builder = StringBuilder()
  builder <+ "# Revision impact\n\n"
  builder <+ "- Changes: \{self.change_count()}\n"
  builder <+ "- Added products: \{self.added_products.length()}\n"
  builder <+ "- Removed products: \{self.removed_products.length()}\n"
  builder <+ "- Modified products: \{self.modified_products.length()}\n"
  builder <+ "- Affected entities: \{self.impact.affected_ids.length()}\n"
  builder <+ "- Risk: \{self.risk()}\n\n"
  if self.impact.field_changes.length() > 0 {
    builder <+ "| Entity | Attribute | Kind |\n"
    builder <+ "| ---: | ---: | --- |\n"
    for change in self.impact.field_changes {
      let kind = match change.kind {
        Added => "added"
        Removed => "removed"
        Modified => "modified"
      }
      builder <+ "| \{change.entity_id} | \{change.attribute} | \{kind} |\n"
    }
  }
  builder.to_string()
}

///|
pub fn revision_changed_entity_ids(before : Model, after : Model) -> Array[Int] {
  diff_models(before, after).changed_ids()
}

///|
pub fn revision_affected_entity_ids(
  before : Model,
  after : Model,
) -> Array[Int] {
  revision_impact(before, after).affected_ids
}

///|
pub fn revision_affected_products(before : Model, after : Model) -> Array[Int] {
  revision_impact(before, after).affected_products
}

///|
pub fn revision_risk_level(before : Model, after : Model) -> String {
  revision_report(before, after).risk()
}