///|
pub(all) struct CandidateExplanation {
  person_id : PersonId
  eligible : Bool
  incremental_cost : Int
  reasons : Array[String]
} derive(Debug, Eq)

///|
pub(all) struct SlotExplanation {
  shift_id : ShiftId
  slot : Int
  candidates : Array[CandidateExplanation]
  eligible_count : Int
  rejected_count : Int
  forced : Bool
} derive(Debug, Eq)

///|
fn diagnostic_reason(item : Diagnostic) -> String {
  match item.code {
    InvalidSlot => "slot is outside shift capacity"
    OutsideAvailability => "person is not available for the whole shift"
    SkillMismatch => "person does not meet the shift skill requirements"
    DuplicateAssignment => "the slot already has an assignment"
    OverlappingAssignment => "person has an overlapping assignment"
    InsufficientRest => "person would receive insufficient rest"
    MaximumMinutesExceeded => "person would exceed maximum minutes"
    UnknownPerson => "person is unknown"
    UnknownShift => "shift is unknown"
    _ => item.message
  }
}

///|
pub fn explain_candidate(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  shift : Shift,
  slot : Int,
  person : Person,
) -> CandidateExplanation {
  match can_assign(problem, assignments, shift, slot, person) {
    None =>
      {
        person_id: person.id,
        eligible: true,
        incremental_cost: incremental_cost(problem, assignments, shift, person),
        reasons: ["eligible"],
      }
    Some(reason) =>
      {
        person_id: person.id,
        eligible: false,
        incremental_cost: 1000000,
        reasons: [diagnostic_reason(reason)],
      }
  }
}

///|
pub fn explain_slot(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  shift : Shift,
  slot : Int,
) -> SlotExplanation {
  let candidates : Array[CandidateExplanation] = []
  let mut eligible = 0
  let mut rejected = 0
  for person in problem.people {
    let detail = explain_candidate(problem, assignments, shift, slot, person)
    candidates.push(detail)
    if detail.eligible {
      eligible = eligible + 1
    } else {
      rejected = rejected + 1
    }
  }
  candidates.sort_by((a, b) => {
    if a.eligible && !b.eligible {
      -1
    } else if !a.eligible && b.eligible {
      1
    } else {
      a.incremental_cost - b.incremental_cost
    }
  })
  {
    shift_id: shift.id,
    slot,
    candidates,
    eligible_count: eligible,
    rejected_count: rejected,
    forced: eligible == 1,
  }
}

///|
pub fn explain_problem(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[SlotExplanation] {
  let output : Array[SlotExplanation] = []
  for shift in problem.shifts {
    for slot = 0; slot < shift.required_people; slot = slot + 1 {
      let mut occupied = false
      for assignment in assignments {
        if assignment.shift_id == shift.id && assignment.slot == slot {
          occupied = true
        }
      }
      if !occupied {
        output.push(explain_slot(problem, assignments, shift, slot))
      }
    }
  }
  output
}

///|
pub fn most_constrained_slot(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> SlotExplanation? {
  let explanations = explain_problem(problem, assignments)
  if explanations.length() == 0 {
    return None
  }
  let mut best = 0
  for i = 1; i < explanations.length(); i = i + 1 {
    if explanations[i].eligible_count < explanations[best].eligible_count {
      best = i
    }
  }
  Some(explanations[best])
}

///|
pub fn forced_slots(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[SlotExplanation] {
  let values : Array[SlotExplanation] = []
  for item in explain_problem(problem, assignments) {
    if item.forced {
      values.push(item)
    }
  }
  values
}

///|
pub fn impossible_slots(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[SlotExplanation] {
  let values : Array[SlotExplanation] = []
  for item in explain_problem(problem, assignments) {
    if item.eligible_count == 0 {
      values.push(item)
    }
  }
  values
}

///|
pub fn render_slot_explanation(
  problem : ScheduleProblem,
  explanation : SlotExplanation,
) -> String {
  let mut title = explanation.shift_id
  match find_shift(problem, explanation.shift_id) {
    Some(shift) => title = shift.label
    None => ()
  }
  let mut text = "## \{title} / slot \{explanation.slot + 1}\n\n"
  text = text +
    "Eligible candidates: \{explanation.eligible_count}; rejected: \{explanation.rejected_count}.\n\n"
  for candidate in explanation.candidates {
    let marker = if candidate.eligible { "eligible" } else { "rejected" }
    text = text +
      "- **\{candidate.person_id}** — \{marker}, cost \{candidate.incremental_cost}: "
    let mut first = true
    for reason in candidate.reasons {
      if !first {
        text = text + "; "
      }
      first = false
      text = text + reason
    }
    text = text + "\n"
  }
  text
}

///|
pub fn render_problem_explanation(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> String {
  let mut text = "# ShiftWeave decision explanation\n\n"
  let explanations = explain_problem(problem, assignments)
  text = text + "Open slots: \{explanations.length()}\n\n"
  for item in explanations {
    text = text + render_slot_explanation(problem, item) + "\n"
  }
  text
}

///|
pub fn summarize_infeasibility(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[String] {
  let messages : Array[String] = []
  let impossible = impossible_slots(problem, assignments)
  if impossible.length() == 0 {
    messages.push(
      "No locally impossible slot was found; infeasibility may result from combinations of assignments.",
    )
  }
  for slot in impossible {
    let mut message = "Shift \{slot.shift_id} slot \{slot.slot + 1} has no eligible candidate."
    let mut availability = 0
    let mut skills = 0
    let mut conflicts = 0
    for candidate in slot.candidates {
      for reason in candidate.reasons {
        if reason.contains("available") {
          availability = availability + 1
        }
        if reason.contains("skill") {
          skills = skills + 1
        }
        if reason.contains("overlap") || reason.contains("rest") {
          conflicts = conflicts + 1
        }
      }
    }
    message = message +
      " Rejections: availability=\{availability}, skills=\{skills}, schedule-conflicts=\{conflicts}."
    messages.push(message)
  }
  messages
}

///|
pub fn suggested_actions(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
) -> Array[String] {
  let actions : Array[String] = []
  for slot in impossible_slots(problem, assignments) {
    let mut lacks_skill = false
    let mut lacks_availability = false
    let mut conflicts = false
    for candidate in slot.candidates {
      for reason in candidate.reasons {
        if reason.contains("skill") {
          lacks_skill = true
        }
        if reason.contains("available") {
          lacks_availability = true
        }
        if reason.contains("overlap") || reason.contains("rest") {
          conflicts = true
        }
      }
    }
    if lacks_skill {
      actions.push(
        "Add or train a person with the skills required by shift \{slot.shift_id}.",
      )
    }
    if lacks_availability {
      actions.push(
        "Extend availability or move shift \{slot.shift_id} to a covered time.",
      )
    }
    if conflicts {
      actions.push(
        "Unlock a nearby assignment or reduce the rest constraint around shift \{slot.shift_id}.",
      )
    }
    if !lacks_skill && !lacks_availability && !conflicts {
      actions.push("Increase staffing capacity for shift \{slot.shift_id}.")
    }
  }
  actions
}