///| Ordering policy for a set of independent booking requests.  The policies

///|
/// are deterministic so a caller can reproduce and explain a batch result.
pub enum BatchPolicy {
  InputOrder
  PriorityThenEarliest
  ShortestJobFirst
} derive(Eq, Debug)

///|
pub fn batch_policies() -> Array[BatchPolicy] {
  [InputOrder, PriorityThenEarliest, ShortestJobFirst]
}

///|
pub fn BatchPolicy::render(self : BatchPolicy) -> String {
  match self {
    InputOrder => "input-order"
    PriorityThenEarliest => "priority-then-earliest"
    ShortestJobFirst => "shortest-job-first"
  }
}

///| A request plus a caller-defined importance score.  Higher priority wins

///| only when `PriorityThenEarliest` is selected; it never silently changes

///|
/// the default input-order behavior.
pub struct BatchItem {
  key : String
  request : BookingRequest
  priority : Int
} derive(Debug)

///|
pub enum BatchItemError {
  EmptyBatchKey
} derive(Eq, Debug)

///|
pub fn BatchItem::new(
  key : String,
  request : BookingRequest,
  priority? : Int = 0,
) -> Result[BatchItem, BatchItemError] {
  if key.length() == 0 {
    Err(EmptyBatchKey)
  } else {
    Ok({ key, request, priority })
  }
}

///|
pub fn BatchItem::key(self : BatchItem) -> String {
  self.key
}

///|
pub fn BatchItem::request(self : BatchItem) -> BookingRequest {
  self.request
}

///|
pub fn BatchItem::priority(self : BatchItem) -> Int {
  self.priority
}

///|
pub enum BatchError {
  DuplicateBatchKey(String)
  ItemFailure(key~ : String, error~ : PlanError)
} derive(Eq, Debug)

///|
pub fn BatchError::render(self : BatchError) -> String {
  match self {
    DuplicateBatchKey(key) => "duplicate batch key: " + key
    ItemFailure(key~, error~) => key + ": " + error.render()
  }
}

///| Per-item result.  A partial batch retains rejected items as evidence,

///|
/// which is more useful to a booking service than an unexplained omission.
pub enum BatchStatus {
  Scheduled(Allocation)
  Rejected(PlanError)
} derive(Debug)

///|
pub fn BatchStatus::is_scheduled(self : BatchStatus) -> Bool {
  match self {
    Scheduled(_) => true
    Rejected(_) => false
  }
}

///|
pub fn BatchStatus::render(self : BatchStatus) -> String {
  match self {
    Scheduled(allocation) => "scheduled " + allocation.render()
    Rejected(error) => "rejected " + error.render()
  }
}

///|
pub struct BatchOutcome {
  key : String
  request_id : String
  status : BatchStatus
} derive(Debug)

///|
pub fn BatchOutcome::key(self : BatchOutcome) -> String {
  self.key
}

///|
pub fn BatchOutcome::request_id(self : BatchOutcome) -> String {
  self.request_id
}

///|
pub fn BatchOutcome::status(self : BatchOutcome) -> BatchStatus {
  self.status
}

///|
pub struct BatchPlan {
  outcomes : Array[BatchOutcome]
  planner : Planner
  policy : BatchPolicy
} derive(Debug)

///|
pub fn BatchPlan::outcomes(self : BatchPlan) -> Array[BatchOutcome] {
  self.outcomes.copy()
}

///|
pub fn BatchPlan::planner(self : BatchPlan) -> Planner {
  self.planner
}

///|
pub fn BatchPlan::policy(self : BatchPlan) -> BatchPolicy {
  self.policy
}

///|
pub fn BatchPlan::scheduled_count(self : BatchPlan) -> Int {
  let mut count = 0
  for outcome in self.outcomes {
    if outcome.status.is_scheduled() {
      count = count + 1
    }
  }
  count
}

///|
pub fn BatchPlan::rejected_count(self : BatchPlan) -> Int {
  self.outcomes.length() - self.scheduled_count()
}

///|
pub fn BatchPlan::summary(self : BatchPlan) -> String {
  "policy=" +
  self.policy.render() +
  " scheduled=" +
  self.scheduled_count().to_string() +
  " rejected=" +
  self.rejected_count().to_string()
}

///|
fn validate_batch(items : Array[BatchItem]) -> Result[Unit, BatchError] {
  for left in 0.. Bool {
  match policy {
    InputOrder => false
    PriorityThenEarliest =>
      left.priority() > right.priority() ||
      (
        left.priority() == right.priority() &&
        left.request().horizon().start() < right.request().horizon().start()
      )
    ShortestJobFirst =>
      left.request().duration() < right.request().duration() ||
      (
        left.request().duration() == right.request().duration() &&
        left.request().horizon().start() < right.request().horizon().start()
      )
  }
}

///|
fn order_items(
  items : Array[BatchItem],
  policy : BatchPolicy,
) -> Array[BatchItem] {
  if policy == InputOrder {
    return items.copy()
  }
  let ordered : Array[BatchItem] = []
  for item in items {
    let mut index = 0
    while index < ordered.length() &&
          !should_run_before(item, ordered[index], policy) {
      index = index + 1
    }
    ordered.insert(index, item)
  }
  ordered
}

///|
/// Attempt every item in the selected deterministic order.  A rejection does

///| not roll back earlier successes, so callers can present a useful partial

///|
/// result and optionally retry rejected requests later.
pub fn Planner::schedule_batch(
  self : Planner,
  items : Array[BatchItem],
  policy? : BatchPolicy = InputOrder,
) -> Result[BatchPlan, BatchError] {
  match validate_batch(items) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let mut current = self
  let outcomes : Array[BatchOutcome] = []
  for item in order_items(items, policy) {
    let request = item.request()
    match current.reserve_earliest(request) {
      Ok(reservation) => {
        let allocation = reservation.allocation()
        current = reservation.planner()
        outcomes.push({
          key: item.key(),
          request_id: allocation.request_id(),
          status: Scheduled(allocation),
        })
      }
      Err(error) =>
        outcomes.push({
          key: item.key(),
          request_id: request.id(),
          status: Rejected(error),
        })
    }
  }
  Ok({ outcomes, planner: current, policy })
}

///|
/// Schedule a batch only if every request fits.  On failure, the caller gets

///|
/// the failing item and retains the exact original planner state.
pub fn Planner::schedule_batch_atomically(
  self : Planner,
  items : Array[BatchItem],
  policy? : BatchPolicy = InputOrder,
) -> Result[BatchPlan, BatchError] {
  match validate_batch(items) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let mut current = self
  let outcomes : Array[BatchOutcome] = []
  for item in order_items(items, policy) {
    let request = item.request()
    match current.reserve_earliest(request) {
      Ok(reservation) => {
        let allocation = reservation.allocation()
        current = reservation.planner()
        outcomes.push({
          key: item.key(),
          request_id: allocation.request_id(),
          status: Scheduled(allocation),
        })
      }
      Err(error) => return Err(ItemFailure(key=item.key(), error~))
    }
  }
  Ok({ outcomes, planner: current, policy })
}