///|
/// Validate one time window and retain an entity context.
pub fn validate_time_window(
  window : TimeWindow,
  entity_id : String,
  field : String,
) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if window.start_minute < 0 {
    issues.push(
      error_diagnostic(
        "time.negative_start", "time window start must not be negative",
      )
      .for_entity(entity_id)
      .at_field(field),
    )
  }
  if window.end_minute <= window.start_minute {
    issues.push(
      error_diagnostic(
        "time.invalid_order", "time window end must be after its start",
      )
      .for_entity(entity_id)
      .at_field(field),
    )
  }
  if window.end_minute > 10080 {
    issues.push(
      warning_diagnostic(
        "time.outside_week", "time window extends beyond a seven-day horizon",
      )
      .for_entity(entity_id)
      .at_field(field),
    )
  }
  issues
}

///|
/// Validate location identity and coordinate bounds.
pub fn validate_location(value : Location) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if value.id.trim().is_empty() {
    issues.push(
      error_diagnostic("location.empty_id", "location id must not be empty").at_field(
        "id",
      ),
    )
  }
  if value.zone.trim().is_empty() {
    issues.push(
      warning_diagnostic("location.empty_zone", "location has no service zone")
      .for_entity(value.id)
      .at_field("zone"),
    )
  }
  if abs_int(value.x) > 100000 || abs_int(value.y) > 100000 {
    issues.push(
      error_diagnostic(
        "location.coordinate_range", "location coordinates exceed supported range",
      ).for_entity(value.id),
    )
  }
  issues
}

///|
/// Validate skill name and level.
pub fn validate_skill(value : Skill, entity_id : String) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if value.name.trim().is_empty() {
    issues.push(
      error_diagnostic("skill.empty_name", "skill name must not be empty").for_entity(
        entity_id,
      ),
    )
  }
  if value.level <= 0 || value.level > 10 {
    issues.push(
      error_diagnostic(
        "skill.invalid_level", "skill level must be between 1 and 10",
      )
      .for_entity(entity_id)
      .at_field(value.name),
    )
  }
  issues
}

///|
/// Validate one worker independently from other request entities.
pub fn validate_worker(value : Worker) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if value.id.trim().is_empty() {
    issues.push(
      error_diagnostic("worker.empty_id", "worker id must not be empty"),
    )
  }
  if value.display_name.trim().is_empty() {
    issues.push(
      warning_diagnostic(
        "worker.empty_display_name", "worker display name is empty",
      ).for_entity(value.id),
    )
  }
  if value.max_minutes <= 0 {
    issues.push(
      error_diagnostic(
        "worker.invalid_max_minutes", "worker maximum minutes must be positive",
      )
      .for_entity(value.id)
      .at_field("max_minutes"),
    )
  }
  if value.max_visits <= 0 {
    issues.push(
      error_diagnostic(
        "worker.invalid_max_visits", "worker maximum visits must be positive",
      )
      .for_entity(value.id)
      .at_field("max_visits"),
    )
  }
  if value.min_break_minutes < 0 {
    issues.push(
      error_diagnostic(
        "worker.negative_break", "worker minimum break must not be negative",
      )
      .for_entity(value.id)
      .at_field("min_break_minutes"),
    )
  }
  if value.availability.length() == 0 && !value.unavailable {
    issues.push(
      warning_diagnostic(
        "worker.no_availability", "available worker has no availability windows",
      ).for_entity(value.id),
    )
  }
  for window in value.availability {
    issues.append(validate_time_window(window, value.id, "availability"))
  }
  for capability in value.skills {
    issues.append(validate_skill(capability, value.id))
  }
  let seen_skills : Array[String] = []
  for capability in value.skills {
    if string_array_contains(seen_skills, capability.name) {
      issues.push(
        warning_diagnostic(
          "worker.duplicate_skill", "worker declares the same skill more than once",
        )
        .for_entity(value.id)
        .at_field(capability.name),
      )
    }
    push_unique_string(seen_skills, capability.name)
  }
  issues.append(validate_location(value.home))
  issues
}

