///|
pub(all) enum EventPredicate {
  TopicMatches(String)
  HeaderEquals(String, String)
  HeaderExists(String)
  PayloadExists(String)
  TextEquals(String, String)
  TextContains(String, String)
  IntAtLeast(String, Int)
  IntAtMost(String, Int)
  IntBetween(String, Int, Int)
  BoolEquals(String, Bool)
  AllOf(Array[EventPredicate])
  AnyOf(Array[EventPredicate])
  Not(EventPredicate)
} derive(Eq, @debug.Debug)

///|
pub(all) struct RuleResult {
  passed : Bool
  code : String
  reason : String
} derive(Eq, @debug.Debug)

///|
pub(all) enum RuleDecision {
  Accept(String)
  Reject(String)
} derive(Eq, @debug.Debug)

///|
pub(all) struct RuleSet {
  name : String
  predicates : Array[EventPredicate]
} derive(Eq, @debug.Debug)

///|
pub fn rule_set(
  name : StringView,
  predicates : ArrayView[EventPredicate],
) -> RuleSet {
  { name: name.to_owned(), predicates: predicates.to_owned() }
}

///|
pub fn RuleResult::ok(code : StringView, reason : StringView) -> RuleResult {
  { passed: true, code: code.to_owned(), reason: reason.to_owned() }
}

///|
pub fn RuleResult::fail(code : StringView, reason : StringView) -> RuleResult {
  { passed: false, code: code.to_owned(), reason: reason.to_owned() }
}

///|
pub fn EventPredicate::evaluate(
  self : EventPredicate,
  event : Envelope,
) -> RuleResult {
  match self {
    TopicMatches(pattern) => evaluate_topic_match(pattern, event)
    HeaderEquals(key, expected) => evaluate_header_equals(event, key, expected)
    HeaderExists(key) => evaluate_header_exists(event, key)
    PayloadExists(path) => evaluate_payload_exists(event, path)
    TextEquals(path, expected) => evaluate_text_equals(event, path, expected)
    TextContains(path, needle) => evaluate_text_contains(event, path, needle)
    IntAtLeast(path, minimum) => evaluate_int_at_least(event, path, minimum)
    IntAtMost(path, maximum) => evaluate_int_at_most(event, path, maximum)
    IntBetween(path, minimum, maximum) =>
      evaluate_int_between(event, path, minimum, maximum)
    BoolEquals(path, expected) => evaluate_bool_equals(event, path, expected)
    AllOf(predicates) => evaluate_all(predicates, event)
    AnyOf(predicates) => evaluate_any(predicates, event)
    Not(predicate) => {
      let result = predicate.evaluate(event)
      if result.passed {
        RuleResult::fail("not", "negated predicate passed: \{result.reason}")
      } else {
        RuleResult::ok("not", "negated predicate failed as expected")
      }
    }
  }
}

///|
pub fn RuleSet::evaluate(self : RuleSet, event : Envelope) -> RuleResult {
  evaluate_all(self.predicates, event)
}

///|
pub fn RuleSet::decide(self : RuleSet, event : Envelope) -> RuleDecision {
  let result = self.evaluate(event)
  if result.passed {
    Accept("\{self.name}: \{result.reason}")
  } else {
    Reject("\{self.name}: \{result.reason}")
  }
}

///|
pub fn RuleDecision::to_handler_result(self : RuleDecision) -> HandlerResult {
  match self {
    Accept(message) => HandlerAck(message)
    Reject(reason) => HandlerDrop(reason)
  }
}

///|
fn evaluate_topic_match(pattern : String, event : Envelope) -> RuleResult {
  match topic_pattern(pattern) {
    Err(err) => RuleResult::fail("topic.invalid", err.message())
    Ok(parsed) =>
      match parsed.matches_topic(event.topic) {
        Err(err) => RuleResult::fail("topic.invalid", err.message())
        Ok(true) => RuleResult::ok("topic.match", "topic matched \{pattern}")
        Ok(false) =>
          RuleResult::fail(
            "topic.mismatch",
            "topic \{event.topic} did not match \{pattern}",
          )
      }
  }
}

///|
fn evaluate_header_exists(event : Envelope, key : String) -> RuleResult {
  match event.header(key) {
    Some(_) => RuleResult::ok("header.exists", "header \{key} exists")
    None => RuleResult::fail("header.missing", "header \{key} is missing")
  }
}

///|
fn evaluate_header_equals(
  event : Envelope,
  key : String,
  expected : String,
) -> RuleResult {
  match event.header(key) {
    Some(actual) if actual == expected =>
      RuleResult::ok("header.equals", "header \{key} matched")
    Some(actual) =>
      RuleResult::fail(
        "header.mismatch",
        "header \{key} expected \{expected} but got \{actual}",
      )
    None => RuleResult::fail("header.missing", "header \{key} is missing")
  }
}

