///|
/// Change classification for a single trace entity between two snapshots.
pub(all) enum ChangeKind {
  Added
  Removed
  Modified
} derive(Eq, Debug)

///|
/// Impact is deliberately conservative. Removing a promised capability or a
/// required behavior is treated as breaking even when source compilation still
/// succeeds.
pub(all) enum ChangeImpact {
  Breaking
  Review
  Informational
} derive(Eq, Debug)

///|
pub(all) struct EntityChange {
  category : String
  id : String
  kind : ChangeKind
  impact : ChangeImpact
  before : String
  after : String
} derive(Eq, Debug)

///|
pub(all) struct SummaryChange {
  field : String
  before : String
  after : String
  impact : ChangeImpact
} derive(Eq, Debug)

///|
pub(all) struct SpecDelta {
  before_project : String
  after_project : String
  summary_changes : Array[SummaryChange]
  capability_changes : Array[EntityChange]
  requirement_changes : Array[EntityChange]
  evidence_changes : Array[EntityChange]
} derive(Eq, Debug)

///|
pub fn ChangeKind::label(self : ChangeKind) -> String {
  match self {
    Added => "added"
    Removed => "removed"
    Modified => "modified"
  }
}

///|
pub fn ChangeImpact::label(self : ChangeImpact) -> String {
  match self {
    Breaking => "breaking"
    Review => "review"
    Informational => "informational"
  }
}

///|
/// Compare two parsed specifications by stable entity identifier. The result
/// contains only changes, so an empty delta is convenient for CI gates.
pub fn compare_specs(before : BoundarySpec, after : BoundarySpec) -> SpecDelta {
  {
    before_project: before.project,
    after_project: after.project,
    summary_changes: compare_summary(before, after),
    capability_changes: compare_capabilities(
      before.capabilities,
      after.capabilities,
    ),
    requirement_changes: compare_requirements(
      before.requirements,
      after.requirements,
    ),
    evidence_changes: compare_evidence(before.evidence, after.evidence),
  }
}

///|
pub fn compare_manifests(before : StringView, after : StringView) -> SpecDelta {
  compare_specs(parse_manifest(before), parse_manifest(after))
}

///|
pub fn SpecDelta::is_empty(self : SpecDelta) -> Bool {
  self.summary_changes.is_empty() &&
  self.capability_changes.is_empty() &&
  self.requirement_changes.is_empty() &&
  self.evidence_changes.is_empty()
}

///|
pub fn SpecDelta::all_entity_changes(self : SpecDelta) -> Array[EntityChange] {
  let changes : Array[EntityChange] = []
  for change in self.capability_changes {
    changes.push(change)
  }
  for change in self.requirement_changes {
    changes.push(change)
  }
  for change in self.evidence_changes {
    changes.push(change)
  }
  changes
}

///|
pub fn SpecDelta::breaking_changes(self : SpecDelta) -> Array[EntityChange] {
  self.all_entity_changes().filter(change => change.impact == Breaking)
}

///|
pub fn SpecDelta::requires_review(self : SpecDelta) -> Bool {
  !self.is_empty()
}

///|
pub fn SpecDelta::has_breaking_change(self : SpecDelta) -> Bool {
  !self.breaking_changes().is_empty() ||
  self.summary_changes.any(change => change.impact == Breaking)
}

///|
pub fn SpecDelta::change_count(self : SpecDelta) -> Int {
  self.summary_changes.length() +
  self.capability_changes.length() +
  self.requirement_changes.length() +
  self.evidence_changes.length()
}

///|
pub fn SpecDelta::to_markdown(self : SpecDelta) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Change Review")
  lines.push("")
  lines.push("- Before: " + printable(self.before_project))
  lines.push("- After: " + printable(self.after_project))
  lines.push("- Changes: " + self.change_count().to_string())
  lines.push("- Breaking: " + self.breaking_changes().length().to_string())
  lines.push("- Review required: " + bool_label(self.requires_review()))
  lines.push("")
  render_summary_change_table(lines, self.summary_changes)
  render_entity_change_table(lines, "Capabilities", self.capability_changes)
  render_entity_change_table(lines, "Requirements", self.requirement_changes)
  render_entity_change_table(lines, "Evidence", self.evidence_changes)
  lines.join("\n")
}

