///|
/// Resource limits applied at an application ingress boundary.
pub(all) struct AppPolicy {
  max_bytes : Int
  max_nodes : Int
  max_depth : Int
  max_headers : Int
  max_batch_messages : Int
  max_frame_bytes : Int
} derive(Eq, Debug)

///|
/// Identifies the ingress representation that was accepted.
pub(all) enum AppIngressKind {
  IngressDocument
  IngressMessage
  IngressBatch
  IngressFrames
} derive(Eq, Debug)

///|
/// One policy violation suitable for logs or an API response.
pub(all) struct AppPolicyIssue {
  path : String
  code : String
  message : String
} derive(Eq, Debug)

///|
/// A policy report retains every independent limit violation.
pub(all) struct AppPolicyReport {
  valid : Bool
  issues : Array[AppPolicyIssue]
} derive(Eq, Debug)

///|
/// Result returned by the application ingress helpers.
pub(all) struct AppIngestResult {
  accepted : Bool
  kind : AppIngressKind
  value : CborValue?
  values : Array[CborValue]
  stats : CborDocumentStats
  report : AppPolicyReport
} derive(Debug)

///|
/// A practical default policy for service and edge workloads.
pub fn app_policy_default() -> AppPolicy {
  {
    max_bytes: 1024 * 1024,
    max_nodes: 10000,
    max_depth: 64,
    max_headers: 64,
    max_batch_messages: 1024,
    max_frame_bytes: 1024 * 1024,
  }
}

///|
/// A smaller policy useful for untrusted public endpoints.
pub fn app_policy_strict() -> AppPolicy {
  {
    max_bytes: 256 * 1024,
    max_nodes: 4096,
    max_depth: 32,
    max_headers: 32,
    max_batch_messages: 256,
    max_frame_bytes: 256 * 1024,
  }
}

///|
/// Build a policy with explicit limits for deterministic tests and deployments.
pub fn app_policy_with_limits(
  base : AppPolicy,
  max_bytes : Int,
  max_nodes : Int,
  max_depth : Int,
  max_headers : Int,
  max_batch_messages : Int,
  max_frame_bytes : Int,
) -> AppPolicy {
  {
    max_bytes: if max_bytes < 1 {
      base.max_bytes
    } else {
      max_bytes
    },
    max_nodes: if max_nodes < 1 {
      base.max_nodes
    } else {
      max_nodes
    },
    max_depth: if max_depth < 1 {
      base.max_depth
    } else {
      max_depth
    },
    max_headers: if max_headers < 1 {
      base.max_headers
    } else {
      max_headers
    },
    max_batch_messages: if max_batch_messages < 1 {
      base.max_batch_messages
    } else {
      max_batch_messages
    },
    max_frame_bytes: if max_frame_bytes < 1 {
      base.max_frame_bytes
    } else {
      max_frame_bytes
    },
  }
}

///|
/// Return a policy line for startup logs and configuration audits.
pub fn app_policy_summary(policy : AppPolicy) -> String {
  "bytes=\{policy.max_bytes} nodes=\{policy.max_nodes} depth=\{policy.max_depth} headers=\{policy.max_headers} batch=\{policy.max_batch_messages} frame=\{policy.max_frame_bytes}"
}

///|
fn policy_issue(
  path : String,
  code : String,
  message : String,
) -> AppPolicyIssue {
  { path, code, message }
}

///|
fn policy_report(issues : Array[AppPolicyIssue]) -> AppPolicyReport {
  { valid: issues.length() == 0, issues }
}

///|
/// Render one policy issue in a stable log-friendly form.
pub fn app_policy_issue_to_string(issue : AppPolicyIssue) -> String {
  issue.code + " at " + issue.path + ": " + issue.message
}

///|
/// Render all policy issues in insertion order.
pub fn app_policy_report_to_string(report : AppPolicyReport) -> String {
  if report.valid {
    "valid"
  } else {
    let builder = StringBuilder::new()
    for i = 0; i < report.issues.length(); i = i + 1 {
      if i > 0 {
        builder.write_string("\n")
      }
      builder.write_string(app_policy_issue_to_string(report.issues[i]))
    }
    builder.to_string()
  }
}

///|
/// Combine policy reports from multiple documents.
pub fn app_policy_combine(
  first : AppPolicyReport,
  second : AppPolicyReport,
) -> AppPolicyReport {
  let issues = []
  for issue in first.issues {
    issues.push(issue)
  }
  for issue in second.issues {
    issues.push(issue)
  }
  policy_report(issues)
}

