// CNAME chain resolution helpers.  A tracker is created once per top-level
// resolution and passed through every recursive lookup so loops spanning more
// than one DNS response cannot evade detection.

///|
fn dns_name_equal(left : String, right : String) -> Bool {
  canonical_dns_name(left) == canonical_dns_name(right)
}

// Check if a name already appears in a CNAME chain (case-insensitive and with
// trailing-dot equivalence, as required by DNS name comparison rules).

///|
fn has_cname_loop(chain : Array[String], name : String) -> Bool {
  for candidate in chain {
    if dns_name_equal(candidate, name) {
      return true
    }
  }
  false
}

///|
pub struct CnameTracker {
  max_depth : Int
  chain : Array[String]
}

///|
pub fn CnameTracker::new(
  origin : String,
  max_depth? : Int = 16,
) -> CnameTracker {
  { max_depth: if max_depth < 0 { 0 } else { max_depth }, chain: [origin] }
}

// Record a new CNAME hop. `max_depth` counts aliases, not the origin.

///|
pub fn CnameTracker::follow(
  self : CnameTracker,
  target : String,
) -> Result[Unit, String] {
  if target == "" {
    return Err("CNAME target is empty")
  }
  if has_cname_loop(self.chain, target) {
    return Err("CNAME loop detected: " + target)
  }
  if self.depth() >= self.max_depth {
    return Err("CNAME chain exceeds max depth: " + self.max_depth.to_string())
  }
  self.chain.push(target)
  Ok(())
}

///|
pub fn CnameTracker::get_chain(self : CnameTracker) -> Array[String] {
  self.chain.copy()
}

///|
pub fn CnameTracker::depth(self : CnameTracker) -> Int {
  if self.chain.length() == 0 {
    0
  } else {
    self.chain.length() - 1
  }
}

///|
pub fn CnameTracker::is_at_max(self : CnameTracker) -> Bool {
  self.depth() >= self.max_depth
}

///|
pub fn CnameTracker::contains(self : CnameTracker, name : String) -> Bool {
  has_cname_loop(self.chain, name)
}

// Extract CNAME targets from a response.  This is intentionally an extraction
// helper only: the resolver follows a target only when the RR owner matches the
// current query name.

///|
pub fn extract_cname_targets(answers : Array[RR]) -> Array[String] {
  let targets : Array[String] = Array::new(capacity=answers.length())
  for rr in answers {
    match rr.rdata {
      CNAME(target) => targets.push(target)
      _ => ()
    }
  }
  targets
}

// Find the CNAME target directly owned by `original_name`, if present.  This
// avoids accidentally following an unrelated CNAME included in the same
// answer section.

///|
pub fn find_cname_final_target(
  answers : Array[RR],
  original_name : String,
) -> String {
  let current = Ref(original_name)
  let tracker = CnameTracker::new(original_name)
  for _ in 0.. return current.val
      Some(target) =>
        match tracker.follow(target) {
          Ok(_) => current.val = target
          // This helper cannot report a resolve error, so stop at the last
          // safe name. The resolver itself propagates CnameLoop instead.
          Err(_) => return current.val
        }
    }
  }
  current.val
}

///|
pub fn response_has_cname(msg : Message) -> Bool {
  for rr in msg.answers {
    if rr.rtype == qtype_cname {
      return true
    }
  }
  false
}

///|
pub fn filter_terminal_records(answers : Array[RR]) -> Array[RR] {
  let result : Array[RR] = Array::new(capacity=answers.length())
  for rr in answers {
    if rr.rtype != qtype_cname {
      result.push(rr)
    }
  }
  result
}