///|
pub(all) enum Action {
  Keep
  Discard
  FileInto(String)
  Redirect(String)
} derive(Eq, Debug)

///|
pub(all) struct Plan {
  actions : Array[Action]
  trace : Array[TraceEvent]
  implicit_keep : Bool
  steps : Int
  comparisons : Int
} derive(Eq, Debug)

///|
priv struct Delivery {
  actions : Array[Action]
  mut implicit_keep : Bool
  mut discarded : Bool
}

///|
fn Delivery::add(
  self : Delivery,
  action : Action,
  rt : Runtime,
  span : Span,
) -> Unit raise SieveError {
  if !self.actions.any(existing => same_delivery(existing, action)) {
    if self.actions.length() >= rt.budget.limits.actions {
      fail("limit.actions", "delivery action limit exceeded", span)
    }
    self.actions.push(action)
  }
}

///|
fn safe_destination(
  destination : String,
  kind : String,
  span : Span,
) -> Unit raise SieveError {
  if destination.is_empty() || !single_line(destination) {
    fail(
      "runtime.destination", "destination must be nonempty and single-line", span,
    )
  }
  if kind == "redirect" && parse_mailbox(destination) is None {
    fail(
      "runtime.redirect", "redirect requires a supported bare addr-spec", span,
    )
  }
}

///|
fn Runtime::execute(
  self : Runtime,
  body : Array[Statement],
  delivery : Delivery,
) -> Bool raise SieveError {
  for statement in body {
    match statement {
      Branch(branches, otherwise, span) => {
        self.budget.step(span)
        let mut selected = false
        for branch in branches {
          if self.condition(branch.condition) {
            selected = true
            if self.execute(branch.body, delivery) {
              return true
            }
            break
          }
        }
        if !selected && self.execute(otherwise, delivery) {
          return true
        }
      }
      Command(name, args, span) => {
        self.budget.step(span)
        let a = split_args(args, span)
        match name {
          "require" => ()
          "set" => self.set_variable(a, span)
          "stop" => {
            self.record("control", "stop", span, None)
            return true
          }
          "keep" => {
            delivery.add(Keep, self, span)
            delivery.implicit_keep = false
            self.record("action", "keep", span, None)
          }
          "discard" => {
            delivery.discarded = true
            delivery.implicit_keep = false
            self.record("action", "discard", span, None)
          }
          "fileinto" | "redirect" => {
            let destination = self.expand(single_arg(a.values[0], span), span)
            safe_destination(destination, name, span)
            let action = if name == "fileinto" {
              FileInto(destination)
            } else {
              Redirect(destination)
            }
            delivery.add(action, self, span)
            if !a.flags.contains("copy") {
              delivery.implicit_keep = false
            }
            self.record("action", name, span, None)
          }
          _ =>
            fail(
              "runtime.command", "unsupported command reached interpreter", span,
            )
        }
      }
    }
  }
  false
}

///|
pub fn Program::run(
  self : Program,
  message : Message,
  options? : ExecutionOptions = ExecutionOptions::default(),
) -> Plan raise SieveError {
  options.check()
  let checked = Message::new(
    message.headers,
    message.size,
    envelope_from=message.envelope_from,
    envelope_to=message.envelope_to,
    limits=self.limits,
  )
  let rt : Runtime = {
    message: checked,
    budget: new_budget(self.limits),
    trace: [],
    variables: Map([]),
    captures: Array::make(10, ""),
    variables_enabled: self.capabilities.contains("variables"),
    options,
  }
  let delivery : Delivery = {
    actions: [],
    implicit_keep: true,
    discarded: false,
  }
  ignore(rt.execute(self.syntax.statements, delivery))
  if delivery.implicit_keep {
    delivery.add(Keep, rt, origin())
  }
  if delivery.actions.is_empty() && delivery.discarded {
    delivery.add(Discard, rt, origin())
  }
  {
    actions: delivery.actions,
    trace: rt.trace,
    implicit_keep: delivery.implicit_keep,
    steps: rt.budget.steps,
    comparisons: rt.budget.comparisons,
  }
}