///|
pub(all) enum FindingSeverity {
  FindingInfo
  FindingWarning
  FindingError
} derive(Eq, Debug)

///|
pub struct ZonePolicy {
  require_soa_value : Bool
  require_apex_ns_value : Bool
  enforce_cname_exclusive_value : Bool
  max_records_value : Int?
  max_ttl_value : Ttl?
} derive(Eq, Debug)

///|
pub struct ZoneFinding {
  code_value : String
  severity_value : FindingSeverity
  message_value : String
  owner_value : DomainName?
} derive(Eq, Debug)

///|
pub struct ZoneValidation {
  finding_values : Array[ZoneFinding]
  record_count_value : Int
} derive(Eq, Debug)

///|
pub fn ZonePolicy::new(
  require_soa? : Bool = true,
  require_apex_ns? : Bool = true,
  enforce_cname_exclusive? : Bool = true,
  max_records? : Int? = None,
  max_ttl? : Ttl? = None,
) -> Result[ZonePolicy, ZoneError] {
  match max_records {
    Some(value) if value < 0 =>
      return Err(
        ZoneError::new(
          IntegrityViolation,
          "maximum record count cannot be negative",
          SourceSpan::point(0, 0),
        ),
      )
    _ => ()
  }
  Ok({
    require_soa_value: require_soa,
    require_apex_ns_value: require_apex_ns,
    enforce_cname_exclusive_value: enforce_cname_exclusive,
    max_records_value: max_records,
    max_ttl_value: max_ttl,
  })
}

///|
pub fn ZonePolicy::standard() -> ZonePolicy {
  ZonePolicy::new().unwrap()
}

///|
fn zone_finding(
  code : String,
  severity : FindingSeverity,
  message : String,
  owner? : DomainName? = None,
) -> ZoneFinding {
  {
    code_value: code,
    severity_value: severity,
    message_value: message,
    owner_value: owner,
  }
}

///|
fn owner_record_counts(
  records : Array[ZoneRecord],
  owner : DomainName,
) -> (Int, Int) {
  let mut cname_count = 0
  let mut other_count = 0
  for record in records {
    if record.owner() == owner {
      if record.record_type() == "CNAME" {
        cname_count = cname_count + 1
      } else {
        other_count = other_count + 1
      }
    }
  }
  (cname_count, other_count)
}

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

///|
pub fn validate_zone(
  zone : ZoneDocument,
  policy : ZonePolicy,
) -> Result[ZoneValidation, ZoneError] {
  let records = zone.records()
  let findings : Array[ZoneFinding] = []
  let origin = zone.origin()
  match policy.max_records_value {
    Some(limit) if records.length() > limit =>
      findings.push(
        zone_finding(
          "zone.record_limit",
          FindingError,
          "zone exceeds the configured record count limit",
        ),
      )
    _ => ()
  }
  let mut soa_count = 0
  let mut apex_soa_count = 0
  let mut apex_ns_count = 0
  for record in records {
    if record.record_type() == "SOA" {
      soa_count = soa_count + 1
      if origin == Some(record.owner()) {
        apex_soa_count = apex_soa_count + 1
      }
    }
    if record.record_type() == "NS" && origin == Some(record.owner()) {
      apex_ns_count = apex_ns_count + 1
    }
    match policy.max_ttl_value {
      Some(limit) if record.ttl().seconds() > limit.seconds() =>
        findings.push(
          zone_finding(
            "zone.ttl_limit",
            FindingError,
            "record TTL exceeds the configured limit",
            owner=Some(record.owner()),
          ),
        )
      _ => ()
    }
  }
  if policy.require_soa_value && soa_count == 0 {
    findings.push(
      zone_finding(
        MissingSoa.code(),
        FindingError,
        "zone contains no SOA record",
      ),
    )
  } else if soa_count > 1 {
    findings.push(
      zone_finding(
        DuplicateSingletonRecord.code(),
        FindingError,
        "zone contains more than one SOA record",
      ),
    )
  } else if soa_count == 1 && apex_soa_count != 1 {
    findings.push(
      zone_finding(
        "zone.soa_not_at_apex",
        FindingError,
        "SOA record owner is not the active zone origin",
      ),
    )
  }
  if policy.require_apex_ns_value && apex_ns_count == 0 {
    findings.push(
      zone_finding(
        MissingApexNs.code(),
        FindingError,
        "zone contains no NS record at its origin",
      ),
    )
  }
  if policy.enforce_cname_exclusive_value {
    let checked : Array[DomainName] = []
    for record in records {
      let owner = record.owner()
      if !owner_already_checked(checked, owner) {
        checked.push(owner)
        let (cname_count, other_count) = owner_record_counts(records, owner)
        if cname_count > 1 || (cname_count > 0 && other_count > 0) {
          findings.push(
            zone_finding(
              ConflictingCname.code(),
              FindingError,
              "CNAME owner also has conflicting record data",
              owner=Some(owner),
            ),
          )
        }
      }
    }
  }
  Ok({ finding_values: findings, record_count_value: records.length() })
}

///|
pub fn ZoneFinding::code(self : ZoneFinding) -> String {
  self.code_value
}

///|
pub fn ZoneFinding::severity(self : ZoneFinding) -> FindingSeverity {
  self.severity_value
}

///|
pub fn ZoneFinding::message(self : ZoneFinding) -> String {
  self.message_value
}

///|
pub fn ZoneFinding::owner(self : ZoneFinding) -> DomainName? {
  self.owner_value
}

///|
pub fn ZoneValidation::findings(self : ZoneValidation) -> Array[ZoneFinding] {
  self.finding_values.copy()
}

///|
pub fn ZoneValidation::record_count(self : ZoneValidation) -> Int {
  self.record_count_value
}

///|
pub fn ZoneValidation::error_count(self : ZoneValidation) -> Int {
  let mut total = 0
  for finding in self.finding_values {
    if finding.severity() == FindingError {
      total = total + 1
    }
  }
  total
}

///|
pub fn ZoneValidation::warning_count(self : ZoneValidation) -> Int {
  let mut total = 0
  for finding in self.finding_values {
    if finding.severity() == FindingWarning {
      total = total + 1
    }
  }
  total
}

///|
pub fn ZoneValidation::accepted(self : ZoneValidation) -> Bool {
  self.error_count() == 0
}