///|
/// Anchored glob matching: '*' spans zero or more UTF-16 units, '?' one unit.
/// Backslash is an ordinary character; no regex, path or URL normalization occurs.
/// A linear-space greedy matcher avoids recursive wildcard expansion.
pub fn glob_matches(pattern : String, value : String) -> Bool {
  let mut p = 0
  let mut v = 0
  let mut star = -1
  let mut retry = 0
  while v < value.length() {
    if p < pattern.length() && (pattern[p] == 63 || pattern[p] == value[v]) {
      p += 1
      v += 1
    } else if p < pattern.length() && pattern[p] == 42 {
      star = p
      p += 1
      retry = v
    } else if star >= 0 {
      retry += 1
      v = retry
      p = star + 1
    } else {
      return false
    }
  }
  while p < pattern.length() && pattern[p] == 42 {
    p += 1
  }
  p == pattern.length()
}

///|
fn rule_matches(rule : Rule, event : Event) -> Bool {
  glob_matches(rule.tool, event.tool) &&
  glob_matches(rule.resource, event.resource)
}

///|
fn check_text(
  value : String,
  path : String,
  max~ : Int32 = 256,
) -> Unit raise InputError {
  if value.is_empty() ||
    value.length() > max ||
    value.any(c => c < ' ' || c == '\u007f') {
    invalid(path, "expected nonempty bounded text without control characters")
  }
}

///|
fn unique_strings(
  values : Array[String],
  path : String,
) -> Unit raise InputError {
  if values.length() > 64 {
    invalid(path, "at most 64 entries")
  }
  let seen : Map[String, Bool] = Map([])
  for value in values {
    check_text(value, path)
    if seen.contains(value) {
      invalid(path, "duplicate entry")
    }
    seen[value] = true
  }
}

///|
fn validate_policy(policy : Policy) -> Unit raise InputError {
  check_text(policy.name, "policy.name")
  if policy.default_decision != "allow" && policy.default_decision != "deny" {
    invalid("policy.default_decision", "expected allow or deny")
  }
  if policy.rules.length() > 128 {
    invalid("policy.rules", "at most 128 rules")
  }
  let ids : Map[String, Bool] = Map([])
  for i, rule in policy.rules {
    let path = "policy.rules[\{i}]"
    check_text(rule.id, "\{path}.id")
    if ids.contains(rule.id) {
      invalid(path, "duplicate rule id")
    }
    ids[rule.id] = true
    check_text(rule.tool, "\{path}.tool")
    check_text(rule.resource, "\{path}.resource", max=1024)
    unique_strings(rule.labels, "\{path}.labels")
    match rule.kind {
      "allow" | "deny" | "require_approval" => ()
      "block_labels" =>
        if rule.labels.is_empty() {
          invalid(path, "block_labels requires labels")
        }
      "budget" | "rate" => {
        guard rule.limit is Some(limit) && limit >= 0 else {
          invalid(path, "budget/rate requires nonnegative limit")
          return
        }
        if rule.kind == "rate" && rule.window_ms is None {
          invalid(path, "rate requires window_ms")
        }
      }
      "require_before" => {
        guard rule.prerequisite is Some(pattern) else {
          invalid(path, "require_before requires prerequisite")
          return
        }
        check_text(pattern, "\{path}.prerequisite")
      }
      _ => invalid(path, "unsupported rule kind")
    }
    if rule.window_ms is Some(window) && window <= 0 {
      invalid(path, "window_ms must be positive")
    }
    if rule.kind != "block_labels" && !rule.labels.is_empty() {
      invalid(path, "inapplicable labels")
    }
    if rule.kind != "budget" && rule.kind != "rate" && rule.limit is Some(_) {
      invalid(path, "inapplicable limit")
    }
    if rule.kind != "budget" &&
      rule.kind != "rate" &&
      rule.kind != "require_before" &&
      rule.window_ms is Some(_) {
      invalid(path, "inapplicable window_ms")
    }
    if rule.kind != "require_before" && rule.prerequisite is Some(_) {
      invalid(path, "inapplicable prerequisite")
    }
  }
}