///|
fn diagnostic(
  code : DiagnosticCode,
  message : String,
  person_id? : PersonId = "",
  shift_id? : ShiftId = "",
  slot? : Int = -1,
  severity? : Int = 2,
) -> Diagnostic {
  {
    code,
    message,
    person_id: if person_id == "" {
      None
    } else {
      Some(person_id)
    },
    shift_id: if shift_id == "" {
      None
    } else {
      Some(shift_id)
    },
    slot: if slot < 0 {
      None
    } else {
      Some(slot)
    },
    severity,
  }
}

///|
pub fn validate_problem(problem : ScheduleProblem) -> Array[Diagnostic] {
  let out : Array[Diagnostic] = []
  if problem.minimum_rest_minutes < 0 {
    out.push(
      diagnostic(InvalidConfiguration, "minimum rest must not be negative"),
    )
  }
  for i = 0; i < problem.people.length(); i = i + 1 {
    let person = problem.people[i]
    if person.id == "" || person.name == "" {
      out.push(
        diagnostic(
          InvalidConfiguration,
          "person id and name are required",
          person_id=person.id,
        ),
      )
    }
    if person.target_minutes < 0 ||
      person.maximum_minutes < person.target_minutes {
      out.push(
        diagnostic(
          InvalidConfiguration,
          "person minute limits are inconsistent",
          person_id=person.id,
        ),
      )
    }
    for span in person.availability {
      if !span.is_valid() {
        out.push(
          diagnostic(
            InvalidTimeSpan,
            "availability span is invalid",
            person_id=person.id,
          ),
        )
      }
    }
    for j = i + 1; j < problem.people.length(); j = j + 1 {
      if problem.people[j].id == person.id {
        out.push(
          diagnostic(
            DuplicatePerson,
            "duplicate person id \{person.id}",
            person_id=person.id,
          ),
        )
      }
    }
  }
  for i = 0; i < problem.shifts.length(); i = i + 1 {
    let shift = problem.shifts[i]
    if !shift.time.is_valid() {
      out.push(
        diagnostic(InvalidTimeSpan, "shift span is invalid", shift_id=shift.id),
      )
    }
    if shift.required_people <= 0 {
      out.push(
        diagnostic(
          InvalidConfiguration,
          "shift requires at least one person",
          shift_id=shift.id,
        ),
      )
    }
    for requirement in shift.requirements {
      if requirement.skill == "" || requirement.minimum_level <= 0 {
        out.push(
          diagnostic(
            InvalidConfiguration,
            "skill requirement is invalid",
            shift_id=shift.id,
          ),
        )
      }
    }
    for j = i + 1; j < problem.shifts.length(); j = j + 1 {
      if problem.shifts[j].id == shift.id {
        out.push(
          diagnostic(
            DuplicateShift,
            "duplicate shift id \{shift.id}",
            shift_id=shift.id,
          ),
        )
      }
    }
  }
  for assignment in problem.existing {
    match find_person(problem, assignment.person_id) {
      None =>
        out.push(
          diagnostic(
            UnknownPerson,
            "assignment references unknown person",
            person_id=assignment.person_id,
            shift_id=assignment.shift_id,
            slot=assignment.slot,
          ),
        )
      Some(_) => ()
    }
    match find_shift(problem, assignment.shift_id) {
      None =>
        out.push(
          diagnostic(
            UnknownShift,
            "assignment references unknown shift",
            person_id=assignment.person_id,
            shift_id=assignment.shift_id,
            slot=assignment.slot,
          ),
        )
      Some(shift) =>
        if assignment.slot < 0 || assignment.slot >= shift.required_people {
          out.push(
            diagnostic(
              InvalidSlot,
              "assignment slot is outside shift capacity",
              person_id=assignment.person_id,
              shift_id=assignment.shift_id,
              slot=assignment.slot,
            ),
          )
        }
    }
  }
  out
}

