///|
/// A candidate device with a deterministic score and reason.
pub(all) struct DeviceCandidate {
  device_id : String
  device : Device
  score : Double
  reason : SelectionReason
}

///|
/// A human-readable explanation for one feature in a plan.
pub(all) struct PlanExplanation {
  feature_id : String
  device_id : String
  reason : SelectionReason
  message : String
}

///|
/// Aggregate quality score for a plan.
pub(all) struct QualityScore {
  coverage : Double
  capability : Bool
  warning_count : Int
  score : Double
}

///|
fn candidate_is_before(left : DeviceCandidate, right : DeviceCandidate) -> Bool {
  if left.score < right.score {
    true
  } else if left.score > right.score {
    false
  } else {
    left.device_id.compare(right.device_id) > 0
  }
}

///|
fn insert_candidate(
  candidates : Array[DeviceCandidate],
  candidate : DeviceCandidate,
) -> Array[DeviceCandidate] {
  let result = []
  let mut inserted = false
  for item in candidates {
    if !inserted && candidate_is_before(candidate, item) {
      result.push(candidate)
      inserted = true
    }
    result.push(item)
  }
  if !inserted {
    result.push(candidate)
  }
  result
}

///|
/// Rank reachable devices that support the requested feature kind.
pub fn rank_devices(
  feature~ : Feature,
  devices~ : Array[Device],
) -> Array[DeviceCandidate] {
  let result = []
  for device in devices {
    if device.can_measure(feature) {
      let score = device.accuracy +
        (device.reach - feature.nominal.abs()) * 0.0001
      let candidate = DeviceCandidate::{
        device_id: device.id,
        device,
        score,
        reason: SelectionReason::Supported,
      }
      let reordered = insert_candidate(result, candidate)
      result.clear()
      for item in reordered {
        result.push(item)
      }
    }
  }
  result
}

///|
/// Select the first deterministic candidate, if one exists.
pub fn choose_device(
  feature~ : Feature,
  devices~ : Array[Device],
) -> DeviceCandidate? {
  let candidates = rank_devices(feature~, devices~)
  candidates.get(0)
}

///|
/// Explain each planned feature and each plan-level warning.
pub fn explain_plan(plan : MeasurementPlan) -> Array[PlanExplanation] {
  let result = []
  for step in plan.steps {
    result.push({
      feature_id: step.feature_id,
      device_id: step.device_id,
      reason: SelectionReason::Supported,
      message: "selected " + step.device_id,
    })
  }
  if plan.steps.length() == 0 {
    for warning in plan.summary.warnings {
      result.push({
        feature_id: warning,
        device_id: "",
        reason: SelectionReason::NoCandidate,
        message: warning,
      })
    }
  }
  result
}

///|
/// Score coverage and capability while keeping warning count visible.
pub fn plan_quality(plan : MeasurementPlan) -> QualityScore {
  let warning_count = plan.summary.warnings.length()
  let multiplier = if plan.summary.capability { 1.0 } else { 0.5 }
  let score = plan.summary.coverage * multiplier
  {
    coverage: plan.summary.coverage,
    capability: plan.summary.capability,
    warning_count,
    score,
  }
}