///|
/// A non-blocking warning about a policy that is valid but may be too broad,
/// redundant, or ineffective.
pub(all) struct PolicyWarning {
  code : String
  location : String
  message : String
} derive(Debug, Eq)

///|
fn policy_warning(
  code : String,
  location : String,
  message : String,
) -> PolicyWarning {
  { code, location, message }
}

///|
fn rule_body_equal(left : Rule, right : Rule) -> Bool {
  left.effect == right.effect &&
  left.subject == right.subject &&
  left.action == right.action &&
  left.resource == right.resource &&
  left.condition == right.condition
}

///|
fn condition_never_satisfied(condition : Condition) -> Bool {
  match condition {
    Not(Always) | Not(Exists(_)) => true
    AllOf(children) => {
      for child in children {
        if condition_never_satisfied(child) {
          return true
        }
      }
      false
    }
    AnyOf(children) => {
      if children.length() == 0 {
        return true
      }
      for child in children {
        if !condition_never_satisfied(child) {
          return false
        }
      }
      true
    }
    _ => false
  }
}

///|
/// Find valid but suspicious policy constructs. Warnings do not block JSON
/// loading or authorization; `validate` remains the strict error check.
pub fn Policy::lint(self : Policy) -> Array[PolicyWarning] {
  let warnings : Array[PolicyWarning] = []
  for role, permissions in self.role_permissions {
    let seen : Map[String, Bool] = {}
    for index, permission in permissions {
      let location = "roles." + role + "." + index.to_string()
      if permission == "*" {
        warnings.push(
          policy_warning(
            "wildcard_permission", location, "role grants every action",
          ),
        )
      }
      if seen.contains(permission) {
        warnings.push(
          policy_warning("duplicate_permission", location, permission),
        )
      }
      seen[permission] = true
    }
  }
  for role, parents in self.role_parents {
    let seen : Map[String, Bool] = {}
    for index, parent in parents {
      let location = "role_parents." + role + "." + index.to_string()
      if seen.contains(parent) {
        warnings.push(policy_warning("duplicate_parent", location, parent))
      }
      seen[parent] = true
    }
  }
  for index, rule in self.rules {
    let location = "rules." + index.to_string()
    if rule.effect is Allow &&
      rule.subject is Any &&
      rule.action is Any &&
      rule.resource is Any &&
      rule.condition is Always {
      warnings.push(
        policy_warning(
          "unrestricted_allow", location, "rule grants every request",
        ),
      )
    }
    if rule.effect is Allow && condition_never_satisfied(rule.condition) {
      warnings.push(
        policy_warning(
          "ineffective_allow", location, "condition cannot become satisfied",
        ),
      )
    }
    for previous_index in 0.. Json {
  Json::object({
    "code": Json::string(self.code),
    "location": Json::string(self.location),
    "message": Json::string(self.message),
  })
}