#!/usr/bin/env ruby
require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "base64"
  gem "bcrypt_pbkdf"
  gem "ed25519"
  gem "sshkit"
end

require "optparse"
require "net/http"
require "uri"

class Load
  include SSHKit::DSL

  attr_reader :users, :setup, :host, :port, :ssh_user
  attr_reader :campfire_image

  def initialize(users: 1000, setup: true, host: "localhost", port: nil, ssh_user: "ubuntu")
    @users = users
    @setup = setup
    @host = host
    @ssh_user = ssh_user
    @port = port || (remote? ? 80 : 3000)
    @campfire_image = "localhost/campfire:latest"
  end

  def setup?
    @setup
  end

  def run
    configure_sshkit
    run_setup if setup?
    start_app
    run_test
    output_results
    cleanup
  end

  def remote?
    ![ "localhost", "127.0.0.1" ].include?(host)
  end

  def ssh_host
    remote? ? host : :local
  end

  def k6_dir
    remote? ? "$PWD" : "$PWD/test/performance"
  end

  private
    def configure_sshkit
      SSHKit::Backend::Netssh.configure do |ssh|
        ssh.connection_timeout = 30
        ssh.ssh_options = { user: ssh_user }
      end
    end

    def run_setup
      puts "Running setup..."
      load = self
      os, arch = host_os_and_arch

      on(:local) do
        execute :docker, :build, "-t", load.campfire_image, "--platform", arch, "."
        if load.remote?
          execute :docker, :save, "-o", "tmp/campfire-image.tar", load.campfire_image
        end
      end

      if remote?
        on(ssh_host) do
          unless execute(:docker, "-v", raise_on_non_zero_exit: false)
            execute :curl, "-fsSL", "https://get.docker.com", "|", :sh
          end
          upload! "tmp/campfire-image.tar", "campfire-image.tar"
          upload! "test/performance/chatter.js", "chatter.js"
          upload! "test/performance/cookies.txt", "cookies.txt"
          execute :docker, :load, "-i", "campfire-image.tar"
        end
      end
    end

    def host_os_and_arch
      os = arch = nil
      on(ssh_host) do
        os = capture("uname -s").strip
        arch = capture("uname -m").strip == "x86_64" ? "linux/amd64" : "linux/arm64"
      end
      [os, arch]
    end

    def start_app
      puts "Starting app on #{host}..."
      load = self

      on(ssh_host) do
        execute :docker, :rm, "-f", :campfire_load, raise_on_non_zero_exit: false
        execute :docker, :run, "-d", "--rm", "--name", :campfire_load, "-p", "#{load.port}:80", "-e", "SECRET_KEY_BASE=dummy", "-e RAILS_ENV=performance", load.campfire_image
      end

      wait_for_app_to_start
    end

    def wait_for_app_to_start
      timeout_at = Time.now + 60
      uri = URI("http://#{host}:#{port}")
      loop do
        sleep 1
        puts "Waiting for app to start..."
        raise "App not started after 60 seconds" if Time.now > timeout_at
        break if Net::HTTP.get_response(uri).code.to_i < 400
        sleep 1
      rescue Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError
      end
    end

    def run_test
      puts "Running load test..."
      load = self

      on(ssh_host) do |host|
        execute :docker, :rm, "-f", :k6, raise_on_non_zero_exit: false
        execute :docker, :run, "--name", :k6, "-u \"$(id -u):$(id -g)\"", "-e HOST=#{load.host}", "-e PORT=#{load.port}", "-e USERS=#{load.users}", "-v", "#{load.k6_dir}:/src", "grafana/k6", :run, "/src/chatter.js"
        if host.local?
          execute :docker, :logs, :k6, "2>", "tmp/k6.log"
        else
          execute :docker, :logs, :k6, "2>", "k6.log"
          download! "k6.log", "tmp/k6.log"
        end
      end
    end

    def output_results
      puts "Outputting results..."
      on(:local) do
        puts "Connections/s"
        puts "-------------"
        puts capture(:cat, "tmp/k6.log", "|", "grep 'Subscription confirmed' | awk '{print $1}' | sort| uniq -c | sed '1d; $d' | awk '{sum += $1; count += 1; max = (max > $1) ? max : $1} END {print \"Average:\",sum/count/6,\"Max:\",max/6}'")
        puts
        puts "Messages/s"
        puts "--------------"
        puts capture(:cat, "tmp/k6.log", "|", "grep 'Message received' | awk '{print $1}' | sort | uniq -c | sed '1d; $d' | awk '{sum += $1; count += 1; max = (max > $1) ? max : $1} END {print \"Average:\",sum/count,\"Max:\",max}'")
      end
    end

    def cleanup
      on(ssh_host) do |host|
        execute :docker, :rm, "-f", :campfire_load, :k6
      end
    end
end

options = {}

OptionParser.new do |opts|
  opts.banner = "Usage: #{$0} [options]"

  opts.on("-u", "--users USERS", "User count") do |value|
    options[:users] = value&.to_i
  end

  opts.on("-S", "--skip-setup", "Skip setup") do
    options[:setup] = false
  end

  opts.on("-h", "--host host", "Host to run the app on") do |host|
    options[:host] = host
  end

  opts.on("--ssh-user ssh_user", "User to ssh to remote hosts") do |ssh_user|
    options[:ssh_user] = ssh_user
  end

  opts.on("-p", "--port PORT", "Application port ") do |port|
    options[:port] = port
  end

  opts.on("--help", "Prints this message") do
    puts opts
    exit 0
  end
end.parse!

Load.new(**options).run
