///|
/// Configuration for a multi-characteristic arrival picker.
pub struct AdvancedPickerConfig {
  amplitude_threshold : Double
  energy_threshold : Double
  kurtosis_threshold : Double
  aic_window : Int
  characteristic_window : Int
  agreement_seconds : Double
  minimum_confidence : Double
}

///|
/// A characteristic function summary used for picker diagnostics.
pub struct PickerCharacteristicSummary {
  name : String
  peak_index : Int
  peak_time : Double
  peak_value : Double
  baseline : Double
  contrast : Double
  confidence : Double
}

///|
/// A consensus arrival assembled from multiple independent methods.
pub struct ConsensusPick {
  index : Int
  time : Double
  method_count : Int
  confidence : Double
  amplitude_pick : ArrivalPick?
  energy_pick : ArrivalPick?
  aic_pick : ArrivalPick?
  kurtosis_pick : ArrivalPick?
}

///|
/// Picker diagnostics for a trace.
pub struct PickerDiagnostics {
  trace_station : String
  sample_count : Int
  amplitude : PickerCharacteristicSummary
  energy : PickerCharacteristicSummary
  aic : PickerCharacteristicSummary
  kurtosis : PickerCharacteristicSummary
  consensus : ConsensusPick?
}

///|
/// Return a conservative multi-method picker configuration.
pub fn default_advanced_picker_config() -> AdvancedPickerConfig {
  {
    amplitude_threshold: 3.0,
    energy_threshold: 1.5,
    kurtosis_threshold: 2.0,
    aic_window: 8,
    characteristic_window: 16,
    agreement_seconds: 0.5,
    minimum_confidence: 0.45,
  }
}

///|
/// Find the peak and baseline contrast of a characteristic function.
pub fn summarize_picker_characteristic(
  name : String,
  characteristic : Array[Double],
  trace : Trace,
  baseline_samples : Int,
) -> PickerCharacteristicSummary {
  if characteristic.is_empty() {
    return {
      name,
      peak_index: -1,
      peak_time: trace.start_time,
      peak_value: 0.0,
      baseline: 0.0,
      contrast: 0.0,
      confidence: 0.0,
    }
  }
  let baseline = mean_samples(
    slice_samples(
      characteristic,
      0,
      baseline_samples.max(1).min(characteristic.length()),
    ),
  )
  let mut peak_index = 0
  for index = 1; index < characteristic.length(); index = index + 1 {
    if characteristic[index] > characteristic[peak_index] {
      peak_index = index
    }
  }
  let peak_value = characteristic[peak_index]
  let contrast = peak_value - baseline
  let confidence = if peak_value.abs() <= 1.0e-12 {
    0.0
  } else {
    (contrast / peak_value.abs()).max(0.0).min(1.0)
  }
  {
    name,
    peak_index,
    peak_time: sample_to_time(trace, peak_index),
    peak_value,
    baseline,
    contrast,
    confidence,
  }
}

///|
/// Calculate normalized characteristic functions for the primary methods.
pub fn picker_characteristics(
  trace : Trace,
  config : AdvancedPickerConfig,
) -> Array[Array[Double]] {
  let amplitude = amplitude_envelope(trace.data)
  let energy = energy_ratio(
    trace.data,
    config.characteristic_window.max(1),
    config.characteristic_window.max(1) * 4,
  )
  let kurtosis = running_kurtosis(trace.data, config.characteristic_window)
  let aic = aic_characteristic(trace.data, config.aic_window)
  [amplitude, energy, aic, kurtosis]
}

///|
/// Pick the strongest characteristic after applying a quiet pre-event period.
pub fn pick_from_characteristic(
  trace : Trace,
  characteristic : Array[Double],
  threshold : Double,
  strategy : PickMethod,
  quiet_samples : Int,
) -> ArrivalPick? {
  if characteristic.is_empty() {
    return None
  }
  let start = quiet_samples.max(0).min(characteristic.length())
  let suffix = slice_samples(characteristic, start, characteristic.length())
  match
    pick_characteristic(
      suffix,
      trace_slice(trace, start, trace.npts()),
      threshold,
      strategy,
    ) {
    None => None
    Some(pick) =>
      Some({
        index: pick.index + start,
        time: sample_to_time(trace, pick.index + start),
        strategy: pick.strategy,
        confidence: pick.confidence,
        characteristic_value: pick.characteristic_value,
      })
  }
}

///|
/// Combine picks that fall inside an agreement window.
pub fn consensus_from_picks(
  trace : Trace,
  picks : Array[ArrivalPick],
  agreement_seconds : Double,
  minimum_confidence : Double,
) -> ConsensusPick? {
  let candidates = picks.filter(pick => pick.confidence >= minimum_confidence)
  if candidates.is_empty() {
    return None
  }
  let tolerance = agreement_seconds.abs()
  let mut best_index = 0
  let mut best_count = 0
  let mut best_confidence = 0.0
  for seed = 0; seed < candidates.length(); seed = seed + 1 {
    let mut count = 0
    let mut total_confidence = 0.0
    for pick in candidates {
      if (pick.time - candidates[seed].time).abs() <= tolerance {
        count += 1
        total_confidence += pick.confidence
      }
    }
    if count > best_count ||
      (count == best_count && total_confidence > best_confidence) {
      best_index = seed
      best_count = count
      best_confidence = total_confidence
    }
  }
  let selected = candidates[best_index]
  if best_count == 0 {
    None
  } else {
    let mut index_total = 0.0
    for pick in candidates {
      if (pick.time - selected.time).abs() <= tolerance {
        index_total += pick.index.to_double() * pick.confidence
      }
    }
    let index = (index_total / best_confidence.max(1.0e-12))
      .round()
      .to_int()
      .max(0)
      .min(trace.npts().max(1) - 1)
    Some({
      index,
      time: sample_to_time(trace, index),
      method_count: best_count,
      confidence: (best_confidence / candidates.length().to_double()).min(1.0),
      amplitude_pick: find_consensus_pick(
        picks,
        selected,
        tolerance,
        AmplitudeThreshold,
      ),
      energy_pick: find_consensus_pick(picks, selected, tolerance, EnergyRatio),
      aic_pick: find_consensus_pick(
        picks,
        selected,
        tolerance,
        AkaikeInformationCriterion,
      ),
      kurtosis_pick: find_consensus_pick(
        picks,
        selected,
        tolerance,
        KurtosisChange,
      ),
    })
  }
}

