///| Lifecycle state for a booking record.  Records are retained after

///|
/// cancellation so an embedding application can preserve an audit trail.
pub enum BookingState {
  Active
  Cancelled
} derive(Eq, Debug)

///|
pub fn BookingState::render(self : BookingState) -> String {
  match self {
    Active => "active"
    Cancelled => "cancelled"
  }
}

///|
/// A named allocation held in the ledger.
pub struct BookingRecord {
  booking_id : String
  allocation : Allocation
  state : BookingState
} derive(Debug)

///|
pub fn BookingRecord::booking_id(self : BookingRecord) -> String {
  self.booking_id
}

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

///|
pub fn BookingRecord::state(self : BookingRecord) -> BookingState {
  self.state
}

///|
pub fn BookingRecord::is_active(self : BookingRecord) -> Bool {
  self.state == Active
}

///|
pub fn BookingRecord::render(self : BookingRecord) -> String {
  self.booking_id + " " + self.state.render() + " " + self.allocation.render()
}

///| Append-only transition record.  It is intentionally simple enough for a

///|
/// UI, CLI or persistence adapter to map into its own storage format.
pub enum LedgerEventKind {
  Created(Allocation)
  Cancelled(Allocation)
  Rescheduled(previous~ : Allocation, replacement~ : Allocation)
} derive(Debug)

///|
pub struct LedgerEvent {
  sequence : Int
  booking_id : String
  kind : LedgerEventKind
} derive(Debug)

///|
pub fn LedgerEvent::sequence(self : LedgerEvent) -> Int {
  self.sequence
}

///|
pub fn LedgerEvent::booking_id(self : LedgerEvent) -> String {
  self.booking_id
}

///|
pub fn LedgerEvent::kind(self : LedgerEvent) -> LedgerEventKind {
  self.kind
}

///|
pub enum LedgerError {
  EmptyBookingId
  DuplicateBookingId(String)
  UnknownBookingId(String)
  BookingNotActive(String)
  SchedulingFailed(PlanError)
} derive(Eq, Debug)

///|
pub fn LedgerError::render(self : LedgerError) -> String {
  match self {
    EmptyBookingId => "booking id cannot be empty"
    DuplicateBookingId(id) => "booking id already exists: " + id
    UnknownBookingId(id) => "unknown booking id: " + id
    BookingNotActive(id) => "booking is not active: " + id
    SchedulingFailed(error) => error.render()
  }
}

///| An immutable in-memory coordination layer over `Planner`.  This is not a

///| database; it provides a correct state model that a database adapter can

///|
/// persist without duplicating cancellation and rescheduling behavior.
pub struct BookingLedger {
  planner : Planner
  records : Array[BookingRecord]
  events : Array[LedgerEvent]
} derive(Debug)

///|
pub struct LedgerChange {
  ledger : BookingLedger
  record : BookingRecord
} derive(Debug)

///|
pub fn LedgerChange::ledger(self : LedgerChange) -> BookingLedger {
  self.ledger
}

///|
pub fn LedgerChange::record(self : LedgerChange) -> BookingRecord {
  self.record
}

///|
pub fn BookingLedger::new(planner : Planner) -> BookingLedger {
  { planner, records: [], events: [] }
}

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

///|
pub fn BookingLedger::records(self : BookingLedger) -> Array[BookingRecord] {
  self.records.copy()
}

///|
pub fn BookingLedger::events(self : BookingLedger) -> Array[LedgerEvent] {
  self.events.copy()
}

///|
pub fn BookingLedger::active_records(
  self : BookingLedger,
) -> Array[BookingRecord] {
  let output : Array[BookingRecord] = []
  for record in self.records {
    if record.is_active() {
      output.push(record)
    }
  }
  output
}

///|
fn BookingLedger::index_of(self : BookingLedger, booking_id : String) -> Int? {
  for index in 0.. LedgerEvent {
  { sequence: events.length() + 1, booking_id, kind }
}

///|
fn with_event(
  planner : Planner,
  records : Array[BookingRecord],
  events : Array[LedgerEvent],
  booking_id : String,
  kind : LedgerEventKind,
) -> BookingLedger {
  let updated_events = events.copy()
  updated_events.push(next_event(updated_events, booking_id, kind))
  { planner, records, events: updated_events }
}

///|
/// Create and retain the earliest allocation for one application-level ID.
pub fn BookingLedger::book(
  self : BookingLedger,
  booking_id : String,
  request : BookingRequest,
) -> Result[LedgerChange, LedgerError] {
  if booking_id.length() == 0 {
    return Err(EmptyBookingId)
  }
  if self.index_of(booking_id) is Some(_) {
    return Err(DuplicateBookingId(booking_id))
  }
  let reservation = match self.planner.reserve_earliest(request) {
    Ok(value) => value
    Err(error) => return Err(SchedulingFailed(error))
  }
  let record = {
    booking_id,
    allocation: reservation.allocation(),
    state: Active,
  }
  let records = self.records.copy()
  records.push(record)
  let ledger = with_event(
    reservation.planner(),
    records,
    self.events,
    booking_id,
    Created(record.allocation()),
  )
  Ok({ ledger, record })
}

///|
/// Cancel an active record and retain its original allocation in history.
pub fn BookingLedger::cancel(
  self : BookingLedger,
  booking_id : String,
) -> Result[LedgerChange, LedgerError] {
  let index = match self.index_of(booking_id) {
    Some(value) => value
    None => return Err(UnknownBookingId(booking_id))
  }
  let current = self.records[index]
  if !current.is_active() {
    return Err(BookingNotActive(booking_id))
  }
  let planner = match self.planner.cancel(current.allocation()) {
    Ok(value) => value
    Err(error) => return Err(SchedulingFailed(error))
  }
  let record = {
    booking_id: current.booking_id(),
    allocation: current.allocation(),
    state: Cancelled,
  }
  let records = self.records.copy()
  records[index] = record
  let ledger = with_event(
    planner,
    records,
    self.events,
    booking_id,
    Cancelled(current.allocation()),
  )
  Ok({ ledger, record })
}

///|
/// Replace an active record's allocation while preserving its booking ID and

///| recording the before/after pair.  The operation is all-or-nothing because

///|
/// `Planner::reschedule` is immutable.
pub fn BookingLedger::reschedule(
  self : BookingLedger,
  booking_id : String,
  request : BookingRequest,
) -> Result[LedgerChange, LedgerError] {
  let index = match self.index_of(booking_id) {
    Some(value) => value
    None => return Err(UnknownBookingId(booking_id))
  }
  let current = self.records[index]
  if !current.is_active() {
    return Err(BookingNotActive(booking_id))
  }
  let result = match self.planner.reschedule(current.allocation(), request) {
    Ok(value) => value
    Err(error) => return Err(SchedulingFailed(error))
  }
  let record = {
    booking_id: current.booking_id(),
    allocation: result.replacement(),
    state: Active,
  }
  let records = self.records.copy()
  records[index] = record
  let ledger = with_event(
    result.planner(),
    records,
    self.events,
    booking_id,
    Rescheduled(previous=current.allocation(), replacement=result.replacement()),
  )
  Ok({ ledger, record })
}