///|
/// Aggregate acceptance metrics for one schedule and request.
pub(all) struct ScheduleAnalytics {
  total_visits : Int
  required_visits : Int
  assigned_visits : Int
  assigned_required_visits : Int
  optional_unassigned : Int
  required_unassigned : Int
  coverage_percent : Int
  required_coverage_percent : Int
  total_service_minutes : Int
  total_travel_minutes : Int
  travel_share_percent : Int
  average_worker_utilization_percent : Int
  busiest_worker_id : String?
  busiest_worker_minutes : Int
  zones_served : Array[String]
  reason_counts : Array[ReasonCount]
} derive(Debug, Eq)

///|
/// Frequency of one stable unassigned reason code.
pub(all) struct ReasonCount {
  code : String
  count : Int
} derive(Debug, Eq)

///|
fn percentage(numerator : Int, denominator : Int) -> Int {
  if denominator <= 0 {
    0
  } else {
    clamp_int(numerator * 100 / denominator, 0, 100)
  }
}

///|
/// Count required visits in a request.
pub fn count_required_visits(visits : Array[Visit]) -> Int {
  let mut count = 0
  for visit in visits {
    if visit.required {
      count = count + 1
    }
  }
  count
}

///|
/// Count assigned visits that are required.
pub fn count_assigned_required(
  assignments : Array[Assignment],
  visits : Array[Visit],
) -> Int {
  let mut count = 0
  for assignment in assignments {
    match find_visit(visits, assignment.visit_id) {
      Some(visit) => if visit.required { count = count + 1 }
      None => ()
    }
  }
  count
}

///|
/// Count required and optional unassigned visits separately.
pub fn unassigned_breakdown(
  unassigned : Array[UnassignedVisit],
  visits : Array[Visit],
) -> (Int, Int) {
  let mut required = 0
  let mut optional = 0
  for missing in unassigned {
    match find_visit(visits, missing.visit_id) {
      Some(visit) =>
        if visit.required {
          required = required + 1
        } else {
          optional = optional + 1
        }
      None => required = required + 1
    }
  }
  (required, optional)
}

///|
/// Aggregate unassigned reason codes into stable sorted counters.
pub fn count_unassigned_reasons(
  unassigned : Array[UnassignedVisit],
) -> Array[ReasonCount] {
  let codes : Array[String] = []
  for missing in unassigned {
    for code in missing.reason_codes {
      push_unique_string(codes, code)
    }
  }
  let result : Array[ReasonCount] = []
  for code in sort_strings(codes) {
    let mut count = 0
    for missing in unassigned {
      if string_array_contains(missing.reason_codes, code) {
        count = count + 1
      }
    }
    result.push({ code, count })
  }
  result
}

///|
/// List service zones represented by assigned visits.
pub fn assigned_zones(
  assignments : Array[Assignment],
  visits : Array[Visit],
) -> Array[String] {
  let zones : Array[String] = []
  for assignment in assignments {
    match find_visit(visits, assignment.visit_id) {
      Some(visit) => push_unique_string(zones, visit.location.zone)
      None => ()
    }
  }
  sort_strings(zones)
}

///|
/// Determine the busiest worker by service minutes with stable id tie-break.
pub fn busiest_worker(loads : Array[WorkerLoad]) -> (String?, Int) {
  let mut best_id : String? = None
  let mut best_minutes = -1
  for load in loads {
    let tie_break = match best_id {
      Some(id) => load.worker_id < id
      None => true
    }
    let should_replace = load.service_minutes > best_minutes ||
      (load.service_minutes == best_minutes && tie_break)
    if should_replace {
      best_id = Some(load.worker_id)
      best_minutes = load.service_minutes
    }
  }
  if best_minutes < 0 {
    (None, 0)
  } else {
    (best_id, best_minutes)
  }
}

///|
/// Average worker utilization from existing normalized loads.
pub fn average_utilization(loads : Array[WorkerLoad]) -> Int {
  if loads.length() == 0 {
    return 0
  }
  let mut total = 0
  for load in loads {
    total = total + load.utilization_percent
  }
  total / loads.length()
}

///|
/// Compute transparent acceptance metrics from a schedule.
pub fn analyze_schedule(
  schedule : Schedule,
  request : ScheduleRequest,
) -> ScheduleAnalytics {
  let required_visits = count_required_visits(request.visits)
  let assigned_required = count_assigned_required(
    schedule.assignments,
    request.visits,
  )
  let (required_unassigned, optional_unassigned) = unassigned_breakdown(
    schedule.unassigned,
    request.visits,
  )
  let loads = all_worker_loads(request.workers, schedule.assignments)
  let mut service_minutes = 0
  for load in loads {
    service_minutes = service_minutes + load.service_minutes
  }
  let travel = total_schedule_travel(schedule.assignments)
  let (busiest_id, busiest_minutes) = busiest_worker(loads)
  {
    total_visits: request.visits.length(),
    required_visits,
    assigned_visits: schedule.assignments.length(),
    assigned_required_visits: assigned_required,
    optional_unassigned,
    required_unassigned,
    coverage_percent: percentage(
      schedule.assignments.length(),
      request.visits.length(),
    ),
    required_coverage_percent: percentage(assigned_required, required_visits),
    total_service_minutes: service_minutes,
    total_travel_minutes: travel,
    travel_share_percent: percentage(travel, service_minutes + travel),
    average_worker_utilization_percent: average_utilization(loads),
    busiest_worker_id: busiest_id,
    busiest_worker_minutes: busiest_minutes,
    zones_served: assigned_zones(schedule.assignments, request.visits),
    reason_counts: count_unassigned_reasons(schedule.unassigned),
  }
}

///|
/// Human-readable analytics for acceptance reports.
pub fn analytics_summary(value : ScheduleAnalytics) -> String {
  let builder = StringBuilder::new()
  builder.write_string(
    "coverage=\{value.coverage_percent}% (\{value.assigned_visits}/\{value.total_visits}), required_coverage=\{value.required_coverage_percent}% (\{value.assigned_required_visits}/\{value.required_visits})\n",
  )
  builder.write_string(
    "service_minutes=\{value.total_service_minutes}, travel_minutes=\{value.total_travel_minutes}, travel_share=\{value.travel_share_percent}%\n",
  )
  builder.write_string(
    "average_worker_utilization=\{value.average_worker_utilization_percent}%, zones=\{value.zones_served.join(", ")}\n",
  )
  match value.busiest_worker_id {
    Some(id) =>
      builder.write_string(
        "busiest_worker=\{id} (\{value.busiest_worker_minutes} service minutes)\n",
      )
    None => builder.write_string("busiest_worker=none\n")
  }
  if value.reason_counts.length() > 0 {
    builder.write_string("unassigned_reasons:\n")
    for item in value.reason_counts {
      builder.write_string("- \{item.code}: \{item.count}\n")
    }
  }
  builder.to_string()
}