///|
pub(all) enum AuditEventKind {
  ProblemValidated
  CandidateConsidered
  CandidateRejected
  AssignmentAdded
  AssignmentMoved
  ImprovementAccepted
  CoverageChecked
  SolutionCompleted
} derive(Eq, Show, ToJson)

///|
pub struct AuditEvent {
  sequence : Int
  kind : AuditEventKind
  message : String
  worker_id : String?
  shift_id : String?
  score_before : Int?
  score_after : Int?
} derive(Eq, Show, ToJson)

///|
pub fn AuditEvent::new(
  sequence : Int,
  kind : AuditEventKind,
  message : String,
) -> AuditEvent {
  {
    sequence,
    kind,
    message,
    worker_id: None,
    shift_id: None,
    score_before: None,
    score_after: None,
  }
}

///|
pub fn AuditEvent::for_assignment(
  self : AuditEvent,
  worker_id : String,
  shift_id : String,
) -> AuditEvent {
  { ..self, worker_id: Some(worker_id), shift_id: Some(shift_id) }
}

///|
pub fn AuditEvent::with_scores(
  self : AuditEvent,
  before : Int,
  after : Int,
) -> AuditEvent {
  { ..self, score_before: Some(before), score_after: Some(after) }
}

///|
pub struct AuditTrail {
  events : Array[AuditEvent]
} derive(Eq, Show, ToJson)

///|
pub fn AuditTrail::empty() -> AuditTrail {
  { events: [] }
}

///|
pub fn AuditTrail::record(
  self : AuditTrail,
  kind : AuditEventKind,
  message : String,
) -> AuditTrail {
  let events = self.events.copy()
  events.push(AuditEvent::new(events.length(), kind, message))
  { events, }
}

///|
pub fn AuditTrail::record_assignment(
  self : AuditTrail,
  kind : AuditEventKind,
  message : String,
  worker_id : String,
  shift_id : String,
) -> AuditTrail {
  let events = self.events.copy()
  events.push(
    AuditEvent::new(events.length(), kind, message).for_assignment(
      worker_id, shift_id,
    ),
  )
  { events, }
}

///|
pub struct AuditedSolution {
  solution : Solution
  audit : AuditTrail
} derive(Eq, Show, ToJson)

///|
pub fn solve_audited(
  problem : PlanningProblem,
  config : SolverConfig,
) -> AuditedSolution {
  let mut audit = AuditTrail::empty()
  let validation = problem.validate()
  audit = audit.record(
    ProblemValidated,
    "validation produced " + validation.length().to_string() + " diagnostic(s)",
  )
  let solution = solve_with_config(problem, config)
  for shift in problem.shifts {
    let eligible = eligible_workers(problem, Schedule::empty(), shift.id)
    audit = audit.record(
      CandidateConsidered,
      shift.label +
      " initially has " +
      eligible.length().to_string() +
      " eligible worker(s)",
    )
    for worker_id in solution.schedule.workers_for(shift.id) {
      audit = audit.record_assignment(
        AssignmentAdded,
        "assignment appears in final solution",
        worker_id,
        shift.id,
      )
    }
    let assigned = assignment_count_for_shift(solution.schedule, shift.id)
    audit = audit.record(
      CoverageChecked,
      shift.label +
      " has " +
      assigned.to_string() +
      "/" +
      shift.required_workers.to_string() +
      " required worker(s)",
    )
  }
  audit = audit.record(
    SolutionCompleted,
    "solver completed with objective " + solution.score.total.to_string(),
  )
  { solution, audit }
}

///|
pub fn render_audit(audit : AuditTrail) -> String {
  let output = StringBuilder::new()
  output.write_string("RosterWeave audit trail\n")
  output.write_string("=====================\n")
  for event in audit.events {
    output.write_string("[")
    output.write_string(event.sequence.to_string())
    output.write_string("] ")
    output.write_string(event.kind.to_string())
    output.write_string(": ")
    output.write_string(event.message)
    match (event.worker_id, event.shift_id) {
      (Some(worker), Some(shift)) =>
        output.write_string(" (worker=" + worker + ", shift=" + shift + ")")
      _ => ()
    }
    output.write_char('\n')
  }
  output.to_string()
}

///|
pub struct SolutionComparison {
  left_score : Int
  right_score : Int
  score_delta : Int
  left_coverage : Int
  right_coverage : Int
  coverage_delta : Int
  left_workload_spread : Int
  right_workload_spread : Int
  workload_spread_delta : Int
  changed_assignments : Int
  recommendation : String
} derive(Eq, Show, ToJson)

///|
fn assignment_difference(left : Schedule, right : Schedule) -> Int {
  let mut changed = 0
  for assignment in left.assignments {
    if !right.contains(assignment.worker_id, assignment.shift_id) {
      changed = changed + 1
    }
  }
  for assignment in right.assignments {
    if !left.contains(assignment.worker_id, assignment.shift_id) {
      changed = changed + 1
    }
  }
  changed
}

///|
pub fn compare_solutions(
  problem : PlanningProblem,
  left : Solution,
  right : Solution,
) -> SolutionComparison {
  let left_coverage = coverage_ratio_per_mille(problem, left.schedule)
  let right_coverage = coverage_ratio_per_mille(problem, right.schedule)
  let left_spread = workload_range(problem, left.schedule)
  let right_spread = workload_range(problem, right.schedule)
  let recommendation = if right.feasible && !left.feasible {
    "right"
  } else if left.feasible && !right.feasible {
    "left"
  } else if right.score.total < left.score.total {
    "right"
  } else if left.score.total < right.score.total {
    "left"
  } else {
    "equivalent"
  }
  {
    left_score: left.score.total,
    right_score: right.score.total,
    score_delta: right.score.total - left.score.total,
    left_coverage,
    right_coverage,
    coverage_delta: right_coverage - left_coverage,
    left_workload_spread: left_spread,
    right_workload_spread: right_spread,
    workload_spread_delta: right_spread - left_spread,
    changed_assignments: assignment_difference(left.schedule, right.schedule),
    recommendation,
  }
}