///|
pub struct ZoneSelection {
  document_value : ZoneDocument
  selected_count_value : Int
  excluded_count_value : Int
} derive(Eq, Debug)

///|
pub struct ZoneTtlRewrite {
  document_value : ZoneDocument
  changed_count_value : Int
  unchanged_count_value : Int
} derive(Eq, Debug)

///|
fn transformation_error(message : String) -> ZoneError {
  ZoneError::new(IntegrityViolation, message, SourceSpan::point(0, 0))
}

///|
fn transformed_document(
  source : ZoneDocument,
  records : Array[ZoneRecord],
) -> ZoneDocument {
  {
    record_values: records,
    origin_value: source.origin(),
    default_ttl_value: source.default_ttl(),
    directive_count_value: source.directive_count(),
  }
}

///|
fn domain_is_at_or_below(owner : DomainName, suffix : DomainName) -> Bool {
  if !owner.is_absolute() || !suffix.is_absolute() {
    return false
  }
  let owner_labels = owner.labels()
  let suffix_labels = suffix.labels()
  if owner_labels.length() < suffix_labels.length() {
    return false
  }
  let offset = owner_labels.length() - suffix_labels.length()
  for index, label in suffix_labels {
    if owner_labels[offset + index] != label {
      return false
    }
  }
  true
}

///|
fn type_filter_contains(types : Array[String], record_type : String) -> Bool {
  if types.length() == 0 {
    return true
  }
  for item in types {
    if item == record_type {
      return true
    }
  }
  false
}

///|
fn normalized_types_contain(
  types : Array[String],
  record_type : String,
) -> Bool {
  for item in types {
    if item == record_type {
      return true
    }
  }
  false
}

///|
fn normalize_type_filter(
  types : Array[String],
) -> Result[Array[String], ZoneError] {
  let normalized : Array[String] = []
  for value in types {
    if !valid_generic_type(value) {
      return Err(
        transformation_error(
          "record type filters must use visible DNS type characters",
        ),
      )
    }
    let expected = uppercase_ascii(value)
    if !normalized_types_contain(normalized, expected) {
      normalized.push(expected)
    }
  }
  Ok(normalized)
}

///|
/// Select records by an optional absolute owner subtree and type allow-list.
pub fn select_zone_records(
  zone : ZoneDocument,
  owner_subtree? : DomainName? = None,
  record_types? : Array[String] = [],
) -> Result[ZoneSelection, ZoneError] {
  match owner_subtree {
    Some(value) if !value.is_absolute() =>
      return Err(
        transformation_error("record selection subtree must be absolute"),
      )
    _ => ()
  }
  let types = match normalize_type_filter(record_types) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let selected : Array[ZoneRecord] = []
  let mut excluded = 0
  for record in zone.records() {
    let owner_matches = match owner_subtree {
      Some(suffix) => domain_is_at_or_below(record.owner(), suffix)
      None => true
    }
    if owner_matches && type_filter_contains(types, record.record_type()) {
      selected.push(record)
    } else {
      excluded = excluded + 1
    }
  }
  Ok({
    document_value: transformed_document(zone, selected),
    selected_count_value: selected.length(),
    excluded_count_value: excluded,
  })
}

///|
fn rewrite_record_ttl(record : ZoneRecord, ttl : Ttl) -> ZoneRecord {
  {
    owner_value: record.owner(),
    ttl_value: ttl,
    class_value: record.record_class(),
    type_value: record.record_type(),
    data_value: record.data(),
    explicit_ttl_value: true,
    span_value: record.span(),
  }
}

///|
fn bounded_ttl(ttl : Ttl, minimum : Ttl?, maximum : Ttl?) -> Ttl {
  match minimum {
    Some(value) if ttl.seconds() < value.seconds() => return value
    _ => ()
  }
  match maximum {
    Some(value) if ttl.seconds() > value.seconds() => return value
    _ => ()
  }
  ttl
}

///|
/// Clamp record TTLs while preserving owner, class, type and record data.
pub fn clamp_zone_ttls(
  zone : ZoneDocument,
  minimum? : Ttl? = None,
  maximum? : Ttl? = None,
) -> Result[ZoneTtlRewrite, ZoneError] {
  match (minimum, maximum) {
    (Some(lower), Some(upper)) if lower.seconds() > upper.seconds() =>
      return Err(transformation_error("minimum TTL cannot exceed maximum TTL"))
    _ => ()
  }
  let records : Array[ZoneRecord] = []
  let mut changed = 0
  let mut unchanged = 0
  for record in zone.records() {
    let ttl = bounded_ttl(record.ttl(), minimum, maximum)
    if ttl == record.ttl() {
      unchanged = unchanged + 1
      records.push(record)
    } else {
      changed = changed + 1
      records.push(rewrite_record_ttl(record, ttl))
    }
  }
  Ok({
    document_value: transformed_document(zone, records),
    changed_count_value: changed,
    unchanged_count_value: unchanged,
  })
}

///|
pub fn ZoneSelection::document(self : ZoneSelection) -> ZoneDocument {
  self.document_value
}

///|
pub fn ZoneSelection::selected_count(self : ZoneSelection) -> Int {
  self.selected_count_value
}

///|
pub fn ZoneSelection::excluded_count(self : ZoneSelection) -> Int {
  self.excluded_count_value
}

///|
pub fn ZoneTtlRewrite::document(self : ZoneTtlRewrite) -> ZoneDocument {
  self.document_value
}

///|
pub fn ZoneTtlRewrite::changed_count(self : ZoneTtlRewrite) -> Int {
  self.changed_count_value
}

///|
pub fn ZoneTtlRewrite::unchanged_count(self : ZoneTtlRewrite) -> Int {
  self.unchanged_count_value
}