#!/usr/bin/env ruby
require "json"
require "csv"

BIN_MS = 10_000
SLO_P95_MS = 500

class Run
  attr_reader :label, :dir

  def initialize(label)
    @label = label
    @dir = File.expand_path("../tmp/bench/#{label}", __dir__)
    abort "No results in tmp/bench/#{label}" unless File.exist?(File.join(dir, "k6.log"))
  end

  def report
    write_series
    print_summary
  end

  def series
    @series ||= build_series
  end

  def meta
    @meta ||= JSON.parse(File.read(File.join(dir, "meta.json")))
  end

  def summary
    @summary ||= JSON.parse(File.read(File.join(dir, "summary.json")))
  rescue Errno::ENOENT
    {}
  end

  def capacity
    breach = first_sustained_breach
    if breach
      breach[:connected]
    else
      nil
    end
  end

  private
    def build_series
      events = parse_k6_log
      stats = parse_stats
      first_ts = events.map { |e| e[:ts] }.min
      return [] unless first_ts

      bins = Hash.new { |h, k| h[k] = { lats: [], conns: 0, sents: 0, errs: 0, cpu: [], mem: [] } }
      events.each do |event|
        bin = (event[:ts] - first_ts) / BIN_MS
        case event[:kind]
        when "CONN" then bins[bin][:conns] += 1
        when "LAT"  then bins[bin][:lats] << event[:value]
        when "SENT" then bins[bin][:sents] += 1
        when "ERR"  then bins[bin][:errs] += 1
        end
      end
      stats.each do |stat|
        bin = (stat[:ts] - first_ts) / BIN_MS
        next if bin < 0
        bins[bin][:cpu] << stat[:cpu]
        bins[bin][:mem] << stat[:mem]
      end

      connected = 0
      (0..bins.keys.max).map do |bin|
        data = bins[bin]
        connected = [ connected + data[:conns], meta["users"] ].min
        {
          elapsed_s: bin * BIN_MS / 1000,
          connected: connected,
          sent: data[:sents],
          samples: data[:lats].size,
          lat_p50: percentile(data[:lats], 50),
          lat_p95: percentile(data[:lats], 95),
          lat_max: data[:lats].max,
          errors: data[:errs],
          cpu_perc: average(data[:cpu]),
          mem_mb: average(data[:mem])&.then { |bytes| (bytes / 1024.0 / 1024.0).round(1) }
        }
      end
    end

    def parse_k6_log
      events = []
      File.foreach(File.join(dir, "k6.log")) do |line|
        if line =~ /BENCH (CONN|LAT|SENT|ERR) (\d+)(?: (\S+))?/
          events << { kind: $1, ts: $2.to_i, value: $3.to_i }
        end
      end
      events
    end

    def parse_stats
      path = File.join(dir, "stats.csv")
      return [] unless File.exist?(path)
      CSV.read(path, headers: true).map do |row|
        { ts: row["epoch_ms"].to_i, cpu: row["cpu_perc"].to_f, mem: row["mem_bytes"].to_i }
      end
    end

    def percentile(values, pct)
      return nil if values.empty?
      sorted = values.sort
      sorted[((pct / 100.0) * (sorted.size - 1)).round]
    end

    def average(values)
      return nil if values.empty?
      (values.sum / values.size.to_f).round(1)
    end

    def write_series
      CSV.open(File.join(dir, "series.csv"), "w") do |csv|
        csv << series.first.keys
        series.each { |row| csv << row.values }
      end
    end

    def first_sustained_breach
      series.each_cons(2) do |a, b|
        if breached?(a) && breached?(b)
          return a
        end
      end
      nil
    end

    def breached?(bin)
      if bin[:samples] > 0 && bin[:lat_p95] && bin[:lat_p95] > SLO_P95_MS
        true
      else
        bin[:errors] > 0
      end
    end

    def print_summary
      latency = summary.dig("metrics", "delivery_latency") || {}
      puts
      puts "== #{label} (#{meta['cpus']} CPU, #{meta['memory']} RAM, ramp to #{meta['users']}) =="
      puts "  delivery latency: p50=#{latency['med']&.round(1)}ms p95=#{latency['p(95)']&.round(1)}ms max=#{latency['max']&.round(1)}ms"
      puts "  messages received: #{summary.dig('metrics', 'bench_messages_received', 'count')}"
      puts "  socket errors: #{summary.dig('metrics', 'bench_socket_errors', 'count') || 0}"
      peak = series.map { |bin| bin[:mem_mb] }.compact.max
      puts "  peak container memory: #{peak} MB"
      if capacity
        puts "  capacity at SLO (p95 <= #{SLO_P95_MS}ms, no errors): ~#{capacity} concurrent users"
      else
        puts "  capacity at SLO: not reached (>= #{series.last[:connected]} concurrent users)"
      end
      puts "  series: tmp/bench/#{label}/series.csv"
    end
end

abort "Usage: bin/bench-report LABEL [LABEL...]" if ARGV.empty?
ARGV.each { |label| Run.new(label).report }