///|
/// Validate one requested visit independently.
pub fn validate_visit(value : Visit) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if value.id.trim().is_empty() {
    issues.push(
      error_diagnostic("visit.empty_id", "visit id must not be empty"),
    )
  }
  if value.recipient_id.trim().is_empty() {
    issues.push(
      error_diagnostic(
        "visit.empty_recipient", "visit recipient id must not be empty",
      ).for_entity(value.id),
    )
  }
  if value.title.trim().is_empty() {
    issues.push(
      warning_diagnostic("visit.empty_title", "visit title is empty").for_entity(
        value.id,
      ),
    )
  }
  if value.duration_minutes <= 0 {
    issues.push(
      error_diagnostic(
        "visit.invalid_duration", "visit duration must be positive",
      )
      .for_entity(value.id)
      .at_field("duration_minutes"),
    )
  }
  if value.duration_minutes > value.window.duration() {
    issues.push(
      error_diagnostic(
        "visit.duration_exceeds_window", "visit duration is longer than its allowed time window",
      ).for_entity(value.id),
    )
  }
  issues.append(validate_time_window(value.window, value.id, "window"))
  issues.append(validate_location(value.location))
  for requirement in value.required_skills {
    issues.append(validate_skill(requirement, value.id))
  }
  for worker_id in value.preferred_worker_ids {
    if value.forbids(worker_id) {
      issues.push(
        error_diagnostic(
          "visit.worker_both_preferred_and_forbidden", "worker cannot be both preferred and forbidden",
        )
        .for_entity(value.id)
        .at_field(worker_id),
      )
    }
  }
  issues
}

///|
/// Validate policy weights and search limits.
pub fn validate_policy(value : SchedulePolicy) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if value.travel_minutes_per_unit <= 0 {
    issues.push(
      error_diagnostic(
        "policy.invalid_travel_rate", "travel minutes per unit must be positive",
      ),
    )
  }
  if value.cross_zone_penalty_minutes < 0 {
    issues.push(
      error_diagnostic(
        "policy.negative_zone_penalty", "cross-zone penalty must not be negative",
      ),
    )
  }
  let weights = [
    value.priority_weight,
    value.preference_weight,
    value.continuity_weight,
    value.travel_weight,
    value.fairness_weight,
    value.disruption_weight,
  ]
  for weight in weights {
    if weight < 0 {
      issues.push(
        error_diagnostic(
          "policy.negative_weight", "schedule policy weights must not be negative",
        ),
      )
    }
  }
  if value.local_search_rounds < 0 || value.local_search_rounds > 1000 {
    issues.push(
      error_diagnostic(
        "policy.invalid_search_rounds", "local search rounds must be 0 through 1000",
      ),
    )
  }
  issues
}

///|
/// Report duplicate identifiers and cross-entity references.
fn validate_request_relations(request : ScheduleRequest) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  let worker_ids : Array[String] = []
  for worker in request.workers {
    if string_array_contains(worker_ids, worker.id) {
      issues.push(
        error_diagnostic(
          "request.duplicate_worker_id", "worker identifiers must be unique",
        ).for_entity(worker.id),
      )
    }
    push_unique_string(worker_ids, worker.id)
  }
  let visit_ids : Array[String] = []
  for visit in request.visits {
    if string_array_contains(visit_ids, visit.id) {
      issues.push(
        error_diagnostic(
          "request.duplicate_visit_id", "visit identifiers must be unique",
        ).for_entity(visit.id),
      )
    }
    push_unique_string(visit_ids, visit.id)
    for worker_id in visit.preferred_worker_ids {
      if !string_array_contains(worker_ids, worker_id) {
        issues.push(
          warning_diagnostic(
            "request.unknown_preferred_worker", "preferred worker does not exist",
          )
          .for_entity(visit.id)
          .at_field(worker_id),
        )
      }
    }
    for worker_id in visit.forbidden_worker_ids {
      if !string_array_contains(worker_ids, worker_id) {
        issues.push(
          warning_diagnostic(
            "request.unknown_forbidden_worker", "forbidden worker does not exist",
          )
          .for_entity(visit.id)
          .at_field(worker_id),
        )
      }
    }
  }
  issues
}

///|
/// Validate a complete scheduling request before solving.
pub fn validate_request(request : ScheduleRequest) -> Array[Diagnostic] {
  let issues : Array[Diagnostic] = []
  if request.workers.length() == 0 {
    issues.push(
      error_diagnostic("request.no_workers", "request contains no workers"),
    )
  }
  if request.visits.length() == 0 {
    issues.push(
      warning_diagnostic("request.no_visits", "request contains no visits"),
    )
  }
  for worker in request.workers {
    issues.append(validate_worker(worker))
  }
  for visit in request.visits {
    issues.append(validate_visit(visit))
  }
  issues.append(validate_policy(request.policy))
  issues.append(validate_request_relations(request))
  issues
}