///|
/// Operational effort estimate for a measurement plan.
///
/// The estimate is deliberately expressed in abstract seconds.  It is a
/// repeatable planning signal, not a promise about a particular machine.
pub(all) struct DurationEstimate {
  step_count : Int
  sample_count : Int
  seconds : Double
  setup_seconds : Double
  measurement_seconds : Double
  transfer_seconds : Double
}

///|
fn duration_sample_count(strategy : Strategy) -> Int {
  match strategy {
    SinglePoint => 1
    RepeatedPoint(count~) => if count > 0 { count } else { 0 }
    Scan(count~) => if count > 0 { count } else { 0 }
  }
}

///|
/// Return the measurement time assigned to one strategy.
pub fn strategy_duration_seconds(strategy : Strategy) -> Double {
  match strategy {
    SinglePoint => 1.0
    RepeatedPoint(count~) => 0.8 + count.to_double() * 1.4
    Scan(count~) => 1.5 + count.to_double() * 1.9
  }
}

///|
/// Return the setup cost of a step, including datum alignment.
pub fn step_setup_seconds(step : MeasurementStep) -> Double {
  if step.direction == "datum-aligned" {
    2.5
  } else {
    1.0
  }
}

///|
/// Return the transfer cost between two device IDs.
pub fn device_transfer_seconds(previous : String, next : String) -> Double {
  if previous == next {
    0.0
  } else {
    3.0
  }
}

///|
fn plan_transfer_seconds(steps : Array[MeasurementStep]) -> Double {
  if steps.length() < 2 {
    0.0
  } else {
    let mut result = 0.0
    for index in 1.. DurationEstimate {
  let mut sample_count = 0
  let mut setup_seconds = 0.0
  let mut measurement_seconds = 0.0
  for step in plan.steps {
    sample_count += duration_sample_count(step.strategy)
    setup_seconds += step_setup_seconds(step)
    measurement_seconds += strategy_duration_seconds(step.strategy)
  }
  let transfer_seconds = plan_transfer_seconds(plan.steps)
  {
    step_count: plan.steps.length(),
    sample_count,
    seconds: setup_seconds + measurement_seconds + transfer_seconds,
    setup_seconds,
    measurement_seconds,
    transfer_seconds,
  }
}

///|
/// Return a stable operational band for dashboards and reports.
pub fn duration_band(estimate : DurationEstimate) -> String {
  if estimate.seconds <= 0.0 {
    "empty"
  } else if estimate.seconds < 15.0 {
    "short"
  } else if estimate.seconds < 60.0 {
    "medium"
  } else {
    "long"
  }
}

///|
/// Return the estimated seconds per collected sample.
pub fn seconds_per_sample(estimate : DurationEstimate) -> Double {
  if estimate.sample_count <= 0 {
    0.0
  } else {
    estimate.seconds / estimate.sample_count.to_double()
  }
}

///|
/// Return the fraction of effort spent collecting measurements.
pub fn measurement_time_ratio(estimate : DurationEstimate) -> Double {
  if estimate.seconds <= 0.0 {
    0.0
  } else {
    estimate.measurement_seconds / estimate.seconds
  }
}

///|
/// Return the fraction of effort spent moving between devices.
pub fn transfer_time_ratio(estimate : DurationEstimate) -> Double {
  if estimate.seconds <= 0.0 {
    0.0
  } else {
    estimate.transfer_seconds / estimate.seconds
  }
}

///|
/// Return a utilization ratio against a positive available time window.
pub fn duration_utilization(
  estimate : DurationEstimate,
  available_seconds : Double,
) -> Double {
  if available_seconds <= 0.0 {
    0.0
  } else {
    estimate.seconds / available_seconds
  }
}

///|
/// Return whether the plan fits inside a positive time window.
pub fn duration_fits(
  estimate : DurationEstimate,
  available_seconds : Double,
) -> Bool {
  available_seconds >= estimate.seconds && available_seconds > 0.0
}

///|
/// Return the number of whole windows needed for this estimate.
pub fn required_time_windows(
  estimate : DurationEstimate,
  window_seconds : Double,
) -> Int {
  if estimate.seconds <= 0.0 || window_seconds <= 0.0 {
    0
  } else {
    let mut windows = 1
    let mut capacity = window_seconds
    while capacity < estimate.seconds {
      windows += 1
      capacity += window_seconds
    }
    windows
  }
}

///|
/// Return a compact deterministic duration label for CLI output.
pub fn render_duration(estimate : DurationEstimate) -> String {
  duration_band(estimate) +
  " steps=" +
  estimate.step_count.to_string() +
  " samples=" +
  estimate.sample_count.to_string() +
  " seconds=" +
  estimate.seconds.to_string()
}

///|
/// Return a conservative score where lower effort is better.
pub fn duration_efficiency(
  estimate : DurationEstimate,
  available_seconds : Double,
) -> Double {
  let utilization = duration_utilization(estimate, available_seconds)
  if utilization <= 0.0 {
    0.0
  } else if utilization >= 1.0 {
    1.0 / utilization
  } else {
    1.0 - utilization
  }
}

///|
/// Return the estimated cost of a single step, including its setup.
pub fn step_duration_seconds(step : MeasurementStep) -> Double {
  step_setup_seconds(step) + strategy_duration_seconds(step.strategy)
}

///|
/// Return the maximum individual step cost in a plan.
pub fn maximum_step_duration(plan : MeasurementPlan) -> Double {
  let mut result = 0.0
  for step in plan.steps {
    let seconds = step_duration_seconds(step)
    if seconds > result {
      result = seconds
    }
  }
  result
}

///|
/// Return the average step cost, or zero for an empty plan.
pub fn average_step_duration(plan : MeasurementPlan) -> Double {
  if plan.steps.length() == 0 {
    0.0
  } else {
    estimate_plan_duration(plan).seconds / plan.steps.length().to_double()
  }
}

///|
/// Return whether transfers dominate the operational estimate.
pub fn transfer_dominates(estimate : DurationEstimate) -> Bool {
  estimate.transfer_seconds > estimate.measurement_seconds
}

///|
/// Return whether the estimate has enough samples for a stable summary.
pub fn duration_has_replicates(estimate : DurationEstimate) -> Bool {
  estimate.sample_count > estimate.step_count
}