///|
pub(all) enum ExplanationKind {
  AssignmentAccepted
  AssignmentRejected
  CoverageFailure
  SkillCoverageFailure
  WorkloadObservation
  PreferenceObservation
  ImprovementSuggestion
} derive(Eq, Show, ToJson)

///|
pub struct Explanation {
  kind : ExplanationKind
  title : String
  detail : String
  worker_id : String?
  shift_id : String?
  priority : Int
} derive(Eq, Show, ToJson)

///|
pub fn Explanation::new(
  kind : ExplanationKind,
  title : String,
  detail : String,
  priority : Int,
) -> Explanation {
  { kind, title, detail, worker_id: None, shift_id: None, priority }
}

///|
pub fn Explanation::for_worker(
  self : Explanation,
  worker_id : String,
) -> Explanation {
  { ..self, worker_id: Some(worker_id) }
}

///|
pub fn Explanation::for_shift(
  self : Explanation,
  shift_id : String,
) -> Explanation {
  { ..self, shift_id: Some(shift_id) }
}

///|
fn violation_detail(violation : ConstraintViolation) -> String {
  match violation.kind {
    UnknownWorker =>
      "The worker identifier does not exist in the planning problem."
    UnknownShift =>
      "The shift identifier does not exist in the planning problem."
    DuplicateAssignment => "The same worker and shift pair is already present."
    WorkerUnavailable =>
      "The shift is outside the worker's declared availability."
    ExplicitlyUnavailable =>
      "The worker explicitly marked this shift as unavailable."
    MissingRequiredPreference => "A required preference was not satisfied."
    OverlappingShift => "Another assigned shift overlaps this shift."
    InsufficientRest =>
      "The assignment would leave less than the required rest time."
    WeeklyMinuteLimit =>
      "The assignment would exceed the worker's maximum minutes."
    ConsecutiveShiftLimit =>
      "The assignment would exceed the consecutive-day limit."
    MissingSkill => "The worker does not hold the required skill level."
    Understaffed =>
      "The number of assigned workers is below the hard requirement."
    SkillShortage => "Too few assigned workers hold the required skill level."
  }
}

///|
pub fn explain_candidate(
  problem : PlanningProblem,
  schedule : Schedule,
  worker_id : String,
  shift_id : String,
) -> Array[Explanation] {
  let explanations : Array[Explanation] = []
  let report = check_candidate(problem, schedule, worker_id, shift_id)
  if report.feasible() {
    let delta = assignment_delta(problem, schedule, worker_id, shift_id)
    let direction = if delta < 0 {
      "improves the objective by " + (-delta).to_string()
    } else if delta == 0 {
      "does not change the objective"
    } else {
      "increases the objective by " + delta.to_string()
    }
    explanations.push(
      Explanation::new(
        AssignmentAccepted,
        "Assignment is feasible",
        "All hard constraints pass; the assignment " + direction + ".",
        1,
      )
      .for_worker(worker_id)
      .for_shift(shift_id),
    )
  } else {
    for violation in report.violations {
      let mut explanation = Explanation::new(
        AssignmentRejected,
        violation.message,
        violation_detail(violation),
        100,
      )
      match violation.worker_id {
        Some(id) => explanation = explanation.for_worker(id)
        None => ()
      }
      match violation.shift_id {
        Some(id) => explanation = explanation.for_shift(id)
        None => ()
      }
      explanations.push(explanation)
    }
  }
  explanations
}

///|
fn coverage_explanations(
  problem : PlanningProblem,
  schedule : Schedule,
) -> Array[Explanation] {
  let explanations : Array[Explanation] = []
  for shift in problem.shifts {
    let assigned = assignment_count_for_shift(schedule, shift.id)
    if assigned < shift.required_workers {
      let missing = shift.required_workers - assigned
      explanations.push(
        Explanation::new(
          CoverageFailure,
          shift.label + " needs " + missing.to_string() + " more worker(s)",
          "No remaining worker could be assigned without violating a hard constraint.",
          1000 + missing,
        ).for_shift(shift.id),
      )
    }
    for requirement in shift.requirements {
      let mut qualified = 0
      for worker_id in schedule.workers_for(shift.id) {
        match problem.worker(worker_id) {
          Some(worker) =>
            if worker.has_skill(requirement.skill_id, requirement.minimum_level) {
              qualified = qualified + 1
            }
          None => ()
        }
      }
      if qualified < requirement.count {
        explanations.push(
          Explanation::new(
            SkillCoverageFailure,
            shift.label + " lacks skill " + requirement.skill_id,
            "Add a qualified worker or reduce the skill requirement after review.",
            900 + requirement.count - qualified,
          ).for_shift(shift.id),
        )
      }
    }
  }
  explanations
}

