///|
pub(all) enum RiskLevel {
  LowRisk
  ModerateRisk
  HighRisk
  CriticalRisk
} derive(Eq, Compare, Show, ToJson)

///|
pub struct ShiftCapacity {
  shift_id : String
  required_workers : Int
  available_workers : Int
  eligible_workers : Int
  capacity_margin : Int
  risk : RiskLevel
} derive(Eq, Show, ToJson)

///|
pub struct SkillCapacity {
  skill_id : String
  required_slots : Int
  qualified_workers : Int
  qualification_instances : Int
  margin : Int
  risk : RiskLevel
} derive(Eq, Show, ToJson)

///|
pub struct CapacityReport {
  shifts : Array[ShiftCapacity]
  skills : Array[SkillCapacity]
  critical_shifts : Int
  critical_skills : Int
  total_required_slots : Int
  total_eligible_instances : Int
  resilience_score : Int
} derive(Eq, Show, ToJson)

///|
fn capacity_risk(required : Int, available : Int) -> RiskLevel {
  if available < required {
    CriticalRisk
  } else if available == required {
    HighRisk
  } else if available <= required * 2 {
    ModerateRisk
  } else {
    LowRisk
  }
}

///|
fn worker_satisfies_all_requirements(worker : Worker, shift : Shift) -> Bool {
  for requirement in shift.requirements {
    if !worker.has_skill(requirement.skill_id, requirement.minimum_level) {
      return false
    }
  }
  true
}

///|
pub fn shift_capacity(
  problem : PlanningProblem,
  shift_id : String,
) -> ShiftCapacity? {
  match problem.shift(shift_id) {
    None => None
    Some(shift) => {
      let mut available = 0
      let mut eligible = 0
      for worker in problem.workers {
        if worker.is_available(shift.range) {
          available = available + 1
          if worker_satisfies_all_requirements(worker, shift) {
            eligible = eligible + 1
          }
        }
      }
      Some({
        shift_id,
        required_workers: shift.required_workers,
        available_workers: available,
        eligible_workers: eligible,
        capacity_margin: available - shift.required_workers,
        risk: capacity_risk(shift.required_workers, available),
      })
    }
  }
}

///|
fn skill_required_slots(problem : PlanningProblem, skill_id : String) -> Int {
  let mut total = 0
  for shift in problem.shifts {
    for requirement in shift.requirements {
      if requirement.skill_id == skill_id {
        total = total + requirement.count
      }
    }
  }
  total
}

///|
fn qualified_worker_count(problem : PlanningProblem, skill_id : String) -> Int {
  let mut total = 0
  for worker in problem.workers {
    match worker.skill_level(skill_id) {
      Some(_) => total = total + 1
      None => ()
    }
  }
  total
}

///|
fn qualification_instances(problem : PlanningProblem, skill_id : String) -> Int {
  let mut total = 0
  for shift in problem.shifts {
    for requirement in shift.requirements {
      if requirement.skill_id == skill_id {
        for worker in problem.workers {
          if worker.is_available(shift.range) &&
            worker.has_skill(skill_id, requirement.minimum_level) {
            total = total + 1
          }
        }
      }
    }
  }
  total
}

///|
pub fn skill_capacity(
  problem : PlanningProblem,
  skill_id : String,
) -> SkillCapacity {
  let required = skill_required_slots(problem, skill_id)
  let qualified = qualified_worker_count(problem, skill_id)
  let instances = qualification_instances(problem, skill_id)
  {
    skill_id,
    required_slots: required,
    qualified_workers: qualified,
    qualification_instances: instances,
    margin: instances - required,
    risk: capacity_risk(required, instances),
  }
}

///|
fn resilience_score(
  total_required : Int,
  total_eligible : Int,
  critical_shifts : Int,
  critical_skills : Int,
) -> Int {
  if total_required == 0 {
    return 1000
  }
  let base = (total_eligible * 1000 / total_required).clamp(min=0, max=1000)
  let penalty = critical_shifts * 80 + critical_skills * 120
  (base - penalty).clamp(min=0, max=1000)
}

///|
pub fn analyze_capacity(problem : PlanningProblem) -> CapacityReport {
  let shifts : Array[ShiftCapacity] = []
  let skills : Array[SkillCapacity] = []
  let mut critical_shifts = 0
  let mut critical_skills = 0
  let mut total_required = 0
  let mut total_eligible = 0
  for shift in problem.shifts {
    match shift_capacity(problem, shift.id) {
      Some(capacity) => {
        shifts.push(capacity)
        total_required = total_required + capacity.required_workers
        total_eligible = total_eligible + capacity.eligible_workers
        if capacity.risk == CriticalRisk {
          critical_shifts = critical_shifts + 1
        }
      }
      None => ()
    }
  }
  for skill in problem.skills {
    let capacity = skill_capacity(problem, skill.id)
    skills.push(capacity)
    if capacity.risk == CriticalRisk {
      critical_skills = critical_skills + 1
    }
  }
  {
    shifts,
    skills,
    critical_shifts,
    critical_skills,
    total_required_slots: total_required,
    total_eligible_instances: total_eligible,
    resilience_score: resilience_score(
      total_required, total_eligible, critical_shifts, critical_skills,
    ),
  }
}