///|
pub fn SpecDelta::to_json_string(self : SpecDelta) -> String {
  Json::object(
    Map([
      ("before_project", Json::string(self.before_project)),
      ("after_project", Json::string(self.after_project)),
      (
        "change_count",
        Json::number(
          self.change_count().to_double(),
          repr=self.change_count().to_string(),
        ),
      ),
      ("has_breaking_change", Json::boolean(self.has_breaking_change())),
      ("summary_changes", summary_changes_to_json(self.summary_changes)),
      ("capability_changes", entity_changes_to_json(self.capability_changes)),
      ("requirement_changes", entity_changes_to_json(self.requirement_changes)),
      ("evidence_changes", entity_changes_to_json(self.evidence_changes)),
    ]),
  ).stringify(indent=2)
}

///|
fn compare_summary(
  before : BoundarySpec,
  after : BoundarySpec,
) -> Array[SummaryChange] {
  let changes : Array[SummaryChange] = []
  add_summary_change(
    changes,
    "project",
    before.project,
    after.project,
    if before.project != after.project {
      Breaking
    } else {
      Informational
    },
  )
  add_summary_change(changes, "purpose", before.purpose, after.purpose, Review)
  add_summary_change(
    changes,
    "scope",
    before.scopes.join(" | "),
    after.scopes.join(" | "),
    Review,
  )
  add_summary_change(
    changes,
    "out-of-scope",
    before.out_of_scope.join(" | "),
    after.out_of_scope.join(" | "),
    Review,
  )
  changes
}

///|
fn compare_capabilities(
  before : Array[Capability],
  after : Array[Capability],
) -> Array[EntityChange] {
  let changes : Array[EntityChange] = []
  for item in before {
    match find_capability(after, item.id) {
      Some(current) =>
        if item != current {
          changes.push({
            category: "capability",
            id: item.id,
            kind: Modified,
            impact: capability_impact(item, current),
            before: capability_to_manifest_line(item),
            after: capability_to_manifest_line(current),
          })
        }
      None =>
        changes.push({
          category: "capability",
          id: item.id,
          kind: Removed,
          impact: Breaking,
          before: capability_to_manifest_line(item),
          after: "",
        })
    }
  }
  for item in after {
    if find_capability(before, item.id) is None {
      changes.push({
        category: "capability",
        id: item.id,
        kind: Added,
        impact: Informational,
        before: "",
        after: capability_to_manifest_line(item),
      })
    }
  }
  sort_entity_changes(changes)
}

///|
fn compare_requirements(
  before : Array[Requirement],
  after : Array[Requirement],
) -> Array[EntityChange] {
  let changes : Array[EntityChange] = []
  for item in before {
    match find_requirement(after, item.id) {
      Some(current) =>
        if item != current {
          changes.push({
            category: "requirement",
            id: item.id,
            kind: Modified,
            impact: requirement_impact(item, current),
            before: requirement_to_manifest_line(item),
            after: requirement_to_manifest_line(current),
          })
        }
      None =>
        changes.push({
          category: "requirement",
          id: item.id,
          kind: Removed,
          impact: if item.level == Must {
            Breaking
          } else {
            Review
          },
          before: requirement_to_manifest_line(item),
          after: "",
        })
    }
  }
  for item in after {
    if find_requirement(before, item.id) is None {
      changes.push({
        category: "requirement",
        id: item.id,
        kind: Added,
        impact: Informational,
        before: "",
        after: requirement_to_manifest_line(item),
      })
    }
  }
  sort_entity_changes(changes)
}

