///|
/// A minute offset from the beginning of the planning horizon.
pub struct Minute {
  value_ : Int
} derive(Eq, Compare, Show, ToJson)

///|
pub fn Minute::new(value : Int) -> Minute {
  { value_: value }
}

///|
pub fn Minute::value(self : Minute) -> Int {
  self.value_
}

///|
pub fn Minute::add(self : Minute, delta : Int) -> Minute {
  { value_: self.value() + delta }
}

///|
pub fn Minute::distance_to(self : Minute, other : Minute) -> Int {
  other.value() - self.value()
}

///|
pub struct TimeRange {
  start : Minute
  end : Minute
} derive(Eq, Show, ToJson)

///|
pub fn TimeRange::new(start : Int, end : Int) -> TimeRange {
  { start: Minute::new(start), end: Minute::new(end) }
}

///|
pub fn TimeRange::duration(self : TimeRange) -> Int {
  self.start.distance_to(self.end)
}

///|
pub fn TimeRange::is_valid(self : TimeRange) -> Bool {
  self.start.value() >= 0 && self.end.value() > self.start.value()
}

///|
pub fn TimeRange::overlaps(self : TimeRange, other : TimeRange) -> Bool {
  self.start.value() < other.end.value() &&
  other.start.value() < self.end.value()
}

///|
pub fn TimeRange::gap_after(self : TimeRange, previous : TimeRange) -> Int {
  previous.end.distance_to(self.start)
}

///|
pub(all) enum SkillLevel {
  Beginner
  Qualified
  Advanced
  Expert
} derive(Eq, Compare, Show, ToJson)

///|
pub fn SkillLevel::rank(self : SkillLevel) -> Int {
  match self {
    Beginner => 1
    Qualified => 2
    Advanced => 3
    Expert => 4
  }
}

///|
pub struct Skill {
  id : String
  name : String
} derive(Eq, Show, ToJson)

///|
pub fn Skill::new(id : String, name : String) -> Skill {
  { id, name }
}

///|
pub struct WorkerSkill {
  skill_id : String
  level : SkillLevel
} derive(Eq, Show, ToJson)

///|
pub fn WorkerSkill::new(skill_id : String, level : SkillLevel) -> WorkerSkill {
  { skill_id, level }
}

///|
pub(all) enum PreferenceKind {
  Prefer
  Avoid
  Require
  Unavailable
} derive(Eq, Show, ToJson)

///|
pub struct ShiftPreference {
  shift_id : String
  kind : PreferenceKind
  weight : Int
} derive(Eq, Show, ToJson)

///|
pub fn ShiftPreference::new(
  shift_id : String,
  kind : PreferenceKind,
  weight : Int,
) -> ShiftPreference {
  { shift_id, kind, weight }
}

///|
pub struct Worker {
  id : String
  name : String
  skills : Array[WorkerSkill]
  availability : Array[TimeRange]
  preferences : Array[ShiftPreference]
  min_minutes : Int
  target_minutes : Int
  max_minutes : Int
  max_consecutive_shifts : Int
  min_rest_minutes : Int
} derive(Eq, Show, ToJson)

///|
pub fn Worker::new(id : String, name : String) -> Worker {
  {
    id,
    name,
    skills: [],
    availability: [],
    preferences: [],
    min_minutes: 0,
    target_minutes: 2400,
    max_minutes: 3000,
    max_consecutive_shifts: 5,
    min_rest_minutes: 660,
  }
}

///|
pub fn Worker::with_limits(
  self : Worker,
  min_minutes : Int,
  target_minutes : Int,
  max_minutes : Int,
) -> Worker {
  { ..self, min_minutes, target_minutes, max_minutes }
}

///|
pub fn Worker::with_rest_rules(
  self : Worker,
  max_consecutive_shifts : Int,
  min_rest_minutes : Int,
) -> Worker {
  { ..self, max_consecutive_shifts, min_rest_minutes }
}

///|
pub fn Worker::add_skill(self : Worker, skill : WorkerSkill) -> Worker {
  let skills = self.skills.copy()
  skills.push(skill)
  { ..self, skills, }
}

///|
pub fn Worker::add_availability(self : Worker, range : TimeRange) -> Worker {
  let availability = self.availability.copy()
  availability.push(range)
  { ..self, availability, }
}

///|
pub fn Worker::add_preference(
  self : Worker,
  preference : ShiftPreference,
) -> Worker {
  let preferences = self.preferences.copy()
  preferences.push(preference)
  { ..self, preferences, }
}