///|
pub struct AbsenceImpact {
  worker_id : String
  baseline_feasible : Bool
  absence_feasible : Bool
  baseline_score : Int
  absence_score : Int
  score_increase : Int
  baseline_coverage : Int
  absence_coverage : Int
  coverage_loss : Int
  newly_critical_shifts : Int
  severity : RiskLevel
} derive(Eq, Show, ToJson)

///|
fn without_worker(
  problem : PlanningProblem,
  worker_id : String,
) -> PlanningProblem {
  let workers : Array[Worker] = []
  for worker in problem.workers {
    if worker.id != worker_id {
      workers.push(worker)
    }
  }
  let locked : Array[Assignment] = []
  for assignment in problem.locked_assignments {
    if assignment.worker_id != worker_id {
      locked.push(assignment)
    }
  }
  { ..problem, workers, locked_assignments: locked }
}

///|
fn critical_shift_count(report : CapacityReport) -> Int {
  let mut count = 0
  for shift in report.shifts {
    if shift.risk == CriticalRisk || shift.risk == HighRisk {
      count = count + 1
    }
  }
  count
}

///|
fn impact_risk(
  absence_feasible : Bool,
  coverage_loss : Int,
  score_increase : Int,
  new_critical : Int,
) -> RiskLevel {
  if !absence_feasible || coverage_loss >= 200 {
    CriticalRisk
  } else if coverage_loss > 0 || new_critical >= 3 {
    HighRisk
  } else if score_increase > 1000 || new_critical > 0 {
    ModerateRisk
  } else {
    LowRisk
  }
}

///|
pub fn analyze_absence(
  problem : PlanningProblem,
  worker_id : String,
  config : SolverConfig,
) -> AbsenceImpact? {
  if problem.worker(worker_id) is None {
    return None
  }
  let baseline = solve_with_config(problem, config)
  let baseline_capacity = analyze_capacity(problem)
  let reduced_problem = without_worker(problem, worker_id)
  let absence = solve_with_config(reduced_problem, config)
  let absence_capacity = analyze_capacity(reduced_problem)
  let baseline_coverage = coverage_ratio_per_mille(problem, baseline.schedule)
  let absence_coverage = coverage_ratio_per_mille(
    reduced_problem,
    absence.schedule,
  )
  let coverage_loss = baseline_coverage - absence_coverage
  let score_increase = absence.score.total - baseline.score.total
  let new_critical = (critical_shift_count(absence_capacity) -
  critical_shift_count(baseline_capacity)).max(0)
  Some({
    worker_id,
    baseline_feasible: baseline.feasible,
    absence_feasible: absence.feasible,
    baseline_score: baseline.score.total,
    absence_score: absence.score.total,
    score_increase,
    baseline_coverage,
    absence_coverage,
    coverage_loss,
    newly_critical_shifts: new_critical,
    severity: impact_risk(
      absence.feasible,
      coverage_loss,
      score_increase,
      new_critical,
    ),
  })
}

///|
pub struct SensitivityReport {
  impacts : Array[AbsenceImpact]
  critical_workers : Array[String]
  high_risk_workers : Array[String]
  most_critical_worker : String?
  worst_coverage_loss : Int
  worst_score_increase : Int
} derive(Eq, Show, ToJson)

///|
fn impact_before(left : AbsenceImpact, right : AbsenceImpact) -> Bool {
  if left.severity != right.severity {
    left.severity > right.severity
  } else if left.coverage_loss != right.coverage_loss {
    left.coverage_loss > right.coverage_loss
  } else if left.score_increase != right.score_increase {
    left.score_increase > right.score_increase
  } else {
    left.worker_id < right.worker_id
  }
}

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

///|
pub fn analyze_sensitivity(
  problem : PlanningProblem,
  config : SolverConfig,
) -> SensitivityReport {
  let impacts : Array[AbsenceImpact] = []
  for worker in problem.workers {
    match analyze_absence(problem, worker.id, config) {
      Some(impact) => impacts.push(impact)
      None => ()
    }
  }
  let impacts = sort_impacts(impacts)
  let critical_workers : Array[String] = []
  let high_risk_workers : Array[String] = []
  let mut worst_coverage_loss = 0
  let mut worst_score_increase = 0
  for impact in impacts {
    if impact.severity == CriticalRisk {
      critical_workers.push(impact.worker_id)
    }
    if impact.severity == CriticalRisk || impact.severity == HighRisk {
      high_risk_workers.push(impact.worker_id)
    }
    if impact.coverage_loss > worst_coverage_loss {
      worst_coverage_loss = impact.coverage_loss
    }
    if impact.score_increase > worst_score_increase {
      worst_score_increase = impact.score_increase
    }
  }
  {
    impacts,
    critical_workers,
    high_risk_workers,
    most_critical_worker: if impacts.length() == 0 {
      None
    } else {
      Some(impacts[0].worker_id)
    },
    worst_coverage_loss,
    worst_score_increase,
  }
}

