///| Overlay a baseline availability with explicit temporary openings and

///| closures.  It is useful with a weekly template: normal work hours provide

///| the baseline while maintenance, holiday closures, and exceptional overtime

///|
/// are recorded as small interval sets.
pub struct AvailabilityOverlay {
  baseline : IntervalSet
  openings : IntervalSet
  closures : IntervalSet
} derive(Debug)

///|
pub fn AvailabilityOverlay::new(baseline : IntervalSet) -> AvailabilityOverlay {
  { baseline, openings: IntervalSet::empty(), closures: IntervalSet::empty() }
}

///|
pub fn AvailabilityOverlay::baseline(self : AvailabilityOverlay) -> IntervalSet {
  self.baseline
}

///|
pub fn AvailabilityOverlay::openings(self : AvailabilityOverlay) -> IntervalSet {
  self.openings
}

///|
pub fn AvailabilityOverlay::closures(self : AvailabilityOverlay) -> IntervalSet {
  self.closures
}

///|
/// Mark one exceptional opening.  The result is immutable, so a caller may

///|
/// compare tentative availability policies before choosing one.
pub fn AvailabilityOverlay::open(
  self : AvailabilityOverlay,
  range : Interval,
) -> AvailabilityOverlay {
  {
    baseline: self.baseline,
    openings: self.openings.add(range),
    closures: self.closures,
  }
}

///|
/// Mark a closure.  Closures win over both baseline and extra openings,

///|
/// protecting a maintenance window even if an overtime opening overlaps it.
pub fn AvailabilityOverlay::close(
  self : AvailabilityOverlay,
  range : Interval,
) -> AvailabilityOverlay {
  {
    baseline: self.baseline,
    openings: self.openings,
    closures: self.closures.add(range),
  }
}

///|
pub fn AvailabilityOverlay::open_many(
  self : AvailabilityOverlay,
  ranges : Array[Interval],
) -> AvailabilityOverlay {
  let mut output = self
  for range in ranges {
    output = output.open(range)
  }
  output
}

///|
pub fn AvailabilityOverlay::close_many(
  self : AvailabilityOverlay,
  ranges : Array[Interval],
) -> AvailabilityOverlay {
  let mut output = self
  for range in ranges {
    output = output.close(range)
  }
  output
}

///|
/// Materialize the final availability.  The expression is intentionally

///|
/// compact and deterministic: `(baseline ∪ openings) − closures`.
pub fn AvailabilityOverlay::resolve(self : AvailabilityOverlay) -> IntervalSet {
  self.baseline.union(self.openings).subtract(self.closures)
}

///|
pub fn AvailabilityOverlay::resolve_within(
  self : AvailabilityOverlay,
  horizon : Interval,
) -> IntervalSet {
  self.resolve().within(horizon)
}

///|
pub fn AvailabilityOverlay::is_open(
  self : AvailabilityOverlay,
  tick : Tick,
) -> Bool {
  self.resolve().contains(tick)
}

///|
pub fn AvailabilityOverlay::explain(
  self : AvailabilityOverlay,
  tick : Tick,
) -> String {
  if self.closures.contains(tick) {
    "closed by exception"
  } else if self.openings.contains(tick) && !self.baseline.contains(tick) {
    "open by exception"
  } else if self.baseline.contains(tick) {
    "open by baseline"
  } else {
    "outside availability"
  }
}

///| The availability computed for one resource together with the exceptions

///| used to derive it.  Keeping the overlay in the value makes an application

///|
/// able to show users why a resource is unavailable.
pub struct ResolvedAvailability {
  overlay : AvailabilityOverlay
  available : IntervalSet
} derive(Debug)

///|
pub fn ResolvedAvailability::overlay(
  self : ResolvedAvailability,
) -> AvailabilityOverlay {
  self.overlay
}

///|
pub fn ResolvedAvailability::available(
  self : ResolvedAvailability,
) -> IntervalSet {
  self.available
}

///|
pub fn resolve_availability(
  overlay : AvailabilityOverlay,
) -> ResolvedAvailability {
  { overlay, available: overlay.resolve() }
}

///|
pub enum OverlayResourceError {
  EmptyResolvedAvailability
  ExistingReservationOutsideAvailability(Interval)
} derive(Eq, Debug)

///|
/// Apply an overlay to an existing resource without discarding active holds.

///| The operation rejects an exception that would hide a currently reserved

///|
/// range, forcing the host to cancel or reschedule it deliberately first.
pub fn ResourceCalendar::with_availability_overlay(
  self : ResourceCalendar,
  overlay : AvailabilityOverlay,
) -> Result[ResourceCalendar, OverlayResourceError] {
  let available = overlay.resolve()
  if available.is_empty() {
    return Err(EmptyResolvedAvailability)
  }
  for blocked in self.blocked().ranges() {
    let mut covered = false
    for open in available.ranges() {
      if open.contains_interval(blocked) {
        covered = true
      }
    }
    if !covered {
      return Err(ExistingReservationOutsideAvailability(blocked))
    }
  }
  Ok({
    id: self.id(),
    capacity: self.capacity(),
    available,
    blocked: self.blocked(),
  })
}

///|
/// Convenience bridge from a weekly template to an exception-aware resource.
pub fn resource_with_weekly_overlay(
  id : String,
  capacity : Int,
  template : WeeklyTemplate,
  week_start : Tick,
  horizon : Interval,
  openings : Array[Interval],
  closures : Array[Interval],
) -> Result[ResourceCalendar, WeeklyResourceError] {
  let baseline = match template.availability_within(week_start, horizon) {
    Ok(value) => value
    Err(error) => return Err(Weekly(error))
  }
  let overlay = AvailabilityOverlay::new(baseline)
    .open_many(openings)
    .close_many(closures)
  match ResourceCalendar::new(id, capacity, overlay.resolve()) {
    Ok(resource) => Ok(resource)
    Err(error) => Err(Resource(error))
  }
}