///|
/// Result of normalizing, deduplicating, and aggregating CIDR blocks.
pub struct AggregationReport {
  input_count : Int
  valid_count : Int
  duplicate_count : Int
  contained_count : Int
  merged_pair_count : Int
  blocks : Array[CidrBlock]
  errors : Array[String]
} derive(Debug)

///|
pub fn AggregationReport::input_count(self : AggregationReport) -> Int {
  self.input_count
}

///|
pub fn AggregationReport::valid_count(self : AggregationReport) -> Int {
  self.valid_count
}

///|
pub fn AggregationReport::duplicate_count(self : AggregationReport) -> Int {
  self.duplicate_count
}

///|
pub fn AggregationReport::contained_count(self : AggregationReport) -> Int {
  self.contained_count
}

///|
pub fn AggregationReport::merged_pair_count(self : AggregationReport) -> Int {
  self.merged_pair_count
}

///|
pub fn AggregationReport::blocks(self : AggregationReport) -> Array[CidrBlock] {
  self.blocks
}

///|
pub fn AggregationReport::errors(self : AggregationReport) -> Array[String] {
  self.errors
}

///|
pub fn AggregationReport::output_count(self : AggregationReport) -> Int {
  self.blocks.length()
}

///|
pub fn AggregationReport::changed(self : AggregationReport) -> Bool {
  self.duplicate_count > 0 ||
  self.contained_count > 0 ||
  self.merged_pair_count > 0
}

///|
pub fn AggregationReport::reduction_count(self : AggregationReport) -> Int {
  self.valid_count - self.output_count()
}

///|
pub fn AggregationReport::summary(self : AggregationReport) -> String {
  "input=" +
  self.input_count.to_string() +
  ", valid=" +
  self.valid_count.to_string() +
  ", output=" +
  self.output_count().to_string() +
  ", duplicates=" +
  self.duplicate_count.to_string() +
  ", contained=" +
  self.contained_count.to_string() +
  ", merged_pairs=" +
  self.merged_pair_count.to_string() +
  ", errors=" +
  self.errors.length().to_string()
}

///|
pub fn AggregationReport::text_report(self : AggregationReport) -> String {
  let mut output = "MoonCIDR aggregation report\n"
  output = output + self.summary() + "\n"
  if self.blocks.length() > 0 {
    output = output + "\nAggregated blocks:\n"
    for block in self.blocks {
      output = output + "- " + block.to_string() + "\n"
    }
  }
  if self.errors.length() > 0 {
    output = output + "\nInvalid inputs:\n"
    for error in self.errors {
      output = output + "- " + error + "\n"
    }
  }
  output
}

///|
/// Parses and aggregates a list of CIDR strings.
///
/// Invalid entries are preserved in `errors` rather than aborting the whole
/// operation, making the API suitable for configuration linting.
pub fn aggregate_cidr_strings(inputs : Array[String]) -> AggregationReport {
  let blocks : Array[CidrBlock] = []
  let errors : Array[String] = []
  for index = 0; index < inputs.length(); index = index + 1 {
    match CidrBlock::parse(inputs[index]) {
      Ok(block) => blocks.push(block)
      Err(error) =>
        errors.push(
          "item " +
          (index + 1).to_string() +
          " (" +
          inputs[index] +
          "): " +
          error,
        )
    }
  }
  aggregate_blocks_with_metadata(inputs.length(), blocks, errors)
}

///|
/// Aggregates already parsed CIDR blocks.
pub fn aggregate_blocks(blocks : Array[CidrBlock]) -> AggregationReport {
  aggregate_blocks_with_metadata(blocks.length(), blocks, [])
}

///|
fn aggregate_blocks_with_metadata(
  input_count : Int,
  input_blocks : Array[CidrBlock],
  errors : Array[String],
) -> AggregationReport {
  let sorted = copy_blocks(input_blocks)
  sort_blocks(sorted)
  let unique : Array[CidrBlock] = []
  let mut duplicate_count = 0
  for block in sorted {
    if contains_equal_block(unique, block) {
      duplicate_count = duplicate_count + 1
    } else {
      unique.push(block)
    }
  }
  let reduced : Array[CidrBlock] = []
  let mut contained_count = 0
  for candidate in unique {
    if is_contained_by_any(reduced, candidate) {
      contained_count = contained_count + 1
    } else {
      remove_blocks_contained_by(reduced, candidate)
      reduced.push(candidate)
      sort_blocks(reduced)
    }
  }
  let (aggregated, merged_pair_count) = merge_sibling_blocks(reduced)
  sort_blocks(aggregated)
  {
    input_count,
    valid_count: input_blocks.length(),
    duplicate_count,
    contained_count,
    merged_pair_count,
    blocks: aggregated,
    errors,
  }
}