///|
pub fn Worker::skill_level(self : Worker, skill_id : String) -> SkillLevel? {
  for skill in self.skills {
    if skill.skill_id == skill_id {
      return Some(skill.level)
    }
  }
  None
}

///|
pub fn Worker::has_skill(
  self : Worker,
  skill_id : String,
  minimum : SkillLevel,
) -> Bool {
  match self.skill_level(skill_id) {
    Some(level) => level.rank() >= minimum.rank()
    None => false
  }
}

///|
pub fn Worker::is_available(self : Worker, range : TimeRange) -> Bool {
  if self.availability.length() == 0 {
    return true
  }
  for allowed in self.availability {
    if allowed.start.value() <= range.start.value() &&
      allowed.end.value() >= range.end.value() {
      return true
    }
  }
  false
}

///|
pub fn Worker::preference_for(
  self : Worker,
  shift_id : String,
) -> ShiftPreference? {
  for preference in self.preferences {
    if preference.shift_id == shift_id {
      return Some(preference)
    }
  }
  None
}

///|
pub struct SkillRequirement {
  skill_id : String
  minimum_level : SkillLevel
  count : Int
} derive(Eq, Show, ToJson)

///|
pub fn SkillRequirement::new(
  skill_id : String,
  minimum_level : SkillLevel,
  count : Int,
) -> SkillRequirement {
  { skill_id, minimum_level, count }
}

///|
pub(all) enum ShiftKind {
  Morning
  Day
  Evening
  Night
  OnCall
  Custom(String)
} derive(Eq, Show, ToJson)

///|
pub fn ShiftKind::is_night(self : ShiftKind) -> Bool {
  match self {
    Night => true
    _ => false
  }
}

///|
pub struct Shift {
  id : String
  label : String
  range : TimeRange
  kind : ShiftKind
  required_workers : Int
  preferred_workers : Int
  requirements : Array[SkillRequirement]
  tags : Array[String]
  day_index : Int
  weekend : Bool
} derive(Eq, Show, ToJson)

///|
pub fn Shift::new(
  id : String,
  label : String,
  start : Int,
  end : Int,
  required_workers : Int,
) -> Shift {
  {
    id,
    label,
    range: TimeRange::new(start, end),
    kind: Day,
    required_workers,
    preferred_workers: required_workers,
    requirements: [],
    tags: [],
    day_index: start / 1440,
    weekend: false,
  }
}

///|
pub fn Shift::with_kind(self : Shift, kind : ShiftKind) -> Shift {
  { ..self, kind, }
}

///|
pub fn Shift::with_preferred_workers(self : Shift, count : Int) -> Shift {
  { ..self, preferred_workers: count }
}

///|
pub fn Shift::with_weekend(self : Shift, weekend : Bool) -> Shift {
  { ..self, weekend, }
}

///|
pub fn Shift::add_requirement(
  self : Shift,
  requirement : SkillRequirement,
) -> Shift {
  let requirements = self.requirements.copy()
  requirements.push(requirement)
  { ..self, requirements, }
}

///|
pub fn Shift::add_tag(self : Shift, tag : String) -> Shift {
  let tags = self.tags.copy()
  tags.push(tag)
  { ..self, tags, }
}

///|
pub struct Assignment {
  worker_id : String
  shift_id : String
  locked : Bool
} derive(Eq, Show, ToJson)

///|
pub fn Assignment::new(worker_id : String, shift_id : String) -> Assignment {
  { worker_id, shift_id, locked: false }
}

///|
pub fn Assignment::locked(worker_id : String, shift_id : String) -> Assignment {
  { worker_id, shift_id, locked: true }
}

///|
pub struct Schedule {
  assignments : Array[Assignment]
} derive(Eq, Show, ToJson)

///|
pub fn Schedule::empty() -> Schedule {
  { assignments: [] }
}

///|
pub fn Schedule::add(self : Schedule, assignment : Assignment) -> Schedule {
  let assignments = self.assignments.copy()
  assignments.push(assignment)
  { assignments, }
}

///|
pub fn Schedule::remove(
  self : Schedule,
  worker_id : String,
  shift_id : String,
) -> Schedule {
  let assignments : Array[Assignment] = []
  for assignment in self.assignments {
    if assignment.worker_id != worker_id || assignment.shift_id != shift_id {
      assignments.push(assignment)
    }
  }
  { assignments, }
}

///|
pub fn Schedule::contains(
  self : Schedule,
  worker_id : String,
  shift_id : String,
) -> Bool {
  for assignment in self.assignments {
    if assignment.worker_id == worker_id && assignment.shift_id == shift_id {
      return true
    }
  }
  false
}

