///|
/// One assignment-level difference between two schedules.
pub(all) enum AssignmentChange {
  Added(assignment~ : Assignment)
  Removed(assignment~ : Assignment)
  Reassigned(before~ : Assignment, after~ : Assignment)
  Retimed(before~ : Assignment, after~ : Assignment)
  ReassignedAndRetimed(before~ : Assignment, after~ : Assignment)
  Unchanged(assignment~ : Assignment)
} derive(Debug, Eq)

///|
/// Complete impact summary for an event-driven reschedule.
pub(all) struct RescheduleReport {
  baseline : Schedule
  updated : Schedule
  events : Array[ScheduleEvent]
  changes : Array[AssignmentChange]
  diagnostics : Array[Diagnostic]
  changed_assignments : Int
  preserved_assignments : Int
  newly_unassigned : Int
  score_change : Int
} derive(Debug, Eq)

///|
/// Stable name for assignment change categories.
pub fn assignment_change_name(change : AssignmentChange) -> String {
  match change {
    Added(..) => "added"
    Removed(..) => "removed"
    Reassigned(..) => "reassigned"
    Retimed(..) => "retimed"
    ReassignedAndRetimed(..) => "reassigned_and_retimed"
    Unchanged(..) => "unchanged"
  }
}

///|
/// Visit id affected by one change.
pub fn assignment_change_visit_id(change : AssignmentChange) -> String {
  match change {
    Added(assignment~) => assignment.visit_id
    Removed(assignment~) => assignment.visit_id
    Reassigned(before~, ..) => before.visit_id
    Retimed(before~, ..) => before.visit_id
    ReassignedAndRetimed(before~, ..) => before.visit_id
    Unchanged(assignment~) => assignment.visit_id
  }
}

///|
/// Compare two assignments for the same visit.
fn compare_assignment(
  before : Assignment,
  after : Assignment,
) -> AssignmentChange {
  let worker_changed = before.worker_id != after.worker_id
  let time_changed = before.start_minute != after.start_minute ||
    before.end_minute != after.end_minute
  if worker_changed && time_changed {
    ReassignedAndRetimed(before~, after~)
  } else if worker_changed {
    Reassigned(before~, after~)
  } else if time_changed {
    Retimed(before~, after~)
  } else {
    Unchanged(assignment=after)
  }
}

///|
/// Compute assignment changes in stable visit-id order.
pub fn diff_schedules(
  baseline : Schedule,
  updated : Schedule,
) -> Array[AssignmentChange] {
  let visit_ids : Array[String] = []
  for assignment in baseline.assignments {
    push_unique_string(visit_ids, assignment.visit_id)
  }
  for assignment in updated.assignments {
    push_unique_string(visit_ids, assignment.visit_id)
  }
  let changes : Array[AssignmentChange] = []
  for visit_id in sort_strings(visit_ids) {
    match
      (
        find_assignment(baseline.assignments, visit_id),
        find_assignment(updated.assignments, visit_id),
      ) {
      (Some(before), Some(after)) =>
        changes.push(compare_assignment(before, after))
      (Some(before), None) => changes.push(Removed(assignment=before))
      (None, Some(after)) => changes.push(Added(assignment=after))
      (None, None) => ()
    }
  }
  changes
}

///|
/// Count visits newly unassigned after an update.
fn count_newly_unassigned(baseline : Schedule, updated : Schedule) -> Int {
  let mut count = 0
  for missing in updated.unassigned {
    let mut existed = false
    for previous in baseline.unassigned {
      if previous.visit_id == missing.visit_id {
        existed = true
      }
    }
    if !existed {
      count = count + 1
    }
  }
  count
}

///|
/// Re-solve after operational events while penalizing unnecessary changes.
pub fn reschedule(
  baseline : Schedule,
  request : ScheduleRequest,
  events : Array[ScheduleEvent],
) -> RescheduleReport {
  let (mutated, event_diagnostics) = apply_events(request, events)
  if diagnostics_have_errors(event_diagnostics) {
    return {
      baseline,
      updated: baseline,
      events,
      changes: [],
      diagnostics: event_diagnostics,
      changed_assignments: 0,
      preserved_assignments: baseline.assignments.length(),
      newly_unassigned: 0,
      score_change: 0,
    }
  }
  let updated = solve({ ..mutated, baseline: Some(baseline) })
  let changes = diff_schedules(baseline, updated)
  let mut changed = 0
  let mut preserved = 0
  for change in changes {
    match change {
      Unchanged(..) => preserved = preserved + 1
      _ => changed = changed + 1
    }
  }
  {
    baseline,
    updated,
    events,
    changes,
    diagnostics: event_diagnostics,
    changed_assignments: changed,
    preserved_assignments: preserved,
    newly_unassigned: count_newly_unassigned(baseline, updated),
    score_change: updated.score.total - baseline.score.total,
  }
}

///|
/// Concise human explanation of one schedule change.
pub fn describe_assignment_change(change : AssignmentChange) -> String {
  match change {
    Added(assignment~) =>
      "\{assignment.visit_id} added for \{assignment.worker_id} at \{assignment.start_minute}"
    Removed(assignment~) =>
      "\{assignment.visit_id} removed from \{assignment.worker_id}"
    Reassigned(before~, after~) =>
      "\{before.visit_id} reassigned from \{before.worker_id} to \{after.worker_id}"
    Retimed(before~, after~) =>
      "\{before.visit_id} moved from \{before.start_minute} to \{after.start_minute}"
    ReassignedAndRetimed(before~, after~) =>
      "\{before.visit_id} moved from \{before.worker_id} at \{before.start_minute} to \{after.worker_id} at \{after.start_minute}"
    Unchanged(assignment~) =>
      "\{assignment.visit_id} preserved for \{assignment.worker_id} at \{assignment.start_minute}"
  }
}

///|
/// Deterministic summary for logs and the CLI.
pub fn reschedule_summary(report : RescheduleReport) -> String {
  "events=\{report.events.length()}, changed=\{report.changed_assignments}, preserved=\{report.preserved_assignments}, newly_unassigned=\{report.newly_unassigned}, score_change=\{report.score_change}"
}