///|
/// Diff two package-level snapshots.
pub fn diff_api_snapshots(
  old_snapshot : ApiSnapshot,
  new_snapshot : ApiSnapshot,
) -> ApiReport {
  diff_items(old_snapshot.items, new_snapshot.items)
}

///|
/// Compare two package-level snapshots and keep diagnostics beside the report.
pub fn compare_api_snapshots(
  old_snapshot : ApiSnapshot,
  new_snapshot : ApiSnapshot,
) -> ApiPackageComparison {
  let diagnostics = merge_diagnostics(
    old_snapshot.diagnostics,
    new_snapshot.diagnostics,
  )
  let report = diff_api_snapshots(old_snapshot, new_snapshot)
  {
    old_snapshot,
    new_snapshot,
    report,
    diagnostics,
    summary: summarize_report(report),
    diagnostic_summary: summarize_diagnostics(diagnostics),
  }
}

///|
/// Build snapshots from file sets and compare them as one package.
pub fn compare_api_file_sets(
  old_files : Array[ApiFile],
  new_files : Array[ApiFile],
) -> ApiPackageComparison {
  compare_api_snapshots(
    build_api_snapshot(old_files),
    build_api_snapshot(new_files),
  )
}

///|
/// Compare two MoonBit public API snapshots.
pub fn diff_interfaces(old_text : String, new_text : String) -> ApiReport {
  diff_items(parse_interface(old_text), parse_interface(new_text))
}

///|
/// Compare two normalized API lists.
pub fn diff_items(
  old_items : Array[ApiItem],
  new_items : Array[ApiItem],
) -> ApiReport {
  let changes : Array[ApiChange] = []
  for old_item in old_items {
    match find_item(new_items, old_item.kind, old_item.name) {
      Some(new_item) =>
        if old_item.signature != new_item.signature {
          changes.push({
            kind: Changed,
            impact: Major,
            item_kind: old_item.kind,
            name: old_item.name,
            old_signature: Some(old_item.signature),
            new_signature: Some(new_item.signature),
          })
        }
      None =>
        match find_kind_changed_item(new_items, old_item) {
          Some(new_item) =>
            changes.push({
              kind: Changed,
              impact: Major,
              item_kind: "\{old_item.kind}->\{new_item.kind}",
              name: old_item.name,
              old_signature: Some("\{old_item.kind} \{old_item.signature}"),
              new_signature: Some("\{new_item.kind} \{new_item.signature}"),
            })
          None =>
            if should_report_nested_change(new_items, old_item) {
              changes.push({
                kind: Removed,
                impact: Major,
                item_kind: old_item.kind,
                name: old_item.name,
                old_signature: Some(old_item.signature),
                new_signature: None,
              })
            }
        }
    }
  }
  for new_item in new_items {
    match find_item(old_items, new_item.kind, new_item.name) {
      Some(_) => ()
      None =>
        match find_kind_changed_item(old_items, new_item) {
          Some(_) => ()
          None =>
            if should_report_nested_change(old_items, new_item) {
              changes.push({
                kind: Added,
                impact: added_impact(new_item.kind),
                item_kind: new_item.kind,
                name: new_item.name,
                old_signature: None,
                new_signature: Some(new_item.signature),
              })
            }
        }
    }
  }
  sort_changes(changes)
  { changes, recommendation: recommended_impact(changes) }
}

///|
/// Count change kinds and impact levels in a report.
pub fn summarize_report(report : ApiReport) -> ApiSummary {
  let mut major = 0
  let mut minor = 0
  let mut patch = 0
  let mut added = 0
  let mut removed = 0
  let mut changed = 0
  for change in report.changes {
    match change.impact {
      Major => major += 1
      Minor => minor += 1
      Patch => patch += 1
    }
    match change.kind {
      Added => added += 1
      Removed => removed += 1
      Changed => changed += 1
    }
  }
  {
    total: report.changes.length(),
    major,
    minor,
    patch,
    added,
    removed,
    changed,
  }
}

///|
/// Return true when diagnostics include at least one error.
pub fn diagnostics_have_errors(diagnostics : Array[ApiDiagnostic]) -> Bool {
  for diagnostic in diagnostics {
    if diagnostic.severity == "error" {
      return true
    }
  }
  false
}

///|
/// Count diagnostics by severity.
pub fn summarize_diagnostics(
  diagnostics : Array[ApiDiagnostic],
) -> DiagnosticSummary {
  let mut errors = 0
  let mut warnings = 0
  let mut infos = 0
  for diagnostic in diagnostics {
    if diagnostic.severity == "error" {
      errors += 1
    } else if diagnostic.severity == "warning" {
      warnings += 1
    } else if diagnostic.severity == "info" {
      infos += 1
    }
  }
  { total: diagnostics.length(), errors, warnings, infos }
}

///|
/// Count API items by kind for package-level diagnostics and reporting.
pub fn count_items_by_kind(items : Array[ApiItem]) -> Array[ApiKindCount] {
  let counts : Array[ApiKindCount] = []
  for item in items {
    match find_kind_count_index(counts, item.kind) {
      Some(index) => {
        let old = counts[index]
        counts[index] = { kind: old.kind, count: old.count + 1 }
      }
      None => counts.push({ kind: item.kind, count: 1 })
    }
  }
  sort_kind_counts(counts)
  counts
}