///|
fn workload_explanations(
  problem : PlanningProblem,
  schedule : Schedule,
) -> Array[Explanation] {
  let explanations : Array[Explanation] = []
  for worker in problem.workers {
    let minutes = worker_assigned_minutes(problem, schedule, worker.id)
    if minutes > worker.target_minutes {
      explanations.push(
        Explanation::new(
          WorkloadObservation,
          worker.name + " is above target workload",
          "Assigned " +
          minutes.to_string() +
          " minutes against a target of " +
          worker.target_minutes.to_string() +
          ".",
          100 + minutes - worker.target_minutes,
        ).for_worker(worker.id),
      )
    } else if minutes < worker.min_minutes {
      explanations.push(
        Explanation::new(
          WorkloadObservation,
          worker.name + " is below minimum workload",
          "Assigned " +
          minutes.to_string() +
          " minutes against a minimum of " +
          worker.min_minutes.to_string() +
          ".",
          100 + worker.min_minutes - minutes,
        ).for_worker(worker.id),
      )
    }
  }
  explanations
}

///|
fn preference_explanations(
  problem : PlanningProblem,
  schedule : Schedule,
) -> Array[Explanation] {
  let explanations : Array[Explanation] = []
  for worker in problem.workers {
    for preference in worker.preferences {
      let assigned = schedule.contains(worker.id, preference.shift_id)
      match preference.kind {
        Prefer =>
          if !assigned {
            explanations.push(
              Explanation::new(
                PreferenceObservation,
                worker.name + " preferred an unassigned shift",
                "This is a soft preference and may be traded for coverage or fairness.",
                preference.weight,
              )
              .for_worker(worker.id)
              .for_shift(preference.shift_id),
            )
          }
        Avoid =>
          if assigned {
            explanations.push(
              Explanation::new(
                PreferenceObservation,
                worker.name + " was assigned an avoided shift",
                "The solver accepted this soft cost to satisfy stronger constraints.",
                preference.weight,
              )
              .for_worker(worker.id)
              .for_shift(preference.shift_id),
            )
          }
        Require =>
          if !assigned {
            explanations.push(
              Explanation::new(
                PreferenceObservation,
                worker.name + " requested a required shift",
                "The request could not be satisfied under the current hard constraints.",
                preference.weight * 10,
              )
              .for_worker(worker.id)
              .for_shift(preference.shift_id),
            )
          }
        Unavailable => ()
      }
    }
  }
  explanations
}

///|
fn explanation_before(left : Explanation, right : Explanation) -> Bool {
  if left.priority != right.priority {
    left.priority > right.priority
  } else {
    left.title < right.title
  }
}

///|
fn sort_explanations(values : Array[Explanation]) -> Array[Explanation] {
  let result = values.copy()
  for i = 1; i < result.length(); i = i + 1 {
    let current = result[i]
    let mut j = i
    while j > 0 && explanation_before(current, result[j - 1]) {
      result[j] = result[j - 1]
      j = j - 1
    }
    result[j] = current
  }
  result
}

///|
pub fn explain_solution(
  problem : PlanningProblem,
  solution : Solution,
) -> Array[Explanation] {
  let explanations : Array[Explanation] = []
  explanations.append(coverage_explanations(problem, solution.schedule))
  explanations.append(workload_explanations(problem, solution.schedule))
  explanations.append(preference_explanations(problem, solution.schedule))
  if workload_range(problem, solution.schedule) > 480 {
    explanations.push(
      Explanation::new(
        ImprovementSuggestion,
        "Workload spread remains high",
        "Review availability or increase local-search iterations to reduce the spread.",
        50,
      ),
    )
  }
  sort_explanations(explanations)
}