///|
pub fn Schedule::workers_for(
  self : Schedule,
  shift_id : String,
) -> Array[String] {
  let result : Array[String] = []
  for assignment in self.assignments {
    if assignment.shift_id == shift_id {
      result.push(assignment.worker_id)
    }
  }
  result
}

///|
pub fn Schedule::shifts_for(
  self : Schedule,
  worker_id : String,
) -> Array[String] {
  let result : Array[String] = []
  for assignment in self.assignments {
    if assignment.worker_id == worker_id {
      result.push(assignment.shift_id)
    }
  }
  result
}

///|
pub struct FairnessWeights {
  workload : Int
  night : Int
  weekend : Int
  preference : Int
  understaffing : Int
  overtime : Int
} derive(Eq, Show, ToJson)

///|
pub fn FairnessWeights::balanced() -> FairnessWeights {
  {
    workload: 10,
    night: 8,
    weekend: 6,
    preference: 4,
    understaffing: 1000,
    overtime: 200,
  }
}

///|
pub struct PlanningProblem {
  name : String
  workers : Array[Worker]
  skills : Array[Skill]
  shifts : Array[Shift]
  locked_assignments : Array[Assignment]
  weights : FairnessWeights
  seed : Int
} derive(Eq, Show, ToJson)

///|
pub fn PlanningProblem::new(name : String) -> PlanningProblem {
  {
    name,
    workers: [],
    skills: [],
    shifts: [],
    locked_assignments: [],
    weights: FairnessWeights::balanced(),
    seed: 1,
  }
}

///|
pub fn PlanningProblem::add_worker(
  self : PlanningProblem,
  worker : Worker,
) -> PlanningProblem {
  let workers = self.workers.copy()
  workers.push(worker)
  { ..self, workers, }
}

///|
pub fn PlanningProblem::add_skill(
  self : PlanningProblem,
  skill : Skill,
) -> PlanningProblem {
  let skills = self.skills.copy()
  skills.push(skill)
  { ..self, skills, }
}

///|
pub fn PlanningProblem::add_shift(
  self : PlanningProblem,
  shift : Shift,
) -> PlanningProblem {
  let shifts = self.shifts.copy()
  shifts.push(shift)
  { ..self, shifts, }
}

///|
pub fn PlanningProblem::lock(
  self : PlanningProblem,
  worker_id : String,
  shift_id : String,
) -> PlanningProblem {
  let locked_assignments = self.locked_assignments.copy()
  locked_assignments.push(Assignment::locked(worker_id, shift_id))
  { ..self, locked_assignments, }
}

///|
pub fn PlanningProblem::worker(self : PlanningProblem, id : String) -> Worker? {
  for worker in self.workers {
    if worker.id == id {
      return Some(worker)
    }
  }
  None
}

///|
pub fn PlanningProblem::shift(self : PlanningProblem, id : String) -> Shift? {
  for shift in self.shifts {
    if shift.id == id {
      return Some(shift)
    }
  }
  None
}

///|
pub(all) enum DiagnosticSeverity {
  Info
  Warning
  Error
} derive(Eq, Show, ToJson)

///|
pub struct Diagnostic {
  code : String
  severity : DiagnosticSeverity
  message : String
  worker_id : String?
  shift_id : String?
} derive(Eq, Show, ToJson)

///|
pub fn Diagnostic::new(
  code : String,
  severity : DiagnosticSeverity,
  message : String,
) -> Diagnostic {
  { code, severity, message, worker_id: None, shift_id: None }
}

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

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

///|
pub struct ScoreBreakdown {
  total : Int
  understaffing : Int
  skill_shortage : Int
  workload_imbalance : Int
  night_imbalance : Int
  weekend_imbalance : Int
  preference_cost : Int
  overtime_cost : Int
} derive(Eq, Show, ToJson)

///|
pub fn ScoreBreakdown::zero() -> ScoreBreakdown {
  {
    total: 0,
    understaffing: 0,
    skill_shortage: 0,
    workload_imbalance: 0,
    night_imbalance: 0,
    weekend_imbalance: 0,
    preference_cost: 0,
    overtime_cost: 0,
  }
}

///|
pub struct SolveStats {
  explored_nodes : Int
  pruned_nodes : Int
  improvements : Int
  elapsed_steps : Int
} derive(Eq, Show, ToJson)

///|
pub struct Solution {
  schedule : Schedule
  score : ScoreBreakdown
  diagnostics : Array[Diagnostic]
  feasible : Bool
  stats : SolveStats
} derive(Eq, Show, ToJson)