///|
/// Ambulatory segmentation and day-level summaries for wearable streams.

///|
/// Coarse activity state inferred from heart rate and movement.
pub(all) enum ActivityState {
  Rest
  LightActivity
  ModerateActivity
  VigorousActivity
  UnknownActivity
} derive(FromJson, ToJson, Debug, Eq)

///|
/// One synchronized wearable observation.
pub(all) struct AmbulatorySample {
  timestamp_seconds : Double
  rr_ms : Double
  heart_rate_bpm : Double
  movement : Double
  temperature_c : Double
  quality_ratio : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// A contiguous activity block.
pub(all) struct ActivityBlock {
  start_index : Int
  end_index : Int
  state : ActivityState
  duration_seconds : Double
  sample_count : Int
  mean_hr : Double
  mean_rr : Double
  rmssd : Double
  quality_ratio : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Day-level ambulatory summary.
pub(all) struct AmbulatorySummary {
  sample_count : Int
  duration_seconds : Double
  rest_seconds : Double
  light_seconds : Double
  moderate_seconds : Double
  vigorous_seconds : Double
  mean_hr : Double
  mean_rr : Double
  valid_ratio : Double
  block_count : Int
  dominant_state : ActivityState
  hr_load : Double
  temperature_mean : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Default heart-rate and movement thresholds.
pub(all) struct ActivityThresholds {
  resting_hr_upper : Double
  light_hr_upper : Double
  moderate_hr_upper : Double
  resting_movement_upper : Double
  unknown_quality_floor : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Conservative thresholds for mixed wearable data.
pub fn ActivityThresholds::default() -> ActivityThresholds {
  {
    resting_hr_upper: 75.0,
    light_hr_upper: 100.0,
    moderate_hr_upper: 140.0,
    resting_movement_upper: 0.12,
    unknown_quality_floor: 0.50,
  }
}

///|
/// Classify a synchronized sample.
pub fn classify_activity(
  sample : AmbulatorySample,
  thresholds : ActivityThresholds,
) -> ActivityState {
  if sample.quality_ratio < thresholds.unknown_quality_floor ||
    sample.heart_rate_bpm <= 0.0 {
    return UnknownActivity
  }
  if sample.heart_rate_bpm <= thresholds.resting_hr_upper &&
    sample.movement <= thresholds.resting_movement_upper {
    Rest
  } else if sample.heart_rate_bpm <= thresholds.light_hr_upper {
    LightActivity
  } else if sample.heart_rate_bpm <= thresholds.moderate_hr_upper {
    ModerateActivity
  } else {
    VigorousActivity
  }
}

///|
/// Return a numeric load weight for an activity state.
pub fn activity_state_weight(state : ActivityState) -> Double {
  match state {
    Rest => 0.0
    LightActivity => 1.0
    ModerateActivity => 2.0
    VigorousActivity => 3.0
    UnknownActivity => 0.0
  }
}

///|
/// Return the duration between adjacent samples with a safe fallback.
pub fn sample_delta_seconds(
  left : AmbulatorySample,
  right : AmbulatorySample,
) -> Double {
  let delta = right.timestamp_seconds - left.timestamp_seconds
  if delta <= 0.0 || delta > 300.0 {
    0.0
  } else {
    delta
  }
}

///|
/// Find the contiguous block containing an index.
pub fn block_for_index(
  blocks : Array[ActivityBlock],
  index : Int,
) -> ActivityBlock? {
  for block in blocks {
    if index >= block.start_index && index < block.end_index {
      return Some(block)
    }
  }
  None
}

///|
fn build_activity_block(
  samples : Array[AmbulatorySample],
  states : Array[ActivityState],
  start : Int,
  end : Int,
) -> ActivityBlock {
  let rr = []
  let hr = []
  let quality = []
  let mut duration = 0.0
  for i in start.. Array[ActivityBlock] {
  if samples.length() == 0 {
    return []
  }
  let states = []
  for sample in samples {
    states.push(classify_activity(sample, thresholds))
  }
  let blocks = []
  let mut start = 0
  let minimum = if minimum_block_samples < 1 {
    1
  } else {
    minimum_block_samples
  }
  let mut i = 1
  while i <= samples.length() {
    let state_changed = i == samples.length() || states[i] != states[start]
    if state_changed {
      if i - start >= minimum || blocks.length() == 0 {
        blocks.push(build_activity_block(samples, states, start, i))
      } else {
        let previous = blocks[blocks.length() - 1]
        blocks[blocks.length() - 1] = build_activity_block(
          samples,
          states,
          previous.start_index,
          i,
        )
      }
      start = i
    }
    i += 1
  }
  blocks
}

///|
/// Aggregate a synchronized sample day.
pub fn summarize_ambulatory(
  samples : Array[AmbulatorySample],
  thresholds : ActivityThresholds,
) -> AmbulatorySummary {
  let blocks = segment_activity(samples, thresholds, 3)
  let mut rest = 0.0
  let mut light = 0.0
  let mut moderate = 0.0
  let mut vigorous = 0.0
  let mut hr_load = 0.0
  let mut total_temperature = 0.0
  let rr = []
  let hr = []
  let quality = []
  for sample in samples {
    rr.push(sample.rr_ms)
    hr.push(sample.heart_rate_bpm)
    quality.push(sample.quality_ratio)
    total_temperature += sample.temperature_c
  }
  for block in blocks {
    match block.state {
      Rest => rest += block.duration_seconds
      LightActivity => light += block.duration_seconds
      ModerateActivity => moderate += block.duration_seconds
      VigorousActivity => vigorous += block.duration_seconds
      UnknownActivity => ()
    }
    hr_load += activity_state_weight(block.state) *
      block.duration_seconds /
      60.0
  }
  let dominant = dominant_activity_state(blocks)
  {
    sample_count: samples.length(),
    duration_seconds: rest + light + moderate + vigorous,
    rest_seconds: rest,
    light_seconds: light,
    moderate_seconds: moderate,
    vigorous_seconds: vigorous,
    mean_hr: mean_value(hr),
    mean_rr: mean_value(rr),
    valid_ratio: mean_value(quality),
    block_count: blocks.length(),
    dominant_state: dominant,
    hr_load,
    temperature_mean: if samples.length() == 0 {
      0.0
    } else {
      total_temperature / samples.length().to_double()
    },
  }
}

///|
/// Choose the state with the largest duration.
pub fn dominant_activity_state(blocks : Array[ActivityBlock]) -> ActivityState {
  let mut best = UnknownActivity
  let mut best_duration = -1.0
  for block in blocks {
    if block.duration_seconds > best_duration {
      best_duration = block.duration_seconds
      best = block.state
    }
  }
  best
}

///|
/// Count samples belonging to a state.
pub fn count_activity_state(
  samples : Array[AmbulatorySample],
  state : ActivityState,
  thresholds : ActivityThresholds,
) -> Int {
  let mut count = 0
  for sample in samples {
    if classify_activity(sample, thresholds) == state {
      count += 1
    }
  }
  count
}

///|
/// Calculate a weighted activity load from blocks.
pub fn ambulatory_activity_load(blocks : Array[ActivityBlock]) -> Double {
  let mut total = 0.0
  for block in blocks {
    total += activity_state_weight(block.state) * block.duration_seconds / 60.0
  }
  total
}

///|
/// Estimate heart-rate recovery after a vigorous block.
pub fn heart_rate_recovery_after_activity(
  blocks : Array[ActivityBlock],
  recovery_minutes : Double,
) -> Double {
  if blocks.length() < 2 {
    return 0.0
  }
  let mut peak = 0.0
  let mut following = 0.0
  for i in 0.. peak {
      peak = blocks[i].mean_hr
      if i + 1 < blocks.length() {
        following = blocks[i + 1].mean_hr
      }
    }
  }
  if peak == 0.0 || following == 0.0 || recovery_minutes <= 0.0 {
    0.0
  } else {
    (peak - following) / recovery_minutes
  }
}

///|
/// Return a state-duration feature vector.
pub fn ambulatory_feature_vector(summary : AmbulatorySummary) -> Array[Double] {
  [
    summary.sample_count.to_double(),
    summary.duration_seconds,
    summary.rest_seconds,
    summary.light_seconds,
    summary.moderate_seconds,
    summary.vigorous_seconds,
    summary.mean_hr,
    summary.mean_rr,
    summary.valid_ratio,
    summary.block_count.to_double(),
    activity_state_weight(summary.dominant_state),
    summary.hr_load,
    summary.temperature_mean,
  ]
}

///|
/// Return whether an ambulatory summary has enough usable signal.
pub fn ambulatory_summary_is_usable(summary : AmbulatorySummary) -> Bool {
  summary.sample_count > 0 &&
  summary.duration_seconds > 0.0 &&
  summary.valid_ratio >= 0.5
}