///|
pub(all) enum ProcessPolicy {
  Response(
    id~ : String,
    trigger~ : String,
    target~ : String,
    within_ms~ : Int64?
  )
  Precedence(id~ : String, trigger~ : String, required~ : String)
  NotCoexist(id~ : String, first~ : String, second~ : String)
  SeparationOfDuties(id~ : String, first~ : String, second~ : String)
}

///|
pub(all) enum PolicyStatus {
  Satisfied
  Violated
  Inconclusive
  NotApplicable
} derive(Eq, @debug.Debug)

///|
pub extend PolicyStatus with Eq::{equal, not_equal}

///|
pub extend PolicyStatus with @debug.Debug::{to_repr}

///|
pub(all) struct PolicyResult {
  case_id : String
  policy : String
  status : PolicyStatus
  event_ids : Array[String]
}

///|
fn ProcessPolicy::identity(self : ProcessPolicy) -> String {
  match self {
    Response(id~, ..)
    | Precedence(id~, ..)
    | NotCoexist(id~, ..)
    | SeparationOfDuties(id~, ..) => id
  }
}

///|
fn ProcessPolicy::labels(self : ProcessPolicy) -> (String, String) {
  match self {
    Response(trigger~, target~, ..) => (trigger, target)
    Precedence(trigger~, required~, ..) => (trigger, required)
    NotCoexist(first~, second~, ..) | SeparationOfDuties(first~, second~, ..) =>
      (first, second)
  }
}

///|
/// Trace-order declarative checks. Response targets must be later occurrences;
/// bounded response requires target.started >= trigger.completed. Missing
/// resource data yields Inconclusive, never a silent separation-of-duty pass.
pub fn EventLog::check_policies(
  self : EventLog,
  policies : Array[ProcessPolicy],
  max_work~ : Int32 = 10000000,
  max_findings~ : Int32 = 100000,
) -> Array[PolicyResult] raise ProcessError {
  model_require(
    max_work >= 1 && max_work <= 100000000,
    "policy.work",
    "invalid work limit",
  )
  model_require(
    max_findings >= 0 && max_findings <= 1000000,
    "policy.findings",
    "invalid evidence limit",
  )
  if policies.length() > 256 {
    raise Capacity(resource="policies", limit=256)
  }
  if self.event_count.to_int64() * policies.length().to_int64() >
    max_work.to_int64() {
    raise Capacity(resource="policy event checks", limit=max_work)
  }
  if self.traces.length().to_int64() * policies.length().to_int64() > 1000000L {
    raise Capacity(resource="policy results", limit=1000000)
  }
  let ids : Map[String, Bool] = Map([])
  for policy in policies {
    let id = policy.identity()
    process_label(id, "policy.id")
    model_require(!ids.contains(id), "policy.id", "duplicate policy")
    ids[id] = true
    let (a, b) = policy.labels()
    process_label(a, "policy.activity")
    process_label(b, "policy.activity")
    match policy {
      Response(within_ms~, ..) =>
        if within_ms is Some(bound) {
          model_require(
            bound >= 0L && bound <= 9007199254740991L,
            "policy.time",
            "invalid duration",
          )
        }
      SeparationOfDuties(..) =>
        model_require(
          a != b,
          "policy.duties",
          "distinct activity labels required",
        )
      _ => ()
    }
  }
  let results = []
  let mut findings = 0
  for trace in self.traces {
    for policy in policies {
      let (a, b) = policy.labels()
      let evidence : Array[String] = []
      let mut applies = false
      let mut incomplete = false
      match policy {
        Response(within_ms~, ..) => {
          let mut next : LogEvent? = None
          let mut index = trace.events.length()
          while index > 0 {
            index -= 1
            let event = trace.events[index]
            if event.activity == a {
              applies = true
              let satisfied = match next {
                None => false
                Some(target) =>
                  match within_ms {
                    None => true
                    Some(bound) =>
                      target.started >= event.completed &&
                      target.started - event.completed <= bound
                  }
              }
              if !satisfied {
                evidence.push(event.id)
              }
            }
            // For time-bounded checks the first target by start time is minimal.
            // An overlapping target cannot satisfy nonnegative gap, but a later
            // non-overlapping target might: bounded lookup handles that below.
            if event.activity == b {
              next = Some(event)
            }
          }
          if within_ms is Some(bound) {
            evidence.clear()
            let targets : Array[(Int32, LogEvent)] = []
            for i, event in trace.events {
              if event.activity == b {
                targets.push((i, event))
              }
            }
            for i, event in trace.events {
              if event.activity != a {
                continue
              }
              // Binary search the earliest target starting no earlier than the
              // trigger completes; position must also be strictly later.
              let mut low = 0
              let mut high = targets.length()
              while low < high {
                let middle = (low + high) / 2
                if targets[middle].1.started < event.completed ||
                  targets[middle].0 <= i {
                  low = middle + 1
                } else {
                  high = middle
                }
              }
              if low >= targets.length() ||
                targets[low].1.started - event.completed > bound {
                evidence.push(event.id)
              }
            }
          }
        }
        Precedence(..) => {
          let mut seen = false
          for event in trace.events {
            if event.activity == a {
              applies = true
              if !seen {
                evidence.push(event.id)
              }
            }
            if event.activity == b {
              seen = true
            }
          }
        }
        NotCoexist(..) => {
          let first = trace.events.filter(event => event.activity == a)
          let second = trace.events.filter(event => event.activity == b)
          applies = !first.is_empty() || !second.is_empty()
          if !first.is_empty() && !second.is_empty() {
            evidence.push(first[0].id)
            evidence.push(second[0].id)
          }
        }
        SeparationOfDuties(..) => {
          let actors : Map[String, String] = Map([])
          let mut first = false
          let mut second = false
          for event in trace.events {
            if event.activity == a {
              first = true
              match event.resource {
                Some(actor) => actors[actor] = event.id
                None => incomplete = true
              }
            }
            if event.activity == b {
              second = true
              if event.resource is None {
                incomplete = true
              }
            }
          }
          applies = first && second
          for event in trace.events {
            if event.activity == b && event.resource is Some(actor) {
              if actors.get(actor) is Some(prior) {
                evidence.push(prior)
                evidence.push(event.id)
              }
            }
          }
        }
      }
      if evidence.length() > max_findings - findings {
        raise Capacity(resource="policy evidence IDs", limit=max_findings)
      }
      findings += evidence.length()
      results.push({
        case_id: trace.id,
        policy: policy.identity(),
        status: if !applies {
          NotApplicable
        } else if !evidence.is_empty() {
          Violated
        } else if incomplete {
          Inconclusive
        } else {
          Satisfied
        },
        event_ids: evidence,
      })
    }
  }
  results
}

///|
pub fn policy_results_to_json(results : Array[PolicyResult]) -> Json {
  Json::array(
    results.map(result => {
      "case_id": result.case_id,
      "policy": result.policy,
      "status": match result.status {
        Satisfied => "satisfied"
        Violated => "violated"
        Inconclusive => "inconclusive"
        NotApplicable => "not_applicable"
      },
      "event_ids": Json::array(result.event_ids.map(Json::string)),
    }),
  )
}