///|
/// Merge diagnostics from two snapshots.
pub fn merge_diagnostics(
  left : Array[ApiDiagnostic],
  right : Array[ApiDiagnostic],
) -> Array[ApiDiagnostic] {
  let diagnostics : Array[ApiDiagnostic] = []
  for diagnostic in left {
    diagnostics.push(diagnostic)
  }
  for diagnostic in right {
    diagnostics.push(diagnostic)
  }
  diagnostics
}

///|
fn added_impact(kind : String) -> Impact {
  if kind == "field" || kind == "constructor" || kind == "trait-method" {
    Major
  } else {
    Minor
  }
}

///|
fn should_report_nested_change(
  opposite_items : Array[ApiItem],
  item : ApiItem,
) -> Bool {
  if !is_nested_kind(item.kind) {
    return true
  }
  for candidate in opposite_items {
    if is_container_kind(candidate.kind) &&
      item.name.has_prefix("\{candidate.name}.") {
      return true
    }
  }
  false
}

///|
fn is_nested_kind(kind : String) -> Bool {
  kind == "field" ||
  kind == "constructor" ||
  kind == "trait-method" ||
  kind == "derive"
}

///|
fn is_container_kind(kind : String) -> Bool {
  kind == "struct" || kind == "enum" || kind == "trait" || kind == "suberror"
}

///|
fn find_item(items : Array[ApiItem], kind : String, name : String) -> ApiItem? {
  for item in items {
    if item.kind == kind && item.name == name {
      return Some(item)
    }
  }
  None
}

///|
fn find_kind_changed_item(items : Array[ApiItem], item : ApiItem) -> ApiItem? {
  if !is_top_level_kind(item.kind) {
    return None
  }
  for candidate in items {
    if candidate.name == item.name &&
      candidate.kind != item.kind &&
      is_top_level_kind(candidate.kind) {
      return Some(candidate)
    }
  }
  None
}

///|
fn is_top_level_kind(kind : String) -> Bool {
  kind == "fn" ||
  kind == "type" ||
  kind == "typealias" ||
  kind == "struct" ||
  kind == "enum" ||
  kind == "trait" ||
  kind == "impl" ||
  kind == "let" ||
  kind == "const" ||
  kind == "suberror" ||
  kind == "using"
}

///|
fn recommended_impact(changes : Array[ApiChange]) -> Impact {
  let mut saw_minor = false
  for change in changes {
    match change.impact {
      Major => return Major
      Minor => saw_minor = true
      Patch => ()
    }
  }
  if saw_minor {
    Minor
  } else {
    Patch
  }
}

///|
fn impact_rank(impact : Impact) -> Int {
  match impact {
    Patch => 0
    Minor => 1
    Major => 2
  }
}

///|
fn change_kind_rank(kind : ChangeKind) -> Int {
  match kind {
    Removed => 0
    Changed => 1
    Added => 2
  }
}

///|
fn sort_items(items : Array[ApiItem]) -> Unit {
  items.sort_by((a, b) => compare_item_key(a, b))
}

///|
fn sort_changes(changes : Array[ApiChange]) -> Unit {
  changes.sort_by((a, b) => compare_change_key(a, b))
}

///|
fn sort_kind_counts(counts : Array[ApiKindCount]) -> Unit {
  counts.sort_by((a, b) => a.kind.lexical_compare(b.kind))
}

///|
fn find_kind_count_index(counts : Array[ApiKindCount], kind : String) -> Int? {
  for i in 0.. Int {
  let kind_cmp = a.kind.lexical_compare(b.kind)
  if kind_cmp != 0 {
    kind_cmp
  } else {
    a.name.lexical_compare(b.name)
  }
}

///|
fn compare_change_key(a : ApiChange, b : ApiChange) -> Int {
  let impact_cmp = impact_rank(b.impact) - impact_rank(a.impact)
  if impact_cmp != 0 {
    impact_cmp
  } else {
    let kind_cmp = change_kind_rank(a.kind) - change_kind_rank(b.kind)
    if kind_cmp != 0 {
      kind_cmp
    } else {
      let item_cmp = a.item_kind.lexical_compare(b.item_kind)
      if item_cmp != 0 {
        item_cmp
      } else {
        a.name.lexical_compare(b.name)
      }
    }
  }
}

///|
fn impact_label(impact : Impact) -> String {
  match impact {
    Patch => "patch"
    Minor => "minor"
    Major => "major"
  }
}

///|
fn change_kind_label(kind : ChangeKind) -> String {
  match kind {
    Added => "added"
    Removed => "removed"
    Changed => "changed"
  }
}

///|
fn change_details(change : ApiChange) -> String {
  match change.kind {
    Added =>
      match change.new_signature {
        Some(signature) => "`\{escape_markdown_cell(signature)}`"
        None => ""
      }
    Removed =>
      match change.old_signature {
        Some(signature) => "`\{escape_markdown_cell(signature)}`"
        None => ""
      }
    Changed => {
      let old_text = match change.old_signature {
        Some(signature) => escape_markdown_cell(signature)
        None => ""
      }
      let new_text = match change.new_signature {
        Some(signature) => escape_markdown_cell(signature)
        None => ""
      }
      "`\{old_text}` -> `\{new_text}`"
    }
  }
}