///|
pub struct CapacityRecommendation {
  code : String
  title : String
  detail : String
  priority : Int
  worker_id : String?
  shift_id : String?
  skill_id : String?
} derive(Eq, Show, ToJson)

///|
fn recommendation(
  code : String,
  title : String,
  detail : String,
  priority : Int,
) -> CapacityRecommendation {
  {
    code,
    title,
    detail,
    priority,
    worker_id: None,
    shift_id: None,
    skill_id: None,
  }
}

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

///|
fn CapacityRecommendation::for_skill(
  self : CapacityRecommendation,
  skill_id : String,
) -> CapacityRecommendation {
  { ..self, skill_id: Some(skill_id) }
}

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

///|
fn recommendation_before(
  left : CapacityRecommendation,
  right : CapacityRecommendation,
) -> Bool {
  if left.priority != right.priority {
    left.priority > right.priority
  } else {
    left.code < right.code
  }
}

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

///|
pub fn capacity_recommendations(
  problem : PlanningProblem,
  capacity : CapacityReport,
  sensitivity : SensitivityReport,
) -> Array[CapacityRecommendation] {
  let recommendations : Array[CapacityRecommendation] = []
  for shift in capacity.shifts {
    if shift.risk == CriticalRisk {
      recommendations.push(
        recommendation(
          "increase_shift_availability",
          "Increase availability for a critical shift",
          "Only " +
          shift.available_workers.to_string() +
          " worker(s) are available for " +
          shift.required_workers.to_string() +
          " required slot(s).",
          1000,
        ).for_shift(shift.shift_id),
      )
    } else if shift.risk == HighRisk {
      recommendations.push(
        recommendation(
          "add_shift_backup", "Add a backup worker", "The shift has no capacity margin if one worker becomes unavailable.",
          700,
        ).for_shift(shift.shift_id),
      )
    }
  }
  for skill in capacity.skills {
    if skill.risk == CriticalRisk || skill.risk == HighRisk {
      recommendations.push(
        recommendation(
          "cross_train_skill",
          "Cross-train an additional worker",
          "Skill " +
          skill.skill_id +
          " has a capacity margin of " +
          skill.margin.to_string() +
          ".",
          if skill.risk == CriticalRisk {
            950
          } else {
            650
          },
        ).for_skill(skill.skill_id),
      )
    }
  }
  for worker_id in sensitivity.critical_workers {
    let worker_name = match problem.worker(worker_id) {
      Some(worker) => worker.name
      None => worker_id
    }
    recommendations.push(
      recommendation(
        "reduce_single_point_of_failure",
        "Reduce dependency on " + worker_name,
        "The schedule becomes critically degraded when this worker is absent.",
        900,
      ).for_worker(worker_id),
    )
  }
  if capacity.resilience_score < 500 {
    recommendations.push(
      recommendation(
        "review_staffing_plan", "Review the overall staffing plan", "The resilience score is below 500/1000; consider more workers, broader availability, or lower demand.",
        850,
      ),
    )
  }
  sort_recommendations(recommendations)
}

///|
pub fn render_capacity_report(
  problem : PlanningProblem,
  capacity : CapacityReport,
) -> String {
  let output = StringBuilder::new()
  output.write_string("FairShift capacity report\n")
  output.write_string("=========================\n")
  output.write_string("Problem: " + problem.name + "\n")
  output.write_string(
    "Resilience: " + capacity.resilience_score.to_string() + "/1000\n",
  )
  output.write_string(
    "Critical shifts: " + capacity.critical_shifts.to_string() + "\n",
  )
  output.write_string(
    "Critical skills: " + capacity.critical_skills.to_string() + "\n",
  )
  output.write_string("\nShift capacity\n")
  output.write_string("--------------\n")
  for shift in capacity.shifts {
    let label = match problem.shift(shift.shift_id) {
      Some(item) => item.label
      None => shift.shift_id
    }
    output.write_string(
      label +
      ": required=" +
      shift.required_workers.to_string() +
      ", available=" +
      shift.available_workers.to_string() +
      ", eligible=" +
      shift.eligible_workers.to_string() +
      ", risk=" +
      shift.risk.to_string() +
      "\n",
    )
  }
  output.write_string("\nSkill capacity\n")
  output.write_string("--------------\n")
  for skill in capacity.skills {
    output.write_string(
      skill.skill_id +
      ": required=" +
      skill.required_slots.to_string() +
      ", instances=" +
      skill.qualification_instances.to_string() +
      ", margin=" +
      skill.margin.to_string() +
      ", risk=" +
      skill.risk.to_string() +
      "\n",
    )
  }
  output.to_string()
}