///|
pub(all) enum ZoneReferenceKind {
  NameServerReference
  CanonicalNameReference
  MailExchangeReference
  ServiceTargetReference
  SoaPrimaryReference
} derive(Eq, Debug)

///|
struct ReferenceCandidate {
  kind_value : ZoneReferenceKind
  target_value : DomainName
  requires_address_value : Bool
  disabled_value : Bool
} derive(Eq, Debug)

///|
pub struct ZoneReference {
  source_owner_value : DomainName
  kind_value : ZoneReferenceKind
  target_value : DomainName
  internal_value : Bool
  target_declared_value : Bool
  address_available_value : Bool
  requires_address_value : Bool
  disabled_value : Bool
} derive(Eq, Debug)

///|
pub struct ZoneReferenceAnalysis {
  reference_values : Array[ZoneReference]
  internal_count_value : Int
  external_count_value : Int
  missing_target_count_value : Int
  addressless_target_count_value : Int
  disabled_count_value : Int
} derive(Eq, Debug)

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

///|
fn reference_kind_text(kind : ZoneReferenceKind) -> String {
  match kind {
    NameServerReference => "ns_target"
    CanonicalNameReference => "cname_target"
    MailExchangeReference => "mx_exchange"
    ServiceTargetReference => "srv_target"
    SoaPrimaryReference => "soa_primary"
  }
}

///|
fn root_target(target : DomainName) -> Bool {
  target.is_absolute() && target.labels().length() == 0
}

///|
fn reference_candidate(record : ZoneRecord) -> ReferenceCandidate? {
  match record.data() {
    NsData(target) =>
      Some({
        kind_value: NameServerReference,
        target_value: target,
        requires_address_value: true,
        disabled_value: false,
      })
    CnameData(target) =>
      Some({
        kind_value: CanonicalNameReference,
        target_value: target,
        requires_address_value: false,
        disabled_value: false,
      })
    MxData(value) =>
      Some({
        kind_value: MailExchangeReference,
        target_value: value.exchange(),
        requires_address_value: true,
        disabled_value: false,
      })
    SrvData(value) => {
      let target = value.target()
      Some({
        kind_value: ServiceTargetReference,
        target_value: target,
        requires_address_value: !root_target(target),
        disabled_value: root_target(target),
      })
    }
    SoaData(value) =>
      Some({
        kind_value: SoaPrimaryReference,
        target_value: value.primary_name(),
        requires_address_value: true,
        disabled_value: false,
      })
    _ => None
  }
}

///|
fn reference_owner_declared(
  records : Array[ZoneRecord],
  target : DomainName,
) -> Bool {
  for record in records {
    if record.owner() == target {
      return true
    }
  }
  false
}

///|
fn reference_address_available(
  records : Array[ZoneRecord],
  target : DomainName,
) -> Bool {
  for record in records {
    if record.owner() == target {
      match record.data() {
        AData(_) | AaaaData(_) => return true
        _ => ()
      }
    }
  }
  false
}

///|
fn build_zone_reference(
  record : ZoneRecord,
  candidate : ReferenceCandidate,
  records : Array[ZoneRecord],
  origin : DomainName,
) -> ZoneReference {
  if candidate.disabled_value {
    return {
      source_owner_value: record.owner(),
      kind_value: candidate.kind_value,
      target_value: candidate.target_value,
      internal_value: false,
      target_declared_value: false,
      address_available_value: false,
      requires_address_value: false,
      disabled_value: true,
    }
  }
  let internal = domain_is_at_or_below(candidate.target_value, origin)
  let declared = internal &&
    reference_owner_declared(records, candidate.target_value)
  let address = internal &&
    reference_address_available(records, candidate.target_value)
  {
    source_owner_value: record.owner(),
    kind_value: candidate.kind_value,
    target_value: candidate.target_value,
    internal_value: internal,
    target_declared_value: declared,
    address_available_value: address,
    requires_address_value: candidate.requires_address_value,
    disabled_value: false,
  }
}

///|
/// Analyze domain-name references without treating external targets as errors.
pub fn analyze_zone_references(
  zone : ZoneDocument,
  max_references? : Int = 100000,
) -> Result[ZoneReferenceAnalysis, ZoneError] {
  if max_references < 0 {
    return Err(reference_error("maximum reference count cannot be negative"))
  }
  let records = zone.records()
  let origin = match zone.origin() {
    Some(value) => value
    None =>
      return Err(
        reference_error("reference analysis requires an absolute zone origin"),
      )
  }
  let references : Array[ZoneReference] = []
  let mut internal_count = 0
  let mut external_count = 0
  let mut missing_count = 0
  let mut addressless_count = 0
  let mut disabled_count = 0
  for record in records {
    match reference_candidate(record) {
      None => ()
      Some(candidate) => {
        if references.length() >= max_references {
          return Err(
            reference_error(
              "zone exceeds the configured reference evidence limit",
            ),
          )
        }
        let reference = build_zone_reference(record, candidate, records, origin)
        references.push(reference)
        if reference.disabled() {
          disabled_count = disabled_count + 1
        } else if reference.internal() {
          internal_count = internal_count + 1
          if !reference.target_declared() {
            missing_count = missing_count + 1
          } else if reference.requires_address() &&
            !reference.address_available() {
            addressless_count = addressless_count + 1
          }
        } else {
          external_count = external_count + 1
        }
      }
    }
  }
  Ok({
    reference_values: references,
    internal_count_value: internal_count,
    external_count_value: external_count,
    missing_target_count_value: missing_count,
    addressless_target_count_value: addressless_count,
    disabled_count_value: disabled_count,
  })
}