///|
/// Prefix issues when a payload is embedded in a larger protocol object.
pub fn app_policy_prefix(
  prefix : String,
  report : AppPolicyReport,
) -> AppPolicyReport {
  let issues = []
  for issue in report.issues {
    issues.push(policy_issue(prefix + issue.path, issue.code, issue.message))
  }
  policy_report(issues)
}

///|
/// Check structural resource limits for one CBOR document.
pub fn cbor_policy_check(
  value : CborValue,
  policy : AppPolicy,
) -> AppPolicyReport {
  let issues = []
  let stats = cbor_document_stats(value)
  if stats.encoded_bytes > policy.max_bytes {
    issues.push(
      policy_issue(
        "$", "max_bytes", "encoded document exceeds the configured byte limit",
      ),
    )
  }
  if stats.nodes > policy.max_nodes {
    issues.push(
      policy_issue("$", "max_nodes", "document contains too many nodes"),
    )
  }
  if stats.max_depth > policy.max_depth {
    issues.push(
      policy_issue(
        "$", "max_depth", "document nesting exceeds the configured depth",
      ),
    )
  }
  policy_report(issues)
}

///|
fn cbor_policy_check_headers(
  message : AppMessage,
  policy : AppPolicy,
) -> AppPolicyReport {
  let issues = []
  if message.headers.length() > policy.max_headers {
    issues.push(
      policy_issue(
        "$.headers", "max_headers", "message contains too many headers",
      ),
    )
  }
  policy_report(issues)
}

///|
fn cbor_policy_check_batch(
  batch : AppMessageBatch,
  policy : AppPolicy,
) -> AppPolicyReport {
  let issues = []
  if batch.messages.length() > policy.max_batch_messages {
    issues.push(
      policy_issue(
        "$.messages", "max_batch_messages", "batch contains too many messages",
      ),
    )
  }
  for i = 0; i < batch.messages.length(); i = i + 1 {
    let message_report = cbor_policy_check(batch.messages[i].payload, policy)
    for issue in message_report.issues {
      issues.push(
        policy_issue(
          "$.messages[" + i.to_string() + "].payload" + issue.path,
          issue.code,
          issue.message,
        ),
      )
    }
    let header_report = cbor_policy_check_headers(batch.messages[i], policy)
    for issue in header_report.issues {
      issues.push(issue)
    }
  }
  policy_report(issues)
}

///|
fn empty_ingest_result(kind : AppIngressKind) -> AppIngestResult {
  {
    accepted: false,
    kind,
    value: None,
    values: [],
    stats: cbor_document_stats(CborValue::Simple(22)),
    report: policy_report([]),
  }
}

///|
fn result_for_value(
  kind : AppIngressKind,
  value : CborValue,
  report : AppPolicyReport,
) -> AppIngestResult {
  {
    accepted: report.valid,
    kind,
    value: Some(value),
    values: [value],
    stats: cbor_document_stats(value),
    report,
  }
}

///|
/// Decode and policy-check a raw CBOR document at an application boundary.
pub fn cbor_ingest_document(
  bytes : Bytes,
  policy : AppPolicy,
) -> AppIngestResult raise CborError {
  if bytes.length() > policy.max_bytes {
    let result = empty_ingest_result(IngressDocument)
    return {
      ..result,
      report: policy_report([
        policy_issue(
          "$", "max_bytes", "wire payload exceeds the configured byte limit",
        ),
      ]),
    }
  }
  let value = decode(bytes)
  result_for_value(IngressDocument, value, cbor_policy_check(value, policy))
}

///|
/// Decode and policy-check one versioned application message.
pub fn cbor_ingest_message(
  bytes : Bytes,
  policy : AppPolicy,
) -> AppIngestResult raise CborError {
  if bytes.length() > policy.max_bytes {
    let result = empty_ingest_result(IngressMessage)
    return {
      ..result,
      report: policy_report([
        policy_issue(
          "$", "max_bytes", "message wire payload exceeds the configured byte limit",
        ),
      ]),
    }
  }
  let message = decode_app_message(bytes)
  let payload_report = cbor_policy_check(message.payload, policy)
  let header_report = cbor_policy_check_headers(message, policy)
  let report = app_policy_combine(payload_report, header_report)
  result_for_value(IngressMessage, app_message_to_cbor(message), report)
}

