#!/usr/bin/env ruby

require 'erb'
require 'fileutils'
require 'tmpdir'
require 'optparse'

File.umask(0022)

SOURCE_DIR = File.absolute_path(File.join(__dir__, '..', '..'))
RUBY_VERSION_FILE = File.join('gitaly', 'version.rb')

def parse_options
  options = {}

  option_parser = OptionParser.new do |opts|
    opts.banner = "Usage: build-proto-gem [options]"

    opts.on("-n", "--name NAME", "Name of the gem. By default, the name is hard-coded to `gitaly`") do |name|
      options[:gem_name] = name
    end

    opts.on_tail("--skip-verify-tag", "Skip verification that this is run for a tagged Gitaly commit") do
      options[:skip_verify_tag] = true
    end

    opts.on("-w", "--working-dir DIR", "Working dir of the gem. If not specified, a temporary dir is used") do |path|
      options[:working_dir] = path
    end

    opts.on("-o", "--output PATH", "output path for the gem") do |path|
      options[:output_path] = File.absolute_path(path)
    end
  end

  option_parser.parse!

  if options[:output_path].nil?
    puts option_parser.help
    exit 1
  end

  return options
end

def main(options)
  version = File.read(File.join(SOURCE_DIR, 'VERSION')).strip
  unless version.match?(/\d+\.\d+\.\d+(-rc\d+)?/)
    abort "Version string #{version.inspect} does not look like a Gitaly Release tag (e.g. \"v1.0.2\"). Aborting."
  end

  if options[:skip_verify_tag]
    matches = /^(\d+\.\d+\.\d+).*/.match(version)
    if matches.nil?
      abort "Invalid version number #{version}"
    end

    ref = capture!(%w[git rev-parse --short HEAD]).chomp
    version = "#{matches[1]}-#{ref}"
  else
    ref = capture!(%w[git describe --tag]).chomp
    if ref != "v#{version}"
      abort "Checkout tag v#{version} to publish.\n\t git checkout v#{version}"
    end
  end

  puts 'Testing for changed files'
  run!(%w[git diff --quiet --exit-code])

  puts 'Testing for staged changes'
  run!(%w[git diff --quiet --cached --exit-code])

  if options[:working_dir]
    output_dir = File.absolute_path(options[:working_dir])
    generate_sources(output_dir, version)
    build_gem(options, output_dir, options[:output_path])
  else
    Dir.mktmpdir do |output_dir|
      generate_sources(output_dir, version)
      build_gem(options, output_dir, options[:output_path])
    end
  end
end

def generate_sources(output_dir, version)
  proto_output_dir = File.absolute_path(File.join(output_dir, 'ruby', 'proto', 'gitaly'))

  FileUtils.mkdir_p(proto_output_dir)

  proto_dir = File.join(SOURCE_DIR, 'proto')
  proto_files = Dir[File.join(proto_dir, '*.proto')].sort

  run!(
    %W[bundle exec grpc_tools_ruby_protoc -I #{proto_dir} --ruby_out=#{proto_output_dir} --grpc_out=#{proto_output_dir}] + proto_files,
    File.join(SOURCE_DIR, 'tools', 'protogem')
  )

  write_version_file(output_dir, version)
  write_ruby_requires(output_dir)
end

def build_gem(options, output_dir, output_path)
  gemspec = <<~EOT
    # coding: utf-8
    prefix = 'ruby/proto'
    $LOAD_PATH.unshift(File.expand_path(File.join(prefix), __dir__))
    require 'gitaly/version'

    Gem::Specification.new do |spec|
      spec.name          = "#{options[:gem_name] || 'gitaly'}"
      spec.version       = Gitaly::VERSION
      spec.authors       = ["GitLab Engineering"]
      spec.email         = ["engineering@gitlab.com"]

      spec.summary       = %q{Auto-generated gRPC client for gitaly}
      spec.description   = %q{Auto-generated gRPC client for gitaly.\nTo publish new versions see https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/PROCESS.md#publishing-the-ruby-gem.}
      spec.homepage      = "https://gitlab.com/gitlab-org/gitaly"
      spec.license       = "MIT"

      spec.metadata = {
        "homepage_uri"      => "https://gitlab.com/gitlab-org/gitaly",
        "bug_tracker_uri"   => "https://gitlab.com/gitlab-org/gitaly/-/issues",
        "source_code_uri"   => "https://gitlab.com/gitlab-org/gitaly/-/tree/master/proto",
        "documentation_uri" => "https://gitlab-org.gitlab.io/gitaly",
      }

      spec.files         = Dir['**/*.rb']
      spec.require_paths = [prefix]

      spec.add_dependency "grpc", "~> 1.0"
    end
  EOT

  gemspec_path = File.absolute_path(File.join(output_dir, 'gitaly.gemspec'))
  open(gemspec_path, 'w') { |f| f.write(gemspec) }

  run!(['gem', 'build', gemspec_path, '--output', output_path], output_dir)
  abort "gem not found" unless File.exist?(output_path)
end

def write_version_file(output_dir, version)
  path = File.join(output_dir, 'ruby', 'proto', RUBY_VERSION_FILE)
  content = <<~EOF
    # This file is generated by #{File.basename($0)}. Do not edit.
    module Gitaly
      VERSION = '#{version}'
    end
  EOF

  open(path, 'w') { |f| f.write(content) }
end

def write_ruby_requires(output_dir)
  requires = Dir.glob(File.join('gitaly', '*_services_pb.rb'), base: File.join(output_dir, 'ruby', 'proto')).sort
  abort "No auto-generated Ruby service files found" if requires.empty?
  requires.unshift(RUBY_VERSION_FILE)

  gem_root = File.join(output_dir, 'ruby', 'proto', 'gitaly.rb')
  gem_root_template = ERB.new <<~EOT
    # This file is generated by #{File.basename($0)}. Do not edit.
    $:.unshift(File.expand_path('../gitaly', __FILE__))
    <% requires.each do |f| %>
    require '<%= f.sub(/\.rb$/, '') %>'
    <% end %>
  EOT

  open(gem_root, 'w') { |f| f.write(gem_root_template.result(binding)) }
end

def run!(cmd, chdir='.')
  GitalySupport.print_cmd(cmd)
  unless system(*cmd, chdir: chdir)
    GitalySupport.fail_cmd!(cmd)
  end
end

def capture!(cmd, chdir='.')
  GitalySupport.print_cmd(cmd)
  output = IO.popen(cmd, chdir: chdir) { |io| io.read }
  GitalySupport.fail_cmd!(cmd) unless $?.success?
  output
end

module GitalySupport
  class << self
    def print_cmd(cmd)
      puts '-> ' + printable_cmd(cmd)
    end

    def fail_cmd!(cmd)
      abort "command failed: #{printable_cmd(cmd)}"
    end

    def printable_cmd(cmd)
      cmd.join(' ')
    end
  end
end

main(parse_options)