///|
pub fn ZoneReference::source_owner(self : ZoneReference) -> DomainName {
  self.source_owner_value
}

///|
pub fn ZoneReference::kind(self : ZoneReference) -> ZoneReferenceKind {
  self.kind_value
}

///|
pub fn ZoneReference::target(self : ZoneReference) -> DomainName {
  self.target_value
}

///|
pub fn ZoneReference::internal(self : ZoneReference) -> Bool {
  self.internal_value
}

///|
pub fn ZoneReference::target_declared(self : ZoneReference) -> Bool {
  self.target_declared_value
}

///|
pub fn ZoneReference::address_available(self : ZoneReference) -> Bool {
  self.address_available_value
}

///|
pub fn ZoneReference::requires_address(self : ZoneReference) -> Bool {
  self.requires_address_value
}

///|
pub fn ZoneReference::disabled(self : ZoneReference) -> Bool {
  self.disabled_value
}

///|
pub fn ZoneReference::healthy(self : ZoneReference) -> Bool {
  self.disabled_value ||
  !self.internal_value ||
  (
    self.target_declared_value &&
    (!self.requires_address_value || self.address_available_value)
  )
}

///|
pub fn ZoneReferenceAnalysis::references(
  self : ZoneReferenceAnalysis,
) -> Array[ZoneReference] {
  self.reference_values.copy()
}

///|
pub fn ZoneReferenceAnalysis::internal_count(
  self : ZoneReferenceAnalysis,
) -> Int {
  self.internal_count_value
}

///|
pub fn ZoneReferenceAnalysis::external_count(
  self : ZoneReferenceAnalysis,
) -> Int {
  self.external_count_value
}

///|
pub fn ZoneReferenceAnalysis::missing_target_count(
  self : ZoneReferenceAnalysis,
) -> Int {
  self.missing_target_count_value
}

///|
pub fn ZoneReferenceAnalysis::addressless_target_count(
  self : ZoneReferenceAnalysis,
) -> Int {
  self.addressless_target_count_value
}

///|
pub fn ZoneReferenceAnalysis::disabled_count(
  self : ZoneReferenceAnalysis,
) -> Int {
  self.disabled_count_value
}

///|
pub fn ZoneReferenceAnalysis::healthy(self : ZoneReferenceAnalysis) -> Bool {
  self.missing_target_count_value == 0 &&
  self.addressless_target_count_value == 0
}

///|
pub fn render_reference_analysis_text(
  analysis : ZoneReferenceAnalysis,
) -> String {
  let mut result = "DNS zone reference analysis" +
    "\nhealthy: " +
    analysis.healthy().to_string() +
    "\ninternal: " +
    analysis.internal_count().to_string() +
    "\nexternal: " +
    analysis.external_count().to_string() +
    "\nmissing targets: " +
    analysis.missing_target_count().to_string() +
    "\naddressless targets: " +
    analysis.addressless_target_count().to_string() +
    "\ndisabled services: " +
    analysis.disabled_count().to_string()
  for reference in analysis.references() {
    result = result +
      "\n[" +
      reference_kind_text(reference.kind()) +
      "] " +
      reference.source_owner().to_text() +
      " -> " +
      reference.target().to_text() +
      " healthy=" +
      reference.healthy().to_string()
  }
  result
}

///|
pub fn render_reference_analysis_json(
  analysis : ZoneReferenceAnalysis,
) -> String {
  let references : Array[Json] = []
  for reference in analysis.references() {
    references.push(
      Json::object({
        "kind": Json::string(reference_kind_text(reference.kind())),
        "source_owner": Json::string(reference.source_owner().to_text()),
        "target": Json::string(reference.target().to_text()),
        "internal": Json::boolean(reference.internal()),
        "target_declared": Json::boolean(reference.target_declared()),
        "requires_address": Json::boolean(reference.requires_address()),
        "address_available": Json::boolean(reference.address_available()),
        "disabled": Json::boolean(reference.disabled()),
        "healthy": Json::boolean(reference.healthy()),
      }),
    )
  }
  Json::object({
    "type": Json::string("dns_zone_reference_analysis"),
    "healthy": Json::boolean(analysis.healthy()),
    "internal_count": Json::number(analysis.internal_count().to_double()),
    "external_count": Json::number(analysis.external_count().to_double()),
    "missing_target_count": Json::number(
      analysis.missing_target_count().to_double(),
    ),
    "addressless_target_count": Json::number(
      analysis.addressless_target_count().to_double(),
    ),
    "disabled_count": Json::number(analysis.disabled_count().to_double()),
    "references": Json::array(references),
  }).stringify()
}