///|
fn merge_sibling_blocks(input : Array[CidrBlock]) -> (Array[CidrBlock], Int) {
  let mut current = copy_blocks(input)
  let mut merged_count = 0
  let mut keep_merging = true
  while keep_merging {
    keep_merging = false
    let mut left_index = -1
    let mut right_index = -1
    let mut parent = CidrBlock::new_unchecked(IPv4::new(0U), 0)
    for left = 0; left < current.length(); left = left + 1 {
      if left_index != -1 {
        break
      }
      for right = left + 1; right < current.length(); right = right + 1 {
        match sibling_parent(current[left], current[right]) {
          Some(value) => {
            left_index = left
            right_index = right
            parent = value
            break
          }
          None => ()
        }
      }
    }
    if left_index != -1 {
      let next : Array[CidrBlock] = []
      for index = 0; index < current.length(); index = index + 1 {
        if index != left_index && index != right_index {
          next.push(current[index])
        }
      }
      next.push(parent)
      current = remove_contained_blocks(next)
      merged_count = merged_count + 1
      keep_merging = true
    }
  }
  (current, merged_count)
}

///|
fn sibling_parent(left : CidrBlock, right : CidrBlock) -> CidrBlock? {
  if left.prefix() == 0 || left.prefix() != right.prefix() {
    return None
  }
  let parent_prefix = left.prefix() - 1
  let left_parent = CidrBlock::new_unchecked(left.network(), parent_prefix)
  let right_parent = CidrBlock::new_unchecked(right.network(), parent_prefix)
  if left_parent == right_parent && left != right {
    Some(left_parent)
  } else {
    None
  }
}

///|
fn remove_contained_blocks(input : Array[CidrBlock]) -> Array[CidrBlock] {
  let sorted = copy_blocks(input)
  sort_blocks(sorted)
  let output : Array[CidrBlock] = []
  for candidate in sorted {
    if !is_contained_by_any(output, candidate) {
      output.push(candidate)
    }
  }
  output
}

///|
fn is_contained_by_any(
  blocks : Array[CidrBlock],
  candidate : CidrBlock,
) -> Bool {
  for existing in blocks {
    if existing.contains_block(candidate) {
      return true
    }
  }
  false
}

///|
fn remove_blocks_contained_by(
  blocks : Array[CidrBlock],
  parent : CidrBlock,
) -> Unit {
  let kept : Array[CidrBlock] = []
  for existing in blocks {
    if !parent.contains_block(existing) {
      kept.push(existing)
    }
  }
  blocks.clear()
  for existing in kept {
    blocks.push(existing)
  }
}

///|
fn contains_equal_block(
  blocks : Array[CidrBlock],
  candidate : CidrBlock,
) -> Bool {
  for block in blocks {
    if block == candidate {
      return true
    }
  }
  false
}

///|
fn copy_blocks(input : Array[CidrBlock]) -> Array[CidrBlock] {
  let output : Array[CidrBlock] = []
  for block in input {
    output.push(block)
  }
  output
}

///|
fn sort_blocks(blocks : Array[CidrBlock]) -> Unit {
  for index = 0; index < blocks.length(); index = index + 1 {
    let mut smallest = index
    for candidate = index + 1
        candidate < blocks.length()
        candidate = candidate + 1 {
      if block_before(blocks[candidate], blocks[smallest]) {
        smallest = candidate
      }
    }
    if smallest != index {
      let saved = blocks[index]
      blocks[index] = blocks[smallest]
      blocks[smallest] = saved
    }
  }
}

///|
fn block_before(left : CidrBlock, right : CidrBlock) -> Bool {
  if left.network().value() < right.network().value() {
    true
  } else {
    left.network().value() == right.network().value() &&
    left.prefix() < right.prefix()
  }
}