///|
/// Operational event applied after a baseline schedule has been published.
pub(all) enum ScheduleEvent {
  WorkerAbsent(worker_id~ : String, from_minute~ : Int, to_minute~ : Int)
  VisitCancelled(visit_id~ : String)
  VisitDelayed(visit_id~ : String, delay_minutes~ : Int)
  EmergencyVisit(visit~ : Visit)
  WorkerCapacityChanged(
    worker_id~ : String,
    max_minutes~ : Int,
    max_visits~ : Int
  )
} derive(Debug, Eq)

///|
/// Stable event kind used by logs and JSON reports.
pub fn event_kind(event : ScheduleEvent) -> String {
  match event {
    WorkerAbsent(..) => "worker_absent"
    VisitCancelled(..) => "visit_cancelled"
    VisitDelayed(..) => "visit_delayed"
    EmergencyVisit(..) => "emergency_visit"
    WorkerCapacityChanged(..) => "worker_capacity_changed"
  }
}

///|
/// Validate one operational event against the current request.
pub fn validate_event(
  event : ScheduleEvent,
  request : ScheduleRequest,
) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  match event {
    WorkerAbsent(worker_id~, from_minute~, to_minute~) => {
      if find_worker(request.workers, worker_id) is None {
        issues.push(
          error_diagnostic(
            "event.unknown_worker", "absence references an unknown worker",
          ).for_entity(worker_id),
        )
      }
      if to_minute <= from_minute {
        issues.push(
          error_diagnostic(
            "event.invalid_absence_window", "absence end must be after its start",
          ).for_entity(worker_id),
        )
      }
    }
    VisitCancelled(visit_id~) =>
      if find_visit(request.visits, visit_id) is None {
        issues.push(
          error_diagnostic(
            "event.unknown_visit", "cancellation references an unknown visit",
          ).for_entity(visit_id),
        )
      }
    VisitDelayed(visit_id~, delay_minutes~) => {
      if find_visit(request.visits, visit_id) is None {
        issues.push(
          error_diagnostic(
            "event.unknown_visit", "delay references an unknown visit",
          ).for_entity(visit_id),
        )
      }
      if delay_minutes <= 0 {
        issues.push(
          error_diagnostic(
            "event.invalid_delay", "delay minutes must be positive",
          ).for_entity(visit_id),
        )
      }
    }
    EmergencyVisit(visit~) => {
      issues.append(validate_visit(visit))
      if find_visit(request.visits, visit.id) is Some(_) {
        issues.push(
          error_diagnostic(
            "event.duplicate_emergency_visit", "emergency visit id already exists in the request",
          ).for_entity(visit.id),
        )
      }
    }
    WorkerCapacityChanged(worker_id~, max_minutes~, max_visits~) => {
      if find_worker(request.workers, worker_id) is None {
        issues.push(
          error_diagnostic(
            "event.unknown_worker", "capacity event references an unknown worker",
          ).for_entity(worker_id),
        )
      }
      if max_minutes <= 0 || max_visits <= 0 {
        issues.push(
          error_diagnostic(
            "event.invalid_capacity", "capacity limits must both be positive",
          ).for_entity(worker_id),
        )
      }
    }
  }
  issues
}

///|
/// Remove one visit from a visit collection.
fn without_visit(visits : Array[Visit], visit_id : String) -> Array[Visit] {
  let result : Array[Visit] = []
  for visit in visits {
    if visit.id != visit_id {
      result.push(visit)
    }
  }
  result
}

///|
/// Replace one visit while retaining deterministic input order.
fn replace_visit(visits : Array[Visit], replacement : Visit) -> Array[Visit] {
  let result : Array[Visit] = []
  let mut replaced = false
  for visit in visits {
    if visit.id == replacement.id {
      result.push(replacement)
      replaced = true
    } else {
      result.push(visit)
    }
  }
  if !replaced {
    result.push(replacement)
  }
  result
}

///|
/// Replace one worker while retaining deterministic input order.
fn replace_worker(
  workers : Array[Worker],
  replacement : Worker,
) -> Array[Worker] {
  let result : Array[Worker] = []
  for worker in workers {
    if worker.id == replacement.id {
      result.push(replacement)
    } else {
      result.push(worker)
    }
  }
  result
}

///|
/// Subtract an absence window from one availability window.
pub fn subtract_window(
  available : TimeWindow,
  absent : TimeWindow,
) -> Array[TimeWindow] {
  if !available.overlaps(absent) {
    return [available]
  }
  let result : Array[TimeWindow] = []
  if absent.start_minute > available.start_minute {
    result.push(
      time_window(
        available.start_minute,
        min_int(absent.start_minute, available.end_minute),
      ),
    )
  }
  if absent.end_minute < available.end_minute {
    result.push(
      time_window(
        max_int(absent.end_minute, available.start_minute),
        available.end_minute,
      ),
    )
  }
  result
}

///|
/// Apply a partial absence without marking the worker absent all day.
fn apply_worker_absence(
  workers : Array[Worker],
  worker_id : String,
  absent : TimeWindow,
) -> Array[Worker] {
  match find_worker(workers, worker_id) {
    None => copy_array(workers)
    Some(target) => {
      let availability : Array[TimeWindow] = []
      for window in target.availability {
        availability.append(subtract_window(window, absent))
      }
      replace_worker(workers, {
        ..target,
        availability,
        unavailable: availability.length() == 0,
      })
    }
  }
}

///|
/// Apply one validated event to a request without mutating the caller.
pub fn apply_event(
  request : ScheduleRequest,
  event : ScheduleEvent,
) -> ScheduleRequest {
  match event {
    WorkerAbsent(worker_id~, from_minute~, to_minute~) =>
      {
        ..request,
        workers: apply_worker_absence(
          request.workers,
          worker_id,
          time_window(from_minute, to_minute),
        ),
      }
    VisitCancelled(visit_id~) =>
      { ..request, visits: without_visit(request.visits, visit_id) }
    VisitDelayed(visit_id~, delay_minutes~) =>
      match find_visit(request.visits, visit_id) {
        None => request
        Some(target) => {
          let delayed = {
            ..target,
            window: time_window(
              target.window.start_minute + delay_minutes,
              target.window.end_minute + delay_minutes,
            ),
          }
          { ..request, visits: replace_visit(request.visits, delayed) }
        }
      }
    EmergencyVisit(visit~) =>
      {
        ..request,
        visits: replace_visit(request.visits, {
          ..visit,
          priority: Critical,
          required: true,
        }),
      }
    WorkerCapacityChanged(worker_id~, max_minutes~, max_visits~) =>
      match find_worker(request.workers, worker_id) {
        None => request
        Some(target) =>
          {
            ..request,
            workers: replace_worker(request.workers, {
              ..target,
              max_minutes,
              max_visits,
            }),
          }
      }
  }
}

///|
/// Apply events sequentially after validating them against evolving state.
pub fn apply_events(
  request : ScheduleRequest,
  events : Array[ScheduleEvent],
) -> (ScheduleRequest, Array[Diagnostic]) {
  let mut current = request
  let diagnostics : Array[Diagnostic] = []
  for event in events {
    let issues = validate_event(event, current)
    diagnostics.append(issues)
    if !diagnostics_have_errors(issues) {
      current = apply_event(current, event)
    }
  }
  (current, diagnostics)
}