///|
pub fn assigned_minutes(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  person_id : PersonId,
) -> Int {
  let mut total = 0
  for assignment in assignments {
    if assignment.person_id == person_id {
      match find_shift(problem, assignment.shift_id) {
        Some(shift) => total = total + shift.time.duration()
        None => ()
      }
    }
  }
  total
}

///|
pub fn can_assign(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  shift : Shift,
  slot : Int,
  person : Person,
) -> Diagnostic? {
  if slot < 0 || slot >= shift.required_people {
    return Some(
      diagnostic(
        InvalidSlot,
        "slot is outside shift capacity",
        person_id=person.id,
        shift_id=shift.id,
        slot~,
      ),
    )
  }
  if !is_available(person, shift) {
    return Some(
      diagnostic(
        OutsideAvailability,
        "person is unavailable for the complete shift",
        person_id=person.id,
        shift_id=shift.id,
        slot~,
      ),
    )
  }
  if !meets_requirements(person, shift) {
    return Some(
      diagnostic(
        SkillMismatch,
        "person does not meet all skill requirements",
        person_id=person.id,
        shift_id=shift.id,
        slot~,
      ),
    )
  }
  for existing in assignments {
    if existing.shift_id == shift.id && existing.slot == slot {
      return Some(
        diagnostic(
          DuplicateAssignment,
          "slot is already assigned",
          person_id=person.id,
          shift_id=shift.id,
          slot~,
        ),
      )
    }
    if existing.person_id == person.id {
      match find_shift(problem, existing.shift_id) {
        Some(other) => {
          if other.time.overlaps(shift.time) {
            return Some(
              diagnostic(
                OverlappingAssignment,
                "person already has an overlapping shift",
                person_id=person.id,
                shift_id=shift.id,
                slot~,
              ),
            )
          }
          let gap = other.time.gap_to(shift.time)
          if gap >= 0 && gap < problem.minimum_rest_minutes {
            return Some(
              diagnostic(
                InsufficientRest,
                "assignment would violate minimum rest",
                person_id=person.id,
                shift_id=shift.id,
                slot~,
              ),
            )
          }
        }
        None => ()
      }
    }
  }
  let after = assigned_minutes(problem, assignments, person.id) +
    shift.time.duration()
  if after > person.maximum_minutes {
    return Some(
      diagnostic(
        MaximumMinutesExceeded,
        "assignment exceeds maximum minutes",
        person_id=person.id,
        shift_id=shift.id,
        slot~,
      ),
    )
  }
  None
}

///|
pub fn validate_assignments(
  problem : ScheduleProblem,
  assignments : Array[Assignment],
  require_complete? : Bool = true,
) -> Array[Diagnostic] {
  let out = validate_problem(problem)
  let accepted : Array[Assignment] = []
  for assignment in assignments {
    match
      (
        find_shift(problem, assignment.shift_id),
        find_person(problem, assignment.person_id),
      ) {
      (Some(shift), Some(person)) =>
        match can_assign(problem, accepted, shift, assignment.slot, person) {
          Some(problem) => out.push(problem)
          None => accepted.push(assignment)
        }
      (None, _) =>
        out.push(
          diagnostic(
            UnknownShift,
            "unknown shift",
            shift_id=assignment.shift_id,
          ),
        )
      (_, None) =>
        out.push(
          diagnostic(
            UnknownPerson,
            "unknown person",
            person_id=assignment.person_id,
          ),
        )
    }
  }
  if require_complete {
    for shift in problem.shifts {
      for slot = 0; slot < shift.required_people; slot = slot + 1 {
        let mut found = false
        for assignment in assignments {
          if assignment.shift_id == shift.id && assignment.slot == slot {
            found = true
          }
        }
        if !found {
          out.push(
            diagnostic(
              UncoveredSlot,
              "required slot is uncovered",
              shift_id=shift.id,
              slot~,
            ),
          )
        }
      }
    }
  }
  out
}

///|
pub fn has_errors(diagnostics : Array[Diagnostic]) -> Bool {
  for item in diagnostics {
    if item.severity >= 2 {
      return true
    }
  }
  false
}