#!/usr/bin/ruby.ruby2.5
#
# A simple script to update sources in rails app in order to bundle
# required gems
#
# (C) 2018 SUSE LLC
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# See http://www.gnu.org/licenses/gpl-2.0.html for full license text.
#
require 'rubygems/package'
require 'zlib'
require 'tempfile'
require 'logger'
require 'fileutils'
require 'optparse'
require 'open3'

# Setup logger
@logger = Logger.new(STDOUT)
@logger.level = Logger::INFO
@logger.progname = File.basename($PROGRAM_NAME)
@logger.formatter = proc do |severity, datetime, progname, msg|
  date_format = datetime.strftime('%Y-%m-%d %H:%M:%S')
  "[#{date_format}] #{severity.ljust(5)} (#{progname}): #{msg}\n"
end

# Parse options
options = { strategy: :spec }
OptionParser.new do |opts|
  opts.banner = "Usage: #{ARGV[0]} [options]"
  opts.on('-oDIR', '--outdir=DIR', 'Output Directory') do |v|
    options[:outdir] = v
  end
  strategies = %i[cpio spec]
  msg = "Choose the strategy the service runs in. Options: #{strategies.join(', ')}"
  opts.on('--strategy=STRING', strategies, msg) do |v|
    options[:strategy] = v
  end
end.parse!

outdir = options[:outdir] || Dir.pwd

# Find Gemfile (matches _service:obs_scm:Gemfile and Gemfile)
gem_file = Dir.glob('*Gemfile').first.to_s
if gem_file.empty?
  @logger.fatal 'No Gemfile found'
  exit(1)
end
@logger.info "Using #{gem_file}"

# Find Gemfile.lock (matches _service:obs_scm:Gemfile.lock and Gemfile.lock)
gem_file_lock = Dir.glob('*Gemfile.lock').first.to_s
if gem_file_lock.empty?
  @logger.fatal 'No Gemfile.lock found'
  exit(1)
end
@logger.info "Using #{gem_file_lock}"

# Get the bundler version defined in gem_file_lock...
next_line = false
File.readlines(gem_file_lock).each do |line|
  @bundledwith = line if next_line
  next_line = false if next_line
  next_line = true if line.start_with?('BUNDLED WITH')
end
@bundledwith = @bundledwith.lstrip.chomp if @bundledwith

def run_command(command:, environment: {})
  stdout_and_stderr_str, status = Open3.capture2e(environment, command)
  @logger.info stdout_and_stderr_str
  abort(stdout_and_stderr_str) unless status.success?
end

def bundle(command:)
  if @bundler_directory
    bundler_bin = "#{@bundler_directory}/bin/bundle _#{Bundler::VERSION}_"
    run_command(command: "#{bundler_bin} #{command}", environment: { 'GEM_HOME' => @bundler_directory })
  else
    run_command(command: "bundle #{command}")
  end
end

def install_bundler(version:)
  @logger.info "Installing bundler version #{version}"
  bundler_directory = Dir.mktmpdir(nil, Dir.pwd)

  Dir.chdir(bundler_directory) do
    run_command(command: "gem install --no-format-executable bundler -v #{version}", environment: { 'GEM_HOME' => bundler_directory })
    # adapt LOAD_PATH, gosh this is hacky...
    bundler_lib_path = Dir.glob('gems/bundler-*/lib').first
    $LOAD_PATH.unshift("#{bundler_directory}/#{bundler_lib_path}")
    require 'bundler'
  end

  ENV['GEM_HOME'] = bundler_directory

  bundler_directory
end

# Install and require the right bundler version
if @bundledwith
  @bundler_directory = install_bundler(version: @bundledwith)
  @logger.info "Using bundler (#{Bundler::VERSION}) defined by 'BUNDLED WITH' in #{gem_file_lock}..."
else
  require 'bundler'
  @logger.info "Using system bundler (#{Bundler::VERSION}), no 'BUNDLED WITH' in #{gem_file_lock}..."
end

# Execute with cpio strategy
if options[:strategy] == :cpio
  FileUtils.cp gem_file, File.join(outdir, 'Gemfile')
  FileUtils.cp gem_file_lock, File.join(outdir, 'Gemfile.lock')
  vendor = Dir.glob('*vendor.obscpio').first.to_s
  unless vendor.empty?
    @logger.info 'Extracting vendor.obscpio...'
    FileUtils.cp vendor, options[:outdir]
    Dir.chdir(options[:outdir]) do
      run_command(command: "cpio -i --make-directories --no-absolute-filenames --format=newc < #{vendor}")
    end
  end

  Dir.chdir(options[:outdir]) do
    if ENV['OBS_SERVICE_BUNDLE_GEMS_MIRROR_URL']
      mirror_url = ENV['OBS_SERVICE_BUNDLE_GEMS_MIRROR_URL']
      # Set HTTP mirror
      bundle(command: "config --local mirror.http://rubygems.org #{mirror_url}")
      # Set HTTPS mirror
      bundle(command: "config --local mirror.https://rubygems.org #{mirror_url}")
    end
    bundle(command: 'config force_ruby_platform true')
    bundle(command: 'package --no-install --all --path . --verbose')
    run_command(command: 'find vendor/cache/ -depth -type f -print |  cpio --format=newc -o > vendor.obscpio')
    FileUtils.rm ['Gemfile', 'Gemfile.lock']
    FileUtils.remove_dir 'vendor'
    FileUtils.remove_dir 'ruby'
    FileUtils.remove_dir '.bundle'
    FileUtils.remove_dir @bundler_directory
  end

  exit 0
end

# Execute with spec strategy
spec = Dir['*.spec'].first
unless spec
  @logger.fatal 'No spec found'
  exit(1)
end

@logger.info 'Resolving...'
definition = Bundler::Definition.build(gem_file, gem_file_lock, nil)
bundled_gems = definition.resolve.to_a.sort_by(&:to_s)
@logger.info "  #{bundled_gems.size} gems..."

@logger.info "Updating #{spec}"
# Now parse the spec file
gems_start = false
new_spec_lines = []
File.open(spec, 'r').each_line do |line|
  if line =~ /^### GEMS START/
    gems_start = true
    new_spec_lines.push(line)
    i = 100
    bundled_gems.each do |s|
      new_spec_lines.push("Source#{i}: https://rubygems.org/downloads/#{s.name}-#{s.version}.gem\n")
      i += 1
    end
    new_spec_lines.push("### GEMS END\n")
    next
  end

  if line =~ /^### GEMS END/
    gems_start = false
    next
  end

  if line =~ /^Source(.*):/
    # drop this one
    next if gems_start
  end

  new_spec_lines.push(line)
end

File.open(File.join(outdir, spec), 'w') do |f|
  f.write(new_spec_lines.join)
end
FileUtils.remove_dir @bundler_directory
@logger.info 'DONE'
