///|
/// Reliability prior for one station in network association.
pub struct StationReliability {
  station : String
  detections : Int
  confirmations : Int
  false_alarms : Int
  mean_quality : Double
  reliability : Double
}

///|
/// A cluster of station events before final catalog insertion.
pub struct EventCluster {
  member_indexes : Array[Int]
  origin_time : Double
  magnitude : Double
  station_count : Int
  coherence : Double
}

///|
/// Network event consensus after reliability weighting.
pub struct NetworkConsensus {
  origin_time : Double
  magnitude : Double
  station_count : Int
  confidence : Double
  members : Array[NetworkEvent]
  supporting_stations : Array[String]
  rejected_members : Int
}

///|
/// Controls temporal clustering and reliability thresholds.
pub struct NetworkConsensusConfig {
  time_tolerance : Double
  minimum_stations : Int
  minimum_confidence : Double
  false_alarm_penalty : Double
}

///|
/// Return conservative consensus defaults.
pub fn default_network_consensus_config() -> NetworkConsensusConfig {
  {
    time_tolerance: 3.0,
    minimum_stations: 2,
    minimum_confidence: 0.45,
    false_alarm_penalty: 0.25,
  }
}

///|
/// Compute station reliability from detection outcomes.
pub fn station_reliability(
  station : String,
  detections : Int,
  confirmations : Int,
  false_alarms : Int,
  mean_quality : Double,
) -> StationReliability {
  let total = detections.max(0).to_double()
  let precision = if total <= 0.0 {
    0.0
  } else {
    (confirmations.max(0).to_double() / total).min(1.0)
  }
  let penalty = if total <= 0.0 {
    0.0
  } else {
    false_alarms.max(0).to_double() / total
  }
  let reliability = (0.55 * precision +
    0.45 * mean_quality.max(0.0).min(1.0) -
    penalty * 0.35)
    .max(0.0)
    .min(1.0)
  {
    station,
    detections,
    confirmations,
    false_alarms,
    mean_quality,
    reliability,
  }
}

///|
/// Find temporal event clusters with single-linkage grouping.
pub fn cluster_network_events(
  events : Array[NetworkEvent],
  tolerance_seconds : Double,
) -> Array[EventCluster] {
  let result : Array[EventCluster] = Array::new()
  let used = Array::make(events.length(), false)
  let tolerance = tolerance_seconds.abs()
  for seed = 0; seed < events.length(); seed = seed + 1 {
    if used[seed] {
      continue
    }
    let members = Array::new()
    let stations = Array::new()
    let mut total_time = 0.0
    let mut total_magnitude = 0.0
    let mut total_confidence = 0.0
    let anchor = events[seed].origin_time
    for index = seed; index < events.length(); index = index + 1 {
      if !used[index] && (events[index].origin_time - anchor).abs() <= tolerance {
        used[index] = true
        members.push(index)
        total_time += events[index].origin_time * events[index].confidence
        total_magnitude += events[index].magnitude * events[index].confidence
        total_confidence += events[index].confidence
        for station in events[index].stations {
          if !stations.contains(station) {
            stations.push(station)
          }
        }
      }
    }
    result.push({
      member_indexes: members,
      origin_time: if total_confidence <= 0.0 {
        anchor
      } else {
        total_time / total_confidence
      },
      magnitude: if total_confidence <= 0.0 {
        0.0
      } else {
        total_magnitude / total_confidence
      },
      station_count: stations.length(),
      coherence: if events.is_empty() {
        0.0
      } else {
        members.length().to_double() / events.length().to_double()
      },
    })
  }
  result
}

///|
/// Merge one cluster into a reliability-weighted consensus event.
pub fn consensus_from_cluster(
  cluster : EventCluster,
  events : Array[NetworkEvent],
  reliabilities : Array[StationReliability],
  config : NetworkConsensusConfig,
) -> NetworkConsensus {
  let members : Array[NetworkEvent] = Array::new()
  let stations : Array[String] = Array::new()
  let mut time_total = 0.0
  let mut magnitude_total = 0.0
  let mut weight_total = 0.0
  let mut rejected = 0
  for index in cluster.member_indexes {
    if index < 0 || index >= events.length() {
      rejected += 1
      continue
    }
    let event = events[index]
    let mut station_weight = 0.0
    for station in event.stations {
      let mut found = 0.75
      for reliability in reliabilities {
        if reliability.station == station {
          found = reliability.reliability
        }
      }
      station_weight += found
      if !stations.contains(station) {
        stations.push(station)
      }
    }
    let weight = station_weight.max(event.confidence).max(1.0e-6)
    if event.confidence >= config.minimum_confidence {
      members.push(event)
      time_total += event.origin_time * weight
      magnitude_total += event.magnitude * weight
      weight_total += weight
    } else {
      rejected += 1
    }
  }
  let origin_time = if weight_total <= 0.0 {
    cluster.origin_time
  } else {
    time_total / weight_total
  }
  let magnitude = if weight_total <= 0.0 {
    cluster.magnitude
  } else {
    magnitude_total / weight_total
  }
  let confidence = (cluster.coherence *
  (stations.length().to_double() / config.minimum_stations.max(1).to_double()).min(
    1.0,
  ) *
  (weight_total / members.length().max(1).to_double()).min(1.0)).min(1.0)
  {
    origin_time,
    magnitude,
    station_count: stations.length(),
    confidence,
    members,
    supporting_stations: stations,
    rejected_members: rejected,
  }
}

///|
/// Select the strongest consensus from all temporal clusters.
pub fn strongest_consensus(
  events : Array[NetworkEvent],
  reliabilities : Array[StationReliability],
  config : NetworkConsensusConfig,
) -> NetworkConsensus? {
  let clusters = cluster_network_events(events, config.time_tolerance)
  let mut best : NetworkConsensus? = None
  for cluster in clusters {
    if cluster.station_count < config.minimum_stations {
      continue
    }
    let candidate = consensus_from_cluster(
      cluster, events, reliabilities, config,
    )
    match best {
      None => best = Some(candidate)
      Some(current) =>
        if candidate.confidence > current.confidence {
          best = Some(candidate)
        }
    }
  }
  best
}

///|
/// Return the stability of a consensus origin-time estimate.
pub fn consensus_time_spread(consensus : NetworkConsensus) -> Double {
  if consensus.members.is_empty() {
    0.0
  } else {
    standard_deviation(consensus.members.map(event => event.origin_time))
  }
}

///|
/// Return a human-readable consensus quality band.
pub fn consensus_quality(consensus : NetworkConsensus) -> String {
  if consensus.confidence >= 0.8 {
    "high"
  } else if consensus.confidence >= 0.55 {
    "medium"
  } else {
    "low"
  }
}

///|
/// Serialize network consensus information.
pub fn network_consensus_text(consensus : NetworkConsensus) -> String {
  "origin=" +
  consensus.origin_time.to_string() +
  " magnitude=" +
  consensus.magnitude.to_string() +
  " stations=" +
  consensus.station_count.to_string() +
  " confidence=" +
  consensus.confidence.to_string() +
  " spread=" +
  consensus_time_spread(consensus).to_string() +
  " rejected=" +
  consensus.rejected_members.to_string() +
  " quality=" +
  consensus_quality(consensus)
}