///|
pub struct ShiftSummary {
  shift_id : String
  label : String
  assigned : Int
  required : Int
  qualified_requirements : Int
  total_requirements : Int
  covered : Bool
} derive(Eq, Show, ToJson)

///|
pub struct ScheduleSummary {
  problem_name : String
  worker_count : Int
  shift_count : Int
  assignment_count : Int
  coverage_per_mille : Int
  workload_spread : Int
  night_spread : Int
  weekend_spread : Int
  feasible : Bool
  score : ScoreBreakdown
  workers : Array[WorkerSummary]
  shifts : Array[ShiftSummary]
} derive(Eq, Show, ToJson)

///|
fn count_requirement_coverage(
  problem : PlanningProblem,
  schedule : Schedule,
  shift : Shift,
) -> Int {
  let mut covered = 0
  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 {
      covered = covered + 1
    }
  }
  covered
}

///|
pub fn summarize_shift(
  problem : PlanningProblem,
  schedule : Schedule,
  shift_id : String,
) -> ShiftSummary? {
  match problem.shift(shift_id) {
    None => None
    Some(shift) => {
      let assigned = assignment_count_for_shift(schedule, shift.id)
      let qualified = count_requirement_coverage(problem, schedule, shift)
      Some({
        shift_id,
        label: shift.label,
        assigned,
        required: shift.required_workers,
        qualified_requirements: qualified,
        total_requirements: shift.requirements.length(),
        covered: assigned >= shift.required_workers &&
        qualified == shift.requirements.length(),
      })
    }
  }
}

///|
fn spread(values : Array[Int]) -> Int {
  if values.length() == 0 {
    return 0
  }
  let mut low = values[0]
  let mut high = values[0]
  for value in values {
    if value < low {
      low = value
    }
    if value > high {
      high = value
    }
  }
  high - low
}

///|
pub fn summarize_schedule(
  problem : PlanningProblem,
  solution : Solution,
) -> ScheduleSummary {
  let workers : Array[WorkerSummary] = []
  let nights : Array[Int] = []
  let weekends : Array[Int] = []
  for worker in problem.workers {
    match summarize_worker(problem, solution.schedule, worker.id) {
      Some(summary) => {
        workers.push(summary)
        nights.push(summary.night_shifts)
        weekends.push(summary.weekend_shifts)
      }
      None => ()
    }
  }
  let shifts : Array[ShiftSummary] = []
  for shift in problem.shifts {
    match summarize_shift(problem, solution.schedule, shift.id) {
      Some(summary) => shifts.push(summary)
      None => ()
    }
  }
  {
    problem_name: problem.name,
    worker_count: problem.workers.length(),
    shift_count: problem.shifts.length(),
    assignment_count: solution.schedule.assignments.length(),
    coverage_per_mille: coverage_ratio_per_mille(problem, solution.schedule),
    workload_spread: workload_range(problem, solution.schedule),
    night_spread: spread(nights),
    weekend_spread: spread(weekends),
    feasible: solution.feasible,
    score: solution.score,
    workers,
    shifts,
  }
}

///|
fn csv_escape(value : String) -> String {
  let mut needs_quotes = false
  for ch in value {
    if ch == ',' || ch == '"' || ch == '\n' || ch == '\r' {
      needs_quotes = true
    }
  }
  if !needs_quotes {
    return value
  }
  let buffer = StringBuilder::new()
  buffer.write_char('"')
  for ch in value {
    if ch == '"' {
      buffer.write_string("\"\"")
    } else {
      buffer.write_char(ch)
    }
  }
  buffer.write_char('"')
  buffer.to_string()
}

///|
pub fn schedule_to_csv(
  problem : PlanningProblem,
  solution : Solution,
) -> String {
  let output = StringBuilder::new()
  output.write_string(
    "shift_id,shift_label,worker_id,worker_name,start_minute,end_minute,kind\n",
  )
  for shift in problem.shifts {
    for worker_id in solution.schedule.workers_for(shift.id) {
      let worker_name = match problem.worker(worker_id) {
        Some(worker) => worker.name
        None => "unknown"
      }
      output.write_string(csv_escape(shift.id))
      output.write_char(',')
      output.write_string(csv_escape(shift.label))
      output.write_char(',')
      output.write_string(csv_escape(worker_id))
      output.write_char(',')
      output.write_string(csv_escape(worker_name))
      output.write_char(',')
      output.write_string(shift.range.start.value().to_string())
      output.write_char(',')
      output.write_string(shift.range.end.value().to_string())
      output.write_char(',')
      output.write_string(csv_escape(shift.kind.to_string()))
      output.write_char('\n')
    }
  }
  output.to_string()
}

///|
pub fn workload_to_csv(
  problem : PlanningProblem,
  solution : Solution,
) -> String {
  let output = StringBuilder::new()
  output.write_string(
    "worker_id,worker_name,assigned_minutes,target_minutes,night_shifts,weekend_shifts\n",
  )
  for worker in problem.workers {
    match summarize_worker(problem, solution.schedule, worker.id) {
      None => ()
      Some(summary) => {
        output.write_string(csv_escape(worker.id))
        output.write_char(',')
        output.write_string(csv_escape(worker.name))
        output.write_char(',')
        output.write_string(summary.assigned_minutes.to_string())
        output.write_char(',')
        output.write_string(summary.target_minutes.to_string())
        output.write_char(',')
        output.write_string(summary.night_shifts.to_string())
        output.write_char(',')
        output.write_string(summary.weekend_shifts.to_string())
        output.write_char('\n')
      }
    }
  }
  output.to_string()
}

///|
fn render_status(value : Bool) -> String {
  if value {
    "yes"
  } else {
    "no"
  }
}

///|
pub fn render_text_report(
  problem : PlanningProblem,
  solution : Solution,
) -> String {
  let summary = summarize_schedule(problem, solution)
  let output = StringBuilder::new()
  output.write_string("RosterWeave schedule report\n")
  output.write_string("=========================\n")
  output.write_string("Problem: " + summary.problem_name + "\n")
  output.write_string("Feasible: " + render_status(summary.feasible) + "\n")
  output.write_string("Workers: " + summary.worker_count.to_string() + "\n")
  output.write_string("Shifts: " + summary.shift_count.to_string() + "\n")
  output.write_string(
    "Assignments: " + summary.assignment_count.to_string() + "\n",
  )
  output.write_string(
    "Coverage: " + summary.coverage_per_mille.to_string() + "/1000\n",
  )
  output.write_string("Objective: " + summary.score.total.to_string() + "\n")
  output.write_string(
    "Workload spread: " + summary.workload_spread.to_string() + " minutes\n",
  )
  output.write_string("\nAssignments\n")
  output.write_string("-----------\n")
  for shift in problem.shifts {
    output.write_string(shift.label + ": ")
    let names : Array[String] = []
    for worker_id in solution.schedule.workers_for(shift.id) {
      match problem.worker(worker_id) {
        Some(worker) => names.push(worker.name)
        None => names.push(worker_id)
      }
    }
    output.write_string(names.join(", "))
    output.write_char('\n')
  }
  let explanations = explain_solution(problem, solution)
  if explanations.length() > 0 {
    output.write_string("\nNotes\n")
    output.write_string("-----\n")
    for explanation in explanations {
      output.write_string(
        "- " + explanation.title + ": " + explanation.detail + "\n",
      )
    }
  }
  output.to_string()
}

///|
pub fn solution_to_json(
  problem : PlanningProblem,
  solution : Solution,
) -> String {
  summarize_schedule(problem, solution).to_json().stringify(indent=2)
}