///|
pub(all) struct ZoneChange {
  kind : String
  owner : String
  record_type : String
  rdata : Array[String]
  before_ttl : Int?
  after_ttl : Int?
  line : Int
} derive(Eq, Debug)

///|
pub(all) struct ZoneDiff {
  changes : Array[ZoneChange]
  diagnostics : Array[Diagnostic]
  serial_order : SerialOrder?
} derive(Debug)

///|
fn record_index(zone : Zone) -> Map[String, Array[ResourceRecord]] {
  let result : Map[String, Array[ResourceRecord]] = Map([])
  for record in zone.records {
    let key = data_key(record)
    match result.get(key) {
      Some(records) => records.push(record)
      None => result.set(key, [record])
    }
  }
  result
}

///|
fn to_change(
  kind : String,
  record : ResourceRecord,
  before_ttl : Int?,
  after_ttl : Int?,
) -> ZoneChange {
  {
    kind,
    owner: record.owner,
    record_type: record.record_type,
    rdata: record.rdata,
    before_ttl,
    after_ttl,
    line: record.line,
  }
}

///|
fn collect_removed(
  before : Zone,
  after_index : Map[String, Array[ResourceRecord]],
  changes : Array[ZoneChange],
) -> Unit {
  let positions : Map[String, Int] = Map([])
  for record in before.records {
    let key = data_key(record)
    let position = match positions.get(key) {
      Some(value) => value
      None => 0
    }
    positions.set(key, position + 1)
    match after_index.get(key) {
      Some(matches) =>
        if position >= matches.length() {
          changes.push(to_change("removed", record, Some(record.ttl), None))
        } else if matches[position].ttl != record.ttl {
          changes.push(
            to_change(
              "ttl-changed",
              record,
              Some(record.ttl),
              Some(matches[position].ttl),
            ),
          )
        }
      None => changes.push(to_change("removed", record, Some(record.ttl), None))
    }
  }
}

///|
fn collect_added(
  after : Zone,
  before_index : Map[String, Array[ResourceRecord]],
  changes : Array[ZoneChange],
) -> Unit {
  let positions : Map[String, Int] = Map([])
  for record in after.records {
    let key = data_key(record)
    let position = match positions.get(key) {
      Some(value) => value
      None => 0
    }
    positions.set(key, position + 1)
    match before_index.get(key) {
      Some(matches) =>
        if position >= matches.length() {
          changes.push(to_change("added", record, None, Some(record.ttl)))
        }
      None => changes.push(to_change("added", record, None, Some(record.ttl)))
    }
  }
}

///|
fn serial_diagnostics(
  before : Zone,
  after : Zone,
  changed : Bool,
) -> (Array[Diagnostic], SerialOrder?) {
  let diagnostics : Array[Diagnostic] = []
  match (apex_serial(before), apex_serial(after)) {
    (Some(old_serial), Some(new_serial)) => {
      let order = compare_serial(old_serial, new_serial)
      match order {
        Same =>
          if changed {
            diagnostics.push(
              diagnostic(
                "D300",
                "error",
                "zone content changed without advancing the SOA serial",
                1,
                1,
                owner=after.origin,
              ),
            )
          }
        Forward => ()
        Backward =>
          diagnostics.push(
            diagnostic(
              "D301",
              "error",
              "SOA serial moves backward under RFC 1982 arithmetic",
              1,
              1,
              owner=after.origin,
            ),
          )
        Undefined =>
          diagnostics.push(
            diagnostic(
              "D302",
              "error",
              "SOA serial delta is undefined under RFC 1982 arithmetic",
              1,
              1,
              owner=after.origin,
            ),
          )
      }
      (diagnostics, Some(order))
    }
    _ => {
      diagnostics.push(
        diagnostic(
          "D303",
          "warning",
          "cannot compare SOA serials",
          1,
          1,
          owner=after.origin,
        ),
      )
      (diagnostics, None)
    }
  }
}

///|
/// Compare two versions of one zone as multisets of resource records.
pub fn compare_zones(before : Zone, after : Zone) -> ZoneDiff {
  let changes : Array[ZoneChange] = []
  let diagnostics : Array[Diagnostic] = []
  for item in before.diagnostics {
    diagnostics.push(
      diagnostic(
        "D010",
        "error",
        "before zone has \{item.code}: \{item.message}",
        item.line,
        item.column,
        owner=item.owner,
      ),
    )
  }
  for item in after.diagnostics {
    diagnostics.push(
      diagnostic(
        "D011",
        "error",
        "after zone has \{item.code}: \{item.message}",
        item.line,
        item.column,
        owner=item.owner,
      ),
    )
  }
  if diagnostics.length() > 0 {
    return { changes, diagnostics, serial_order: None, }
  }
  if before.origin != after.origin {
    diagnostics.push(diagnostic("D001", "error", "zone origins differ", 1, 1))
    return { changes, diagnostics, serial_order: None, }
  }
  collect_removed(before, record_index(after), changes)
  collect_added(after, record_index(before), changes)
  let (serial_notes, serial_order) = serial_diagnostics(
    before,
    after,
    changes.length() > 0,
  )
  for item in serial_notes {
    diagnostics.push(item)
  }
  { changes, diagnostics, serial_order, }
}