///|
/// Find one pick of a requested method in an agreement cluster.
fn find_consensus_pick(
  picks : Array[ArrivalPick],
  selected : ArrivalPick,
  tolerance : Double,
  strategy : PickMethod,
) -> ArrivalPick? {
  for pick in picks {
    if same_pick_strategy(pick.strategy, strategy) &&
      (pick.time - selected.time).abs() <= tolerance {
      return Some(pick)
    }
  }
  None
}

///|
/// Compare picker strategy constructors without requiring enum equality.
fn same_pick_strategy(left : PickMethod, right : PickMethod) -> Bool {
  match (left, right) {
    (AmplitudeThreshold, AmplitudeThreshold) => true
    (EnergyRatio, EnergyRatio) => true
    (AkaikeInformationCriterion, AkaikeInformationCriterion) => true
    (KurtosisChange, KurtosisChange) => true
    _ => false
  }
}

///|
/// Run all four methods and construct a consensus pick.
pub fn advanced_pick(
  trace : Trace,
  config : AdvancedPickerConfig,
) -> ConsensusPick? {
  let amplitude = pick_from_characteristic(
    trace,
    amplitude_envelope(trace.data),
    config.amplitude_threshold,
    AmplitudeThreshold,
    config.characteristic_window,
  )
  let energy = pick_from_characteristic(
    trace,
    energy_ratio(
      trace.data,
      config.characteristic_window.max(1),
      config.characteristic_window.max(1) * 4,
    ),
    config.energy_threshold,
    EnergyRatio,
    config.characteristic_window,
  )
  let aic = pick_from_characteristic(
    trace,
    aic_characteristic(trace.data, config.aic_window),
    config.aic_window.to_double(),
    AkaikeInformationCriterion,
    config.characteristic_window,
  )
  let kurtosis = pick_from_characteristic(
    trace,
    running_kurtosis(trace.data, config.characteristic_window),
    config.kurtosis_threshold,
    KurtosisChange,
    config.characteristic_window,
  )
  let picks = Array::new()
  match amplitude {
    Some(value) => picks.push(value)
    None => ()
  }
  match energy {
    Some(value) => picks.push(value)
    None => ()
  }
  match aic {
    Some(value) => picks.push(value)
    None => ()
  }
  match kurtosis {
    Some(value) => picks.push(value)
    None => ()
  }
  consensus_from_picks(
    trace,
    picks,
    config.agreement_seconds,
    config.minimum_confidence,
  )
}

///|
/// Build a complete picker diagnostic record.
pub fn picker_diagnostics(
  trace : Trace,
  config : AdvancedPickerConfig,
) -> PickerDiagnostics {
  let characteristics = picker_characteristics(trace, config)
  let amplitude = summarize_picker_characteristic(
    "amplitude",
    characteristics[0],
    trace,
    config.characteristic_window,
  )
  let energy = summarize_picker_characteristic(
    "energy",
    characteristics[1],
    trace,
    config.characteristic_window,
  )
  let aic = summarize_picker_characteristic(
    "aic",
    characteristics[2].map(value => -value),
    trace,
    config.characteristic_window,
  )
  let kurtosis = summarize_picker_characteristic(
    "kurtosis",
    characteristics[3],
    trace,
    config.characteristic_window,
  )
  {
    trace_station: trace.station,
    sample_count: trace.npts(),
    amplitude,
    energy,
    aic,
    kurtosis,
    consensus: advanced_pick(trace, config),
  }
}

///|
/// Return whether a consensus pick has sufficient independent evidence.
pub fn consensus_is_reliable(
  pick : ConsensusPick,
  minimum_methods : Int,
) -> Bool {
  pick.method_count >= minimum_methods.max(1) && pick.confidence > 0.0
}

///|
/// Return pick latency relative to a trace start.
pub fn consensus_latency(trace : Trace, pick : ConsensusPick) -> Double {
  pick.time - trace.start_time
}

///|
/// Format a consensus pick for an event table.
pub fn consensus_pick_text(pick : ConsensusPick) -> String {
  "index=" +
  pick.index.to_string() +
  " time=" +
  pick.time.to_string() +
  " methods=" +
  pick.method_count.to_string() +
  " confidence=" +
  pick.confidence.to_string()
}

///|
/// Format picker diagnostics for JSON-like logs.
pub fn picker_diagnostics_text(diagnostics : PickerDiagnostics) -> String {
  let consensus = match diagnostics.consensus {
    Some(pick) => consensus_pick_text(pick)
    None => "none"
  }
  "station=" +
  diagnostics.trace_station +
  " samples=" +
  diagnostics.sample_count.to_string() +
  " amplitude_confidence=" +
  diagnostics.amplitude.confidence.to_string() +
  " energy_confidence=" +
  diagnostics.energy.confidence.to_string() +
  " aic_confidence=" +
  diagnostics.aic.confidence.to_string() +
  " kurtosis_confidence=" +
  diagnostics.kurtosis.confidence.to_string() +
  " consensus=" +
  consensus
}