///|
/// Decode and policy-check one ordered application batch.
pub fn cbor_ingest_batch(
  bytes : Bytes,
  policy : AppPolicy,
) -> AppIngestResult raise CborError {
  if bytes.length() > policy.max_bytes {
    let result = empty_ingest_result(IngressBatch)
    return {
      ..result,
      report: policy_report([
        policy_issue(
          "$", "max_bytes", "batch wire payload exceeds the configured byte limit",
        ),
      ]),
    }
  }
  let batch = decode_app_batch(bytes)
  let report = cbor_policy_check_batch(batch, policy)
  let values = []
  for message in batch.messages {
    values.push(app_message_to_cbor(message))
  }
  {
    accepted: report.valid,
    kind: IngressBatch,
    value: Some(app_batch_to_cbor(batch)),
    values,
    stats: cbor_document_stats(app_batch_to_cbor(batch)),
    report,
  }
}

///|
/// Decode and policy-check every value in a length-delimited frame stream.
pub fn cbor_ingest_frames(
  bytes : Bytes,
  policy : AppPolicy,
) -> AppIngestResult raise CborError {
  if bytes.length() > policy.max_bytes {
    let result = empty_ingest_result(IngressFrames)
    return {
      ..result,
      report: policy_report([
        policy_issue(
          "$", "max_bytes", "frame stream exceeds the configured byte limit",
        ),
      ]),
    }
  }
  let values = decode_cbor_frames(bytes, policy.max_frame_bytes)
  let issues = []
  let mut nodes = 0
  let mut depth = 0
  for i = 0; i < values.length(); i = i + 1 {
    let value = values[i]
    let report = cbor_policy_check(value, policy)
    for issue in report.issues {
      issues.push(
        policy_issue(
          "$.frames[" + i.to_string() + "]." + issue.path,
          issue.code,
          issue.message,
        ),
      )
    }
    let stats = cbor_document_stats(value)
    nodes = nodes + stats.nodes
    if stats.max_depth > depth {
      depth = stats.max_depth
    }
  }
  let report = policy_report(issues)
  {
    accepted: report.valid,
    kind: IngressFrames,
    value: None,
    values,
    stats: {
      nodes,
      containers: 0,
      max_depth: depth,
      text_bytes: 0,
      binary_bytes: 0,
      encoded_bytes: bytes.length(),
    },
    report,
  }
}

///|
/// Return true when a raw document passes the configured policy.
pub fn cbor_policy_accepts(value : CborValue, policy : AppPolicy) -> Bool {
  cbor_policy_check(value, policy).valid
}

///|
/// Validate a message payload together with its application headers.
pub fn cbor_message_policy_accepts(
  message : AppMessage,
  policy : AppPolicy,
) -> Bool {
  cbor_policy_check(message.payload, policy).valid &&
  cbor_policy_check_headers(message, policy).valid
}

///|
/// Return a compact summary of an ingress result for metrics.
pub fn app_ingest_summary(result : AppIngestResult) -> String {
  let status = if result.accepted { "accepted" } else { "rejected" }
  let kind = match result.kind {
    IngressDocument => "document"
    IngressMessage => "message"
    IngressBatch => "batch"
    IngressFrames => "frames"
  }
  "status=\{status} kind=\{kind} values=\{result.values.length()} nodes=\{result.stats.nodes} bytes=\{result.stats.encoded_bytes} issues=\{result.report.issues.length()}"
}

///|
/// Return all policy issue codes in stable order.
pub fn app_ingest_issue_codes(result : AppIngestResult) -> Array[String] {
  let codes = []
  for issue in result.report.issues {
    codes.push(issue.code)
  }
  codes
}

///|
/// Reject a report with a caller-provided business rule.
pub fn app_ingest_add_business_issue(
  result : AppIngestResult,
  path : String,
  code : String,
  message : String,
) -> AppIngestResult {
  let issues = []
  for issue in result.report.issues {
    issues.push(issue)
  }
  issues.push(policy_issue(path, code, message))
  let report = policy_report(issues)
  { ..result, accepted: false, report }
}

///|
/// Return the configured maximum transport size.
pub fn app_policy_transport_limit(policy : AppPolicy) -> Int {
  if policy.max_bytes < policy.max_frame_bytes {
    policy.max_bytes
  } else {
    policy.max_frame_bytes
  }
}

///|
/// Return whether a batch can be framed under the policy.
pub fn app_batch_fits_policy(
  batch : AppMessageBatch,
  policy : AppPolicy,
) -> Bool {
  batch.messages.length() <= policy.max_batch_messages &&
  app_batch_encoded_size(batch) <= policy.max_bytes &&
  app_batch_payloads_within_budget(batch, policy.max_nodes)
}

