///|
pub(all) struct TopicStats {
  channel_id : Int
  topic : String
  message_count : Int
  payload_bytes : UInt64
  first_time : UInt64?
  last_time : UInt64?
  maximum_gap : UInt64
  median_gap : UInt64
  p95_gap : UInt64
  arrival_regressions : Int
  sequence_discontinuities : Int
  publish_after_log : Int
  frequency_hz : Double
}

///|
pub(all) struct LogStats {
  schema_count : Int
  channel_count : Int
  message_count : Int
  payload_bytes : UInt64
  topics : Array[TopicStats]
}

///|
pub fn summarize(log : Log) -> LogStats raise McapError {
  validate_log(log)
  let topics = []
  let mut payload_bytes = 0UL
  for channel in log.channels {
    let messages = log.messages.filter(fn(message) {
      message.channel_id == channel.id
    })
    let times : Array[UInt64] = []
    let mut bytes = 0UL
    let mut arrival_regressions = 0
    let mut sequence_discontinuities = 0
    let mut publish_after_log = 0
    let mut previous_time : UInt64? = None
    let mut previous_sequence : UInt? = None
    for message in messages {
      times.push(message.log_time)
      bytes += message.data.length().to_uint64()
      match previous_time {
        Some(previous) =>
          if message.log_time < previous {
            arrival_regressions += 1
          }
        None => ()
      }
      match previous_sequence {
        Some(previous) =>
          if previous != 0U &&
            message.sequence != 0U &&
            message.sequence != previous + 1U {
            sequence_discontinuities += 1
          }
        None => ()
      }
      if message.publish_time > message.log_time {
        publish_after_log += 1
      }
      previous_time = Some(message.log_time)
      previous_sequence = Some(message.sequence)
    }
    times.sort()
    let gaps : Array[UInt64] = []
    for i = 1; i < times.length(); i = i + 1 {
      gaps.push(times[i] - times[i - 1])
    }
    gaps.sort()
    let first_time = if times.is_empty() { None } else { Some(times[0]) }
    let last_time = if times.is_empty() {
      None
    } else {
      Some(times[times.length() - 1])
    }
    let maximum_gap = if gaps.is_empty() {
      0UL
    } else {
      gaps[gaps.length() - 1]
    }
    let median_gap = if gaps.is_empty() {
      0UL
    } else {
      gaps[(gaps.length() - 1) / 2]
    }
    let p95_gap = if gaps.is_empty() {
      0UL
    } else {
      gaps[(gaps.length() - 1) * 95 / 100]
    }
    let frequency_hz = if times.length() > 1 &&
      times[times.length() - 1] > times[0] {
      (times.length() - 1).to_double() *
      1000000000.0 /
      (times[times.length() - 1] - times[0]).to_double()
    } else {
      0.0
    }
    topics.push({
      channel_id: channel.id,
      topic: channel.topic,
      message_count: messages.length(),
      payload_bytes: bytes,
      first_time,
      last_time,
      maximum_gap,
      median_gap,
      p95_gap,
      arrival_regressions,
      sequence_discontinuities,
      publish_after_log,
      frequency_hz,
    })
    payload_bytes += bytes
  }
  {
    schema_count: log.schemas.length(),
    channel_count: log.channels.length(),
    message_count: log.messages.length(),
    payload_bytes,
    topics,
  }
}

///|
/// Human-readable report without converting timestamps to floating-point numbers.
pub fn stats_report(stats : LogStats) -> String {
  let out = StringBuilder::new()
  out.write_string("MoonMCAP log summary\n")
  out.write_string(
    "schemas=\{stats.schema_count} channels=\{stats.channel_count} messages=\{stats.message_count} payload_bytes=\{stats.payload_bytes}\n",
  )
  for topic in stats.topics {
    out.write_string(
      "topic=\{topic.topic} channel=\{topic.channel_id} count=\{topic.message_count} bytes=\{topic.payload_bytes}\n",
    )
    match (topic.first_time, topic.last_time) {
      (Some(first), Some(last)) =>
        out.write_string(
          "  time_ns=[\{first},\{last}] maximum_gap_ns=\{topic.maximum_gap}\n",
        )
      _ => out.write_string("  no messages\n")
    }
    out.write_string(
      "  arrival_regressions=\{topic.arrival_regressions} sequence_discontinuities=\{topic.sequence_discontinuities} publish_after_log=\{topic.publish_after_log}\n",
    )
  }
  out.to_string()
}