///|
pub(all) enum AuditSeverity {
  AuditInfo
  AuditWarning
  AuditError
} derive(Eq, Debug)

///|
pub struct AuditPolicy {
  max_gap_bytes_value : UInt64?
  allowed_start_value : UInt64?
  allowed_end_exclusive_value : UInt64?
  max_segments_value : Int?
  max_total_bytes_value : Int?
  require_entry_in_data_value : Bool
} derive(Eq, Debug)

///|
pub struct AuditFinding {
  code_value : String
  severity_value : AuditSeverity
  message_value : String
  address_value : UInt64?
} derive(Eq, Debug)

///|
pub struct ImageAudit {
  finding_values : Array[AuditFinding]
  segment_count_value : Int
  total_bytes_value : Int
  gap_count_value : Int
} derive(Eq, Debug)

///|
pub fn AuditPolicy::new(
  max_gap_bytes? : UInt64? = None,
  allowed_start? : UInt64? = None,
  allowed_end_exclusive? : UInt64? = None,
  max_segments? : Int? = None,
  max_total_bytes? : Int? = None,
  require_entry_in_data? : Bool = false,
) -> Result[AuditPolicy, FirmwareError] {
  match (allowed_start, allowed_end_exclusive) {
    (Some(start), Some(end)) if start >= end =>
      return Err(invalid_image_range("audit address range must be non-empty"))
    _ => ()
  }
  match max_segments {
    Some(value) if value < 0 =>
      return Err(invalid_image_range("audit segment limit cannot be negative"))
    _ => ()
  }
  match max_total_bytes {
    Some(value) if value < 0 =>
      return Err(invalid_image_range("audit byte limit cannot be negative"))
    _ => ()
  }
  Ok({
    max_gap_bytes_value: max_gap_bytes,
    allowed_start_value: allowed_start,
    allowed_end_exclusive_value: allowed_end_exclusive,
    max_segments_value: max_segments,
    max_total_bytes_value: max_total_bytes,
    require_entry_in_data_value: require_entry_in_data,
  })
}

///|
pub fn AuditPolicy::standard() -> AuditPolicy {
  {
    max_gap_bytes_value: None,
    allowed_start_value: None,
    allowed_end_exclusive_value: None,
    max_segments_value: None,
    max_total_bytes_value: None,
    require_entry_in_data_value: false,
  }
}

///|
fn audit_finding(
  code : String,
  severity : AuditSeverity,
  message : String,
  address? : UInt64? = None,
) -> AuditFinding {
  {
    code_value: code,
    severity_value: severity,
    message_value: message,
    address_value: address,
  }
}

///|
fn image_contains_address(image : FirmwareImage, address : UInt64) -> Bool {
  image.byte_at(address) is Some(_)
}

///|
/// Evaluate deterministic image acceptance rules without modifying the image.
pub fn audit_image(image : FirmwareImage, policy : AuditPolicy) -> ImageAudit {
  let findings : Array[AuditFinding] = []
  let segments = image.segments()
  let gaps = image.gaps()
  if segments.length() == 0 {
    findings.push(
      audit_finding(
        "image.empty",
        AuditWarning,
        "firmware image contains no data bytes",
      ),
    )
  }
  match policy.max_segments_value {
    Some(limit) if segments.length() > limit =>
      findings.push(
        audit_finding(
          "image.segment_limit",
          AuditError,
          "firmware image exceeds the configured segment limit",
        ),
      )
    _ => ()
  }
  match policy.max_total_bytes_value {
    Some(limit) if image.total_bytes() > limit =>
      findings.push(
        audit_finding(
          "image.byte_limit",
          AuditError,
          "firmware image exceeds the configured byte limit",
        ),
      )
    _ => ()
  }
  for gap in gaps {
    match policy.max_gap_bytes_value {
      Some(limit) if gap.length() > limit =>
        findings.push(
          audit_finding(
            "image.gap_limit",
            AuditWarning,
            "sparse image gap exceeds the configured limit",
            address=Some(gap.start()),
          ),
        )
      _ => ()
    }
  }
  for segment in segments {
    match policy.allowed_start_value {
      Some(start) if segment.address() < start =>
        findings.push(
          audit_finding(
            "image.address_below_range",
            AuditError,
            "segment begins below the allowed address range",
            address=Some(segment.address()),
          ),
        )
      _ => ()
    }
    match policy.allowed_end_exclusive_value {
      Some(end) if segment.end_exclusive() > end =>
        findings.push(
          audit_finding(
            "image.address_above_range",
            AuditError,
            "segment ends above the allowed address range",
            address=Some(segment.address()),
          ),
        )
      _ => ()
    }
  }
  if policy.require_entry_in_data_value {
    match image.entry_point() {
      Some(value) if !image_contains_address(image, value) =>
        findings.push(
          audit_finding(
            "image.entry_outside_data",
            AuditError,
            "entry point does not reference a byte in the image",
            address=Some(value),
          ),
        )
      None =>
        findings.push(
          audit_finding(
            "image.entry_missing",
            AuditError,
            "image has no entry point",
          ),
        )
      _ => ()
    }
  }
  {
    finding_values: findings,
    segment_count_value: segments.length(),
    total_bytes_value: image.total_bytes(),
    gap_count_value: gaps.length(),
  }
}

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

///|
pub fn AuditFinding::severity(self : AuditFinding) -> AuditSeverity {
  self.severity_value
}

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

///|
pub fn AuditFinding::address(self : AuditFinding) -> UInt64? {
  self.address_value
}

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

///|
pub fn ImageAudit::segment_count(self : ImageAudit) -> Int {
  self.segment_count_value
}

///|
pub fn ImageAudit::total_bytes(self : ImageAudit) -> Int {
  self.total_bytes_value
}

///|
pub fn ImageAudit::gap_count(self : ImageAudit) -> Int {
  self.gap_count_value
}

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

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

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