///|
/// Check every document in an ordered collection and retain indexed paths.
pub fn cbor_policy_check_many(
  values : Array[CborValue],
  policy : AppPolicy,
) -> AppPolicyReport {
  let issues = []
  if values.length() > policy.max_batch_messages {
    issues.push(
      policy_issue(
        "$.items", "max_batch_messages", "document collection exceeds the configured item limit",
      ),
    )
  }
  for i = 0; i < values.length(); i = i + 1 {
    let report = cbor_policy_check(values[i], policy)
    for issue in report.issues {
      issues.push(
        policy_issue(
          "$.items[" + i.to_string() + "]" + issue.path,
          issue.code,
          issue.message,
        ),
      )
    }
  }
  policy_report(issues)
}

///|
/// Count nodes across a collection for admission control and metrics.
pub fn cbor_policy_many_nodes(values : Array[CborValue]) -> Int {
  let mut total = 0
  for value in values {
    total = total + cbor_document_stats(value).nodes
  }
  total
}

///|
/// Measure the exact encoded footprint of a collection.
pub fn cbor_policy_many_encoded_bytes(values : Array[CborValue]) -> Int {
  let mut total = 0
  for value in values {
    total = total + encode(value).length()
  }
  total
}

///|
/// Validate that a policy is internally coherent before accepting config.
pub fn app_policy_is_sane(policy : AppPolicy) -> Bool {
  policy.max_bytes > 0 &&
  policy.max_nodes > 0 &&
  policy.max_depth > 0 &&
  policy.max_headers > 0 &&
  policy.max_batch_messages > 0 &&
  policy.max_frame_bytes > 0 &&
  policy.max_frame_bytes <= policy.max_bytes
}

///|
/// Check aggregate byte and node budgets without losing per-document errors.
pub fn app_policy_many_within_budget(
  values : Array[CborValue],
  policy : AppPolicy,
) -> Bool {
  app_policy_is_sane(policy) &&
  values.length() <= policy.max_batch_messages &&
  cbor_policy_many_encoded_bytes(values) <= policy.max_bytes &&
  cbor_policy_many_nodes(values) <= policy.max_nodes &&
  cbor_policy_check_many(values, policy).valid
}

///|
/// Return the aggregate node count recorded in an ingress result.
pub fn app_ingest_values_nodes(result : AppIngestResult) -> Int {
  cbor_policy_many_nodes(result.values)
}

///|
/// Return the exact re-encoded size of values retained by an ingress result.
pub fn app_ingest_values_encoded_bytes(result : AppIngestResult) -> Int {
  cbor_policy_many_encoded_bytes(result.values)
}

///|
/// A single status predicate for adapters that should reject invalid ingress.
pub fn app_ingest_is_valid(result : AppIngestResult) -> Bool {
  result.accepted && result.report.valid
}

///|
/// Check whether a policy has no looser resource limit than another policy.
pub fn app_policy_is_stricter_or_equal(
  policy : AppPolicy,
  other : AppPolicy,
) -> Bool {
  policy.max_bytes <= other.max_bytes &&
  policy.max_nodes <= other.max_nodes &&
  policy.max_depth <= other.max_depth &&
  policy.max_headers <= other.max_headers &&
  policy.max_batch_messages <= other.max_batch_messages &&
  policy.max_frame_bytes <= other.max_frame_bytes
}

///|
/// Produce a compact budget snapshot for logs and telemetry.
pub fn app_policy_budget_summary(
  values : Array[CborValue],
  policy : AppPolicy,
) -> String {
  let bytes = cbor_policy_many_encoded_bytes(values)
  let nodes = cbor_policy_many_nodes(values)
  "items=\{values.length()} bytes=\{bytes}/\{policy.max_bytes} nodes=\{nodes}/\{policy.max_nodes} valid=\{app_policy_many_within_budget(values, policy)}"
}

///|
/// Count occurrences of a policy code for dashboards and alert grouping.
pub fn app_policy_report_code_count(
  report : AppPolicyReport,
  code : String,
) -> Int {
  let mut count = 0
  for issue in report.issues {
    if issue.code == code {
      count = count + 1
    }
  }
  count
}

///|
/// Test whether a report contains a particular admission failure class.
pub fn app_policy_report_has_code(
  report : AppPolicyReport,
  code : String,
) -> Bool {
  app_policy_report_code_count(report, code) > 0
}

///|
/// Return the first issue code for a stable caller-facing rejection reason.
pub fn app_ingest_primary_issue(result : AppIngestResult) -> String? {
  if result.report.issues.length() == 0 {
    None
  } else {
    Some(result.report.issues[0].code)
  }
}

///|
/// Check the encoded collection against both storage and transport budgets.
pub fn app_policy_collection_fits_transport(
  values : Array[CborValue],
  policy : AppPolicy,
) -> Bool {
  app_policy_many_within_budget(values, policy) &&
  cbor_policy_many_encoded_bytes(values) <= app_policy_transport_limit(policy)
}