///|
fn evaluate_payload_exists(event : Envelope, path : String) -> RuleResult {
  match event.payload_path(path) {
    Some(_) => RuleResult::ok("payload.exists", "payload path \{path} exists")
    None =>
      RuleResult::fail("payload.missing", "payload path \{path} is missing")
  }
}

///|
fn evaluate_text_equals(
  event : Envelope,
  path : String,
  expected : String,
) -> RuleResult {
  match event.payload_path(path) {
    Some(VText(actual)) if actual == expected =>
      RuleResult::ok("text.equals", "payload text \{path} matched")
    Some(VText(actual)) =>
      RuleResult::fail(
        "text.mismatch",
        "payload text \{path} expected \{expected} but got \{actual}",
      )
    Some(_) => RuleResult::fail("text.type", "payload path \{path} is not text")
    None =>
      RuleResult::fail("payload.missing", "payload path \{path} is missing")
  }
}

///|
fn evaluate_text_contains(
  event : Envelope,
  path : String,
  needle : String,
) -> RuleResult {
  match event.payload_path(path) {
    Some(VText(actual)) if actual.contains(needle) =>
      RuleResult::ok("text.contains", "payload text \{path} contains \{needle}")
    Some(VText(_)) =>
      RuleResult::fail(
        "text.not_contains",
        "payload text \{path} does not contain \{needle}",
      )
    Some(_) => RuleResult::fail("text.type", "payload path \{path} is not text")
    None =>
      RuleResult::fail("payload.missing", "payload path \{path} is missing")
  }
}

///|
fn evaluate_int_at_least(
  event : Envelope,
  path : String,
  minimum : Int,
) -> RuleResult {
  match event.payload_path(path) {
    Some(VInt(actual)) if actual >= minimum =>
      RuleResult::ok("int.min", "payload int \{path} >= \{minimum}")
    Some(VInt(actual)) =>
      RuleResult::fail(
        "int.too_small",
        "payload int \{path} expected >= \{minimum} but got \{actual}",
      )
    Some(_) => RuleResult::fail("int.type", "payload path \{path} is not int")
    None =>
      RuleResult::fail("payload.missing", "payload path \{path} is missing")
  }
}

///|
fn evaluate_int_at_most(
  event : Envelope,
  path : String,
  maximum : Int,
) -> RuleResult {
  match event.payload_path(path) {
    Some(VInt(actual)) if actual <= maximum =>
      RuleResult::ok("int.max", "payload int \{path} <= \{maximum}")
    Some(VInt(actual)) =>
      RuleResult::fail(
        "int.too_large",
        "payload int \{path} expected <= \{maximum} but got \{actual}",
      )
    Some(_) => RuleResult::fail("int.type", "payload path \{path} is not int")
    None =>
      RuleResult::fail("payload.missing", "payload path \{path} is missing")
  }
}

///|
fn evaluate_int_between(
  event : Envelope,
  path : String,
  minimum : Int,
  maximum : Int,
) -> RuleResult {
  if minimum > maximum {
    return RuleResult::fail(
      "int.range.invalid",
      "minimum \{minimum} is greater than maximum \{maximum}",
    )
  }
  let low = evaluate_int_at_least(event, path, minimum)
  if !low.passed {
    return low
  }
  evaluate_int_at_most(event, path, maximum)
}

///|
fn evaluate_bool_equals(
  event : Envelope,
  path : String,
  expected : Bool,
) -> RuleResult {
  match event.payload_path(path) {
    Some(VBool(actual)) if actual == expected =>
      RuleResult::ok("bool.equals", "payload bool \{path} matched")
    Some(VBool(actual)) =>
      RuleResult::fail(
        "bool.mismatch",
        "payload bool \{path} expected \{expected} but got \{actual}",
      )
    Some(_) => RuleResult::fail("bool.type", "payload path \{path} is not bool")
    None =>
      RuleResult::fail("payload.missing", "payload path \{path} is missing")
  }
}

///|
fn evaluate_all(
  predicates : Array[EventPredicate],
  event : Envelope,
) -> RuleResult {
  for predicate in predicates {
    let result = predicate.evaluate(event)
    if !result.passed {
      return result
    }
  }
  RuleResult::ok("all", "all predicates passed")
}

///|
fn evaluate_any(
  predicates : Array[EventPredicate],
  event : Envelope,
) -> RuleResult {
  guard predicates.length() > 0 else {
    return RuleResult::fail("any.empty", "no predicates provided")
  }
  let reasons : Array[String] = []
  for predicate in predicates {
    let result = predicate.evaluate(event)
    if result.passed {
      return result
    }
    reasons.push(result.reason)
  }
  RuleResult::fail("any.none", reasons.join("; "))
}