///|
/// A definite input or slot-capacity problem in a coverage request.
pub(all) enum CoverageIssue {
  CoverageDuplicateWorkerId(Int)
  CoverageNoEligibleWorker(Int, String)
  CoverageSlotShortfall(Int, Int, Int)
  CoverageWorkloadShortfall(Int, Int)
  CoveragePolicyShortfall(Int, Int)
} derive(Eq, Debug)

///|
pub fn CoverageIssue::message(self : CoverageIssue) -> String {
  match self {
    CoverageDuplicateWorkerId(id) =>
      "worker id \{id} is declared more than once"
    CoverageNoEligibleWorker(id, name) =>
      "coverage requirement \{id} (\{name}) has no eligible worker"
    CoverageSlotShortfall(slot, required, assignable) =>
      "slot \{slot} needs \{required} workers, but at most \{assignable} positions can be filled"
    CoverageWorkloadShortfall(required, assignable) =>
      "coverage needs \{required} assignments, but worker workload limits allow at most \{assignable}"
    CoveragePolicyShortfall(required, assignable) =>
      "coverage needs \{required} assignments, but skill, slot, and workload limits together allow at most \{assignable}"
  }
}

///|
/// Preflight issues reported against original coverage demand identifiers.
pub(all) struct CoverageAnalysis {
  issues : Array[CoverageIssue]
} derive(Eq, Debug)

///|
pub fn CoverageAnalysis::is_feasible(self : CoverageAnalysis) -> Bool {
  self.issues.length() == 0
}

///|
pub fn CoverageAnalysis::summary(self : CoverageAnalysis) -> String {
  if self.is_feasible() {
    return "coverage request passed preflight analysis"
  }
  let mut output = ""
  for index = 0; index < self.issues.length(); index = index + 1 {
    if index > 0 {
      output = output + "; "
    }
    output = output + self.issues[index].message()
  }
  output
}

///|
/// Find definite demand conflicts before applying workload or rest policy.
///
/// This does not prove that a policy-constrained roster is feasible.
pub fn analyze_coverage(
  workers : Array[Worker],
  requirements : Array[CoverageRequirement],
) -> Result[CoverageAnalysis, RosterError] {
  match validate_coverage_requirements(requirements) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let expansion = expand_coverage_requirements(requirements)
  let roster_analysis = analyze_roster(workers, expansion.shifts)
  let issues : Array[CoverageIssue] = []
  let reported = Array::make(requirements.length(), false)
  for issue in roster_analysis.issues {
    match issue {
      DuplicateWorkerIdFound(id) => issues.push(CoverageDuplicateWorkerId(id))
      NoEligibleWorkerFound(shift_id, _) => {
        let index = expansion.requirement_indexes[shift_id]
        if !reported[index] {
          let requirement = requirements[index]
          issues.push(
            CoverageNoEligibleWorker(requirement.id, requirement.name),
          )
          reported[index] = true
        }
      }
      SlotCoverageShortfall(slot, required, assignable) =>
        issues.push(CoverageSlotShortfall(slot, required, assignable))
      DuplicateShiftIdFound(_) => ()
    }
  }
  Ok({ issues, })
}

///|
/// Find definite coverage conflicts under the requested roster policy.
///
/// The workload bound counts each worker's distinct eligible slots, capped
/// by their workload limit. Rest rules can make capacity smaller, so a
/// passing analysis is not a proof that a roster exists.
pub fn analyze_coverage_with_policy(
  workers : Array[Worker],
  requirements : Array[CoverageRequirement],
  policy : RosterPolicy,
) -> Result[CoverageAnalysis, RosterError] {
  match validate_roster_policy(policy) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let analysis = match analyze_coverage(workers, requirements) {
    Err(error) => return Err(error)
    Ok(analysis) => analysis
  }
  if !analysis.is_feasible() {
    return Ok(analysis)
  }
  match policy.max_shifts_per_worker {
    None => Ok(analysis)
    Some(limit) => {
      let mut required = 0
      for requirement in requirements {
        required = required + requirement.workers_needed
      }
      let mut assignable = 0
      for worker in workers {
        let slots : Array[Int] = []
        for requirement in requirements {
          if (
              requirement.required_skill.length() == 0 ||
              worker.skills.contains(requirement.required_skill)
            ) &&
            worker.available_slots.contains(requirement.slot) &&
            !int_member(slots, requirement.slot) {
            slots.push(requirement.slot)
          }
        }
        let worker_capacity = if slots.length() < limit {
          slots.length()
        } else {
          limit
        }
        assignable = assignable + worker_capacity
      }
      if assignable < required {
        analysis.issues.push(CoverageWorkloadShortfall(required, assignable))
      } else {
        let expansion = expand_coverage_requirements(requirements)
        let fillable = maximum_policy_coverage(workers, expansion.shifts, limit)
        if fillable < required {
          analysis.issues.push(CoveragePolicyShortfall(required, fillable))
        }
      }
      Ok(analysis)
    }
  }
}