///|
pub struct ZoneIndex {
  record_values : Array[ZoneRecord]
  owner_values : Array[DomainName]
} derive(Eq, Debug)

///|
pub struct CnameResolution {
  chain_values : Array[DomainName]
  terminal_record_values : Array[ZoneRecord]
} derive(Eq, Debug)

///|
fn contains_owner(owners : Array[DomainName], owner : DomainName) -> Bool {
  for existing in owners {
    if existing == owner {
      return true
    }
  }
  false
}

///|
pub fn index_zone(zone : ZoneDocument) -> ZoneIndex {
  let records = zone.records()
  let owners : Array[DomainName] = []
  for record in records {
    let owner = record.owner()
    if !contains_owner(owners, owner) {
      owners.push(owner)
    }
  }
  { record_values: records, owner_values: owners }
}

///|
pub fn ZoneIndex::owners(self : ZoneIndex) -> Array[DomainName] {
  self.owner_values.copy()
}

///|
pub fn ZoneIndex::owner_count(self : ZoneIndex) -> Int {
  self.owner_values.length()
}

///|
pub fn ZoneIndex::records(self : ZoneIndex) -> Array[ZoneRecord] {
  self.record_values.copy()
}

///|
pub fn ZoneIndex::records_for_owner(
  self : ZoneIndex,
  owner : DomainName,
) -> Array[ZoneRecord] {
  let result : Array[ZoneRecord] = []
  for record in self.record_values {
    if record.owner() == owner {
      result.push(record)
    }
  }
  result
}

///|
pub fn ZoneIndex::records_for_type(
  self : ZoneIndex,
  record_type : String,
) -> Array[ZoneRecord] {
  let expected = uppercase_ascii(record_type)
  let result : Array[ZoneRecord] = []
  for record in self.record_values {
    if record.record_type() == expected {
      result.push(record)
    }
  }
  result
}

///|
pub fn ZoneIndex::lookup(
  self : ZoneIndex,
  owner : DomainName,
  record_type : String,
) -> Array[ZoneRecord] {
  let expected = uppercase_ascii(record_type)
  let result : Array[ZoneRecord] = []
  for record in self.record_values {
    if record.owner() == owner && record.record_type() == expected {
      result.push(record)
    }
  }
  result
}

///|
fn cname_target(records : Array[ZoneRecord]) -> DomainName? {
  for record in records {
    match record.data() {
      CnameData(value) => return Some(value)
      _ => ()
    }
  }
  None
}

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

///|
pub fn ZoneIndex::resolve_cname(
  self : ZoneIndex,
  start : DomainName,
  max_hops? : Int = 16,
) -> Result[CnameResolution, ZoneError] {
  if max_hops < 0 {
    return Err(index_error("CNAME hop limit cannot be negative"))
  }
  let chain : Array[DomainName] = [start]
  let mut current = start
  let mut hops = 0
  while true {
    let records = self.records_for_owner(current)
    let target = cname_target(records)
    match target {
      None =>
        return Ok({ chain_values: chain, terminal_record_values: records })
      Some(value) => {
        if hops >= max_hops {
          return Err(
            index_error("CNAME chain exceeds the configured hop limit"),
          )
        }
        if contains_owner(chain, value) {
          return Err(index_error("CNAME chain contains a cycle"))
        }
        chain.push(value)
        current = value
        hops = hops + 1
      }
    }
  }
  Err(index_error("CNAME resolution ended unexpectedly"))
}

///|
pub fn CnameResolution::chain(self : CnameResolution) -> Array[DomainName] {
  self.chain_values.copy()
}

///|
pub fn CnameResolution::chain_texts(self : CnameResolution) -> Array[String] {
  let result : Array[String] = []
  for name in self.chain_values {
    result.push(name.to_text())
  }
  result
}

///|
pub fn CnameResolution::terminal_records(
  self : CnameResolution,
) -> Array[ZoneRecord] {
  self.terminal_record_values.copy()
}