///|
fn compare_evidence(
  before : Array[Evidence],
  after : Array[Evidence],
) -> Array[EntityChange] {
  let changes : Array[EntityChange] = []
  for item in before {
    match find_evidence(after, item.id) {
      Some(current) =>
        if item != current {
          changes.push({
            category: "evidence",
            id: item.id,
            kind: Modified,
            impact: evidence_impact(item, current),
            before: evidence_to_manifest_line(item),
            after: evidence_to_manifest_line(current),
          })
        }
      None =>
        changes.push({
          category: "evidence",
          id: item.id,
          kind: Removed,
          impact: if item.status == Verified {
            Review
          } else {
            Informational
          },
          before: evidence_to_manifest_line(item),
          after: "",
        })
    }
  }
  for item in after {
    if find_evidence(before, item.id) is None {
      changes.push({
        category: "evidence",
        id: item.id,
        kind: Added,
        impact: Informational,
        before: "",
        after: evidence_to_manifest_line(item),
      })
    }
  }
  sort_entity_changes(changes)
}

///|
fn capability_impact(before : Capability, after : Capability) -> ChangeImpact {
  if before.inputs != after.inputs || before.outputs != after.outputs {
    Breaking
  } else {
    Review
  }
}

///|
fn requirement_impact(
  before : Requirement,
  after : Requirement,
) -> ChangeImpact {
  if before.level == Must && after.level != Must {
    Breaking
  } else if before.capabilities != after.capabilities {
    Breaking
  } else {
    Review
  }
}

///|
fn evidence_impact(before : Evidence, after : Evidence) -> ChangeImpact {
  if before.status == Verified && after.status != Verified {
    Breaking
  } else {
    Review
  }
}

///|
fn find_capability(items : Array[Capability], id : String) -> Capability? {
  items.iter().find_first(item => item.id == id)
}

///|
fn sort_entity_changes(values : Array[EntityChange]) -> Array[EntityChange] {
  let mut sorted : Array[EntityChange] = []
  for value in values {
    let next : Array[EntityChange] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted && value.id.lexical_compare(existing.id) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn find_requirement(items : Array[Requirement], id : String) -> Requirement? {
  items.iter().find_first(item => item.id == id)
}

///|
fn find_evidence(items : Array[Evidence], id : String) -> Evidence? {
  items.iter().find_first(item => item.id == id)
}

///|
fn add_summary_change(
  changes : Array[SummaryChange],
  field : String,
  before : String,
  after : String,
  impact : ChangeImpact,
) -> Unit {
  if before != after {
    changes.push({ field, before, after, impact })
  }
}

///|
fn render_summary_change_table(
  lines : Array[String],
  changes : Array[SummaryChange],
) -> Unit {
  lines.push("## Summary")
  lines.push("")
  lines.push("| Field | Impact | Before | After |")
  lines.push("| --- | --- | --- | --- |")
  if changes.is_empty() {
    lines.push("| info | informational | unchanged | unchanged |")
  } else {
    for change in changes {
      lines.push(
        "| " +
        markdown_cell(change.field) +
        " | " +
        change.impact.label() +
        " | " +
        markdown_cell(change.before) +
        " | " +
        markdown_cell(change.after) +
        " |",
      )
    }
  }
  lines.push("")
}

///|
fn render_entity_change_table(
  lines : Array[String],
  title : String,
  changes : Array[EntityChange],
) -> Unit {
  lines.push("## " + title)
  lines.push("")
  lines.push("| ID | Change | Impact | Before | After |")
  lines.push("| --- | --- | --- | --- | --- |")
  if changes.is_empty() {
    lines.push("| - | unchanged | informational | - | - |")
  } else {
    for change in changes {
      lines.push(
        "| " +
        markdown_cell(change.id) +
        " | " +
        change.kind.label() +
        " | " +
        change.impact.label() +
        " | " +
        markdown_cell(change.before) +
        " | " +
        markdown_cell(change.after) +
        " |",
      )
    }
  }
  lines.push("")
}

///|
fn summary_changes_to_json(items : Array[SummaryChange]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("field", Json::string(item.field)),
          ("before", Json::string(item.before)),
          ("after", Json::string(item.after)),
          ("impact", Json::string(item.impact.label())),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn entity_changes_to_json(items : Array[EntityChange]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("category", Json::string(item.category)),
          ("id", Json::string(item.id)),
          ("kind", Json::string(item.kind.label())),
          ("impact", Json::string(item.impact.label())),
          ("before", Json::string(item.before)),
          ("after", Json::string(item.after)),
        ]),
      ),
    )
  }
  Json::array(values)
}