///|
fn validation_error(code : String, message : String) -> Diagnostic {
  Diagnostic::new(code, Error, message)
}

///|
fn has_duplicate_worker_ids(workers : Array[Worker]) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for i = 0; i < workers.length(); i = i + 1 {
    for j = i + 1; j < workers.length(); j = j + 1 {
      if workers[i].id == workers[j].id {
        diagnostics.push(
          validation_error(
            "duplicate_worker",
            "worker id appears more than once: " + workers[i].id,
          ).for_worker(workers[i].id),
        )
      }
    }
  }
  diagnostics
}

///|
fn has_duplicate_shift_ids(shifts : Array[Shift]) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for i = 0; i < shifts.length(); i = i + 1 {
    for j = i + 1; j < shifts.length(); j = j + 1 {
      if shifts[i].id == shifts[j].id {
        diagnostics.push(
          validation_error(
            "duplicate_shift",
            "shift id appears more than once: " + shifts[i].id,
          ).for_shift(shifts[i].id),
        )
      }
    }
  }
  diagnostics
}

///|
fn validate_workers(problem : PlanningProblem) -> Array[Diagnostic] {
  let diagnostics = has_duplicate_worker_ids(problem.workers)
  for worker in problem.workers {
    if worker.id.trim().length() == 0 {
      diagnostics.push(
        validation_error("empty_worker_id", "worker id cannot be empty"),
      )
    }
    if worker.name.trim().length() == 0 {
      diagnostics.push(
        validation_error("empty_worker_name", "worker name cannot be empty").for_worker(
          worker.id,
        ),
      )
    }
    if worker.min_minutes < 0 ||
      worker.target_minutes < 0 ||
      worker.max_minutes < 0 {
      diagnostics.push(
        validation_error(
          "negative_minutes", "worker minute limits cannot be negative",
        ).for_worker(worker.id),
      )
    }
    if worker.min_minutes > worker.target_minutes ||
      worker.target_minutes > worker.max_minutes {
      diagnostics.push(
        validation_error(
          "invalid_minute_limits", "expected min_minutes <= target_minutes <= max_minutes",
        ).for_worker(worker.id),
      )
    }
    if worker.max_consecutive_shifts <= 0 {
      diagnostics.push(
        validation_error(
          "invalid_consecutive_limit", "max_consecutive_shifts must be positive",
        ).for_worker(worker.id),
      )
    }
    if worker.min_rest_minutes < 0 {
      diagnostics.push(
        validation_error("negative_rest", "min_rest_minutes cannot be negative").for_worker(
          worker.id,
        ),
      )
    }
    for range in worker.availability {
      if not(range.is_valid()) {
        diagnostics.push(
          validation_error(
            "invalid_availability", "availability range is invalid",
          ).for_worker(worker.id),
        )
      }
    }
    for skill in worker.skills {
      let mut found = false
      for known in problem.skills {
        if known.id == skill.skill_id {
          found = true
        }
      }
      if not(found) {
        diagnostics.push(
          validation_error(
            "unknown_worker_skill",
            "worker references unknown skill: " + skill.skill_id,
          ).for_worker(worker.id),
        )
      }
    }
  }
  diagnostics
}

///|
fn validate_shifts(problem : PlanningProblem) -> Array[Diagnostic] {
  let diagnostics = has_duplicate_shift_ids(problem.shifts)
  for shift in problem.shifts {
    if shift.id.trim().length() == 0 {
      diagnostics.push(
        validation_error("empty_shift_id", "shift id cannot be empty"),
      )
    }
    if not(shift.range.is_valid()) {
      diagnostics.push(
        validation_error("invalid_shift_range", "shift time range is invalid").for_shift(
          shift.id,
        ),
      )
    }
    if shift.required_workers < 0 {
      diagnostics.push(
        validation_error(
          "negative_required_workers", "required_workers cannot be negative",
        ).for_shift(shift.id),
      )
    }
    if shift.preferred_workers < shift.required_workers {
      diagnostics.push(
        validation_error(
          "preferred_below_required", "preferred_workers cannot be lower than required_workers",
        ).for_shift(shift.id),
      )
    }
    for requirement in shift.requirements {
      if requirement.count <= 0 {
        diagnostics.push(
          validation_error(
            "invalid_skill_count", "skill requirement count must be positive",
          ).for_shift(shift.id),
        )
      }
      let mut found = false
      for skill in problem.skills {
        if skill.id == requirement.skill_id {
          found = true
        }
      }
      if not(found) {
        diagnostics.push(
          validation_error(
            "unknown_required_skill",
            "shift references unknown skill: " + requirement.skill_id,
          ).for_shift(shift.id),
        )
      }
    }
  }
  diagnostics
}

///|
fn validate_locked_assignments(problem : PlanningProblem) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for assignment in problem.locked_assignments {
    if problem.worker(assignment.worker_id) is None {
      diagnostics.push(
        validation_error(
          "unknown_locked_worker",
          "locked assignment references unknown worker: " + assignment.worker_id,
        ),
      )
    }
    if problem.shift(assignment.shift_id) is None {
      diagnostics.push(
        validation_error(
          "unknown_locked_shift",
          "locked assignment references unknown shift: " + assignment.shift_id,
        ),
      )
    }
  }
  diagnostics
}

///|
pub fn PlanningProblem::validate(self : PlanningProblem) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  if self.name.trim().length() == 0 {
    diagnostics.push(
      validation_error("empty_problem_name", "problem name cannot be empty"),
    )
  }
  if self.workers.length() == 0 {
    diagnostics.push(
      validation_error("no_workers", "at least one worker is required"),
    )
  }
  if self.shifts.length() == 0 {
    diagnostics.push(
      validation_error("no_shifts", "at least one shift is required"),
    )
  }
  diagnostics.append(validate_workers(self))
  diagnostics.append(validate_shifts(self))
  diagnostics.append(validate_locked_assignments(self))
  diagnostics
}

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