///|
/// Failures that are meaningful to a booking UI or command-line adapter.
pub enum PlanError {
  DuplicateResourceId(String)
  UnknownResource(String)
  InsufficientCapacity(Int)
  NoFeasibleSlot(String)
  CannotReserve(String)
  CannotRelease(String)
} derive(Eq, Debug)

///|
pub fn PlanError::render(self : PlanError) -> String {
  match self {
    DuplicateResourceId(id) => "duplicate resource: " + id
    UnknownResource(id) => "unknown resource: " + id
    InsufficientCapacity(capacity) =>
      "insufficient capacity: " + capacity.to_string()
    NoFeasibleSlot(id) => "no feasible slot: " + id
    CannotReserve(id) => "could not reserve resource: " + id
    CannotRelease(id) => "could not release resource: " + id
  }
}

///| A concrete answer to a booking query. `reserved` includes the request's

///|
/// cleanup/setup buffers while `event` is what an end user sees.
pub struct Allocation {
  request_id : String
  event : Interval
  reserved : Interval
  resource_ids : Array[String]
} derive(Debug)

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

///|
pub fn Allocation::event(self : Allocation) -> Interval {
  self.event
}

///|
pub fn Allocation::reserved(self : Allocation) -> Interval {
  self.reserved
}

///|
pub fn Allocation::resource_ids(self : Allocation) -> Array[String] {
  self.resource_ids.copy()
}

///|
pub fn Allocation::render(self : Allocation) -> String {
  let mut names = ""
  for index in 0.. 0 {
      names = names + ","
    }
    names = names + self.resource_ids[index]
  }
  self.request_id + " event=" + self.event.render() + " resources=" + names
}

///| Successful reservation result.  `planner` is the successor state that

///|
/// includes the reservation; the original planner remains unchanged.
pub struct Reservation {
  allocation : Allocation
  planner : Planner
} derive(Debug)

///|
pub fn Reservation::allocation(self : Reservation) -> Allocation {
  self.allocation
}

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

///|
/// Output of a successful move.  Retaining both allocations lets a caller

///| explain the change and write an audit record without reverse engineering

///|
/// resource state.
pub struct RescheduleResult {
  previous : Allocation
  replacement : Allocation
  planner : Planner
} derive(Debug)

///|
pub fn RescheduleResult::previous(self : RescheduleResult) -> Allocation {
  self.previous
}

///|
pub fn RescheduleResult::replacement(self : RescheduleResult) -> Allocation {
  self.replacement
}

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

///| A small in-memory planning state.  Persistence, authentication and time

///|
/// zone conversion deliberately stay outside this portable core library.
pub struct Planner {
  resources : Array[ResourceCalendar]
} derive(Debug)

///|
pub fn Planner::new(
  resources : Array[ResourceCalendar],
) -> Result[Planner, PlanError] {
  for left in 0.. Array[ResourceCalendar] {
  self.resources.copy()
}

///|
fn Planner::resource(self : Planner, id : String) -> ResourceCalendar? {
  for resource in self.resources {
    if resource.id() == id {
      return Some(resource)
    }
  }
  None
}

///|
fn Planner::required_calendars(
  self : Planner,
  request : BookingRequest,
) -> Result[Array[ResourceCalendar], PlanError] {
  self.calendars_for_ids(request.required_resources())
}

///|
fn Planner::calendars_for_ids(
  self : Planner,
  ids : Array[String],
) -> Result[Array[ResourceCalendar], PlanError] {
  let selected : Array[ResourceCalendar] = []
  for id in ids {
    match self.resource(id) {
      Some(calendar) => selected.push(calendar)
      None => return Err(UnknownResource(id))
    }
  }
  Ok(selected)
}

///|
fn request_search_window(request : BookingRequest) -> Interval {
  {
    start: request.horizon().start() - request.buffer_before,
    end: request.horizon().end() + request.buffer_after,
  }
}

///|
fn allocation_from_free(
  free : IntervalSet,
  request : BookingRequest,
  ids : Array[String],
) -> Allocation? {
  let search = request_search_window(request)
  match free.first_fit(request.reserved_duration(), search.start()) {
    None => None
    Some(reserved) => {
      let event = request.event_from_reserved(reserved)
      if event.start() < request.horizon().start() ||
        event.end() > request.horizon().end() {
        None
      } else {
        Some({ request_id: request.id(), event, reserved, resource_ids: ids })
      }
    }
  }
}

///|
fn common_free(
  calendars : Array[ResourceCalendar],
  horizon : Interval,
) -> IntervalSet {
  let mut output = calendars[0].free_within(horizon)
  for index in 1.. Result[Allocation, PlanError] {
  let requested = request.required_resources()
  let search = request_search_window(request)
  if requested.length() > 0 {
    let calendars = match self.required_calendars(request) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let free = common_free(calendars, search)
    match allocation_from_free(free, request, requested) {
      Some(allocation) => Ok(allocation)
      None => Err(NoFeasibleSlot(request.id()))
    }
  } else {
    let mut best : Allocation? = None
    let mut found_capacity = false
    for calendar in self.resources {
      if calendar.capacity() < request.capacity_needed() {
        continue
      }
      found_capacity = true
      let ids = [calendar.id()]
      match allocation_from_free(calendar.free_within(search), request, ids) {
        None => ()
        Some(candidate) =>
          match best {
            None => best = Some(candidate)
            Some(current) =>
              if candidate.event().start() < current.event().start() ||
                (
                  candidate.event().start() == current.event().start() &&
                  candidate.resource_ids()[0] < current.resource_ids()[0]
                ) {
                best = Some(candidate)
              }
          }
      }
    }
    match best {
      Some(allocation) => Ok(allocation)
      None =>
        if found_capacity {
          Err(NoFeasibleSlot(request.id()))
        } else {
          Err(InsufficientCapacity(request.capacity_needed()))
        }
    }
  }
}

///|
/// Reserve the earliest matching slot and return a successor planner.  Every
/// target calendar is validated before the successor is returned, preventing
/// partial multi-resource bookings.
pub fn Planner::reserve_earliest(
  self : Planner,
  request : BookingRequest,
) -> Result[Reservation, PlanError] {
  let allocation = match self.find_earliest(request) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let updated : Array[ResourceCalendar] = []
  for calendar in self.resources {
    let mut affected = false
    for id in allocation.resource_ids() {
      if calendar.id() == id {
        affected = true
      }
    }
    if affected {
      match calendar.reserve(allocation.reserved()) {
        Ok(next) => updated.push(next)
        Err(_) => return Err(CannotReserve(calendar.id()))
      }
    } else {
      updated.push(calendar)
    }
  }
  match Planner::new(updated) {
    Ok(planner) => Ok({ allocation, planner })
    Err(error) => Err(error)
  }
}

///|
/// Cancel a known allocation.  All affected calendars are checked before a

///|
/// successor is built, preventing a partial release of a multi-resource slot.
pub fn Planner::cancel(
  self : Planner,
  allocation : Allocation,
) -> Result[Planner, PlanError] {
  for id in allocation.resource_ids() {
    match self.resource(id) {
      None => return Err(UnknownResource(id))
      Some(calendar) =>
        if !calendar.has_reservation(allocation.reserved()) {
          return Err(CannotRelease(id))
        }
    }
  }
  let updated : Array[ResourceCalendar] = []
  for calendar in self.resources {
    let mut affected = false
    for id in allocation.resource_ids() {
      if calendar.id() == id {
        affected = true
      }
    }
    if affected {
      match calendar.release(allocation.reserved()) {
        Ok(next) => updated.push(next)
        Err(_) => return Err(CannotRelease(calendar.id()))
      }
    } else {
      updated.push(calendar)
    }
  }
  Planner::new(updated)
}

///|
/// Move an allocation to the earliest slot matching `replacement`.  A failed

///|
/// replacement attempt does not mutate the caller's original planner.
pub fn Planner::reschedule(
  self : Planner,
  allocation : Allocation,
  replacement : BookingRequest,
) -> Result[RescheduleResult, PlanError] {
  let released = match self.cancel(allocation) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match released.reserve_earliest(replacement) {
    Ok(reservation) =>
      Ok({
        previous: allocation,
        replacement: reservation.allocation(),
        planner: reservation.planner(),
      })
    Err(error) => Err(error)
  }
}

///|
pub fn Planner::explain_unavailability(
  self : Planner,
  request : BookingRequest,
) -> String {
  match self.find_earliest(request) {
    Ok(allocation) => "available: " + allocation.render()
    Err(DuplicateResourceId(id)) => "invalid planner: duplicate resource " + id
    Err(UnknownResource(id)) => "unknown required resource: " + id
    Err(InsufficientCapacity(capacity)) =>
      "no resource supplies required capacity " + capacity.to_string()
    Err(NoFeasibleSlot(id)) => "no free slot in horizon for request " + id
    Err(CannotReserve(id)) => "resource changed while reserving: " + id
    Err(CannotRelease(id)) => "allocation is not active on resource: " + id
  }
}