require 'yaml'
require 'json'
require 'rbconfig'
require_relative 'general_support'

module CiWorkflowSupport
  # Make instance methods available as class methods
  extend self

  DISTRIBUTIONS_BUCKETS_MAX_NUM = 4

  @@config = nil


  def config
    @@config
  end

  def load_config
    @@config = YAML.safe_load(File.read(config_file_path, encoding: 'utf-8'),
      filename: config_file_path)
    @@config = recursively_symbolize_keys(@@config)
  end


  def indentyaml(object)
    # Change symbols to strings
    object = JSON.parse(JSON.dump(object))

    str = YAML.dump(object)
    str.sub!(/^---\n/, '')

    indent = ' ' * 10
    lines = str.split("\n")
    lines.map! { |line| indent + line }
    "\n" + lines.join("\n")
  end

  def editing_warning_comment(source)
    "# WARNING: DO NOT EDIT THIS FILE!!!\n" \
    "#\n" \
    "# This file is autogenerated from .github/workflows/#{source}.yml.erb\n" \
    "# by ./internal-scripts/generate-ci-cd-yaml.rb.\n" \
    "# Please edit the .erb file instead, then regenerate YAML\n" \
    "# by running that script.\n" \
    "#\n" \
    "# TIP: run this on your development machine to ensure generate-ci-cd-yaml.rb\n" \
    "# is run automatically as a Git pre-commit hook:\n" \
    "#\n" \
    "#   git config core.hooksPath .githooks"
  end


  def distributions
    @distributions ||= begin
      if config[:distributions] == 'all'
        envs = Dir["#{GeneralSupport::ROOT}/environments/*"].map do |path|
          File.basename(path)
        end
        envs.delete('utility')
        envs.sort!
        names = envs
      elsif config[:distributions].is_a?(Array)
        names = config[:distributions]
      else
        abort "Config error: 'distributions' must be set to 'all' or to a list"
      end

      names.map do |name|
        {
          name: name,
          package_format: autodetect_package_format(name),
          test_image: determine_test_image_for(name),
        }
      end
    end
  end

  def distribution_buckets
    @distribution_buckets ||= GeneralSupport.bucketize(distributions,
      DISTRIBUTIONS_BUCKETS_MAX_NUM)
  end

  def variants
    @variants ||= begin
      result = []
      if config[:variants][:normal]
        result << {
          name: 'normal',
          package_suffix: ''
        }
      end
      if config[:variants][:jemalloc]
        result << {
          name: 'jemalloc',
          package_suffix: '-jemalloc'
        }
      end
      if config[:variants][:malloctrim]
        result << {
          name: 'malloctrim',
          package_suffix: '-malloctrim'
        }
      end
      result
    end
  end


  def docker_images
    Dir["#{GeneralSupport::ROOT}/environments/*"].find_all do |path|
      File.exist?("#{path}/Dockerfile")
    end.sort.map do |path|
      id = File.basename(path)
      {
        name: "ghcr.io/fullstaq-ruby/server-edition-ci-images",
        id: id,
        tag: id + '-v' + read_single_value_file("#{path}/image_tag")
      }
    end
  end

  def docker_image_artifact_name(distribution_name)
    "docker-image-#{distribution_name}"
  end


  def common_deb_version
    config[:common][:deb][:version]
  end

  def common_deb_package_revision
    config[:common][:deb][:package_revision]
  end

  def common_deb_basename
    common_package_basename(:DEB)
  end

  def common_rpm_version
    config[:common][:rpm][:version]
  end

  def common_rpm_package_revision
    config[:common][:rpm][:package_revision]
  end

  def common_rpm_basename
    common_package_basename(:RPM)
  end

  def common_package_basename(package_format)
    case package_format
    when :DEB
      "fullstaq-ruby-common_#{common_deb_version}-#{common_deb_package_revision}_all.deb"
    when :RPM
      "fullstaq-ruby-common-#{common_rpm_version}-#{common_rpm_package_revision}.noarch.rpm"
    else
      raise "Unsupported package format: #{package_format.inspect}"
    end
  end

  def common_deb_artifact_name
    'common-deb'
  end

  def common_rpm_artifact_name
    'common-rpm'
  end


  def rbenv_version
    config[:rbenv][:version]
  end

  def rbenv_package_revision
    config[:rbenv][:package_revision]
  end

  def rbenv_deb_basename
    rbenv_package_basename(:DEB)
  end

  def rbenv_rpm_basename
    rbenv_package_basename(:RPM)
  end

  def rbenv_package_basename(package_format)
    case package_format
    when :DEB
      "fullstaq-rbenv_#{rbenv_version}-#{rbenv_package_revision}_all.deb"
    when :RPM
      version_str = rbenv_version.to_s.gsub('-', '_')
      revision_str = rbenv_package_revision.to_s.gsub('-', '_')
      "fullstaq-rbenv-#{version_str}-#{revision_str}.noarch.rpm"
    else
      raise "Unsupported package format: #{package_format.inspect}"
    end
  end

  def rbenv_source_artifact_name
    'rbenv-src'
  end

  def rbenv_deb_artifact_name
    'rbenv-deb'
  end

  def rbenv_rpm_artifact_name
    'rbenv-rpm'
  end


  def jemalloc_version
    config[:jemalloc_version]
  end

  def jemalloc_source_basename
    "jemalloc-#{config[:jemalloc_version]}.tar.bz2"
  end

  def jemalloc_source_url
    "https://github.com/jemalloc/jemalloc/releases/download/#{jemalloc_version}/#{jemalloc_source_basename}"
  end


  def ruby_source_versions
    @ruby_source_versions ||= begin
      result = []
      result << (config[:ruby][:minor_version_packages] || []).map do |entry|
        entry[:full_version]
      end
      result << (config[:ruby][:tiny_version_packages] || []).map do |entry|
        entry[:full_version]
      end
      result.flatten!
      result.uniq!
      result
    end
  end

  def ruby_package_versions
    @ruby_package_versions ||= begin
      all = (config[:ruby][:minor_version_packages] || []) +
        (config[:ruby][:tiny_version_packages] || [])
      all.map do |entry|
        entry = entry.dup
        entry[:id] = entry[:minor_version] || entry[:full_version]
        entry
      end
    end
  end

  def ruby_package_versions_for_distro(distro)
    result = ruby_package_versions.find_all do |ruby_package_version|
      !ruby_package_version_excluded_from_distro?(ruby_package_version, distro)
    end
    if result.empty?
      abort "Config error: all Ruby versions have been excluded from #{distro[:name]}"
    end
    result
  end

  def ruby_package_version_excluded_from_distro?(ruby_package_version, distro)
    ruby_minor_version = (ruby_package_version[:minor_version] ||
      infer_ruby_minor_version_from_full_version(ruby_package_version[:full_version]))
    config[:distribution_exclusions].any? do |exclusion|
      exclusion[:ruby_minor_version] == ruby_minor_version &&
        exclusion[:distros].include?(distro[:name])
    end
  end

  def ruby_source_basename(source_version)
    "ruby-#{source_version}.tar.gz"
  end

  def ruby_source_url(source_version)
    minor_version = source_version.sub(/(.+)\..*/, '\1')
    "https://cache.ruby-lang.org/pub/ruby/#{minor_version}/#{ruby_source_basename(source_version)}"
  end

  def ruby_package_basename(package_version, distro, variant)
    case distro[:package_format]
    when :DEB
      "fullstaq-ruby-#{package_version[:id]}#{variant[:package_suffix]}_#{package_version[:package_revision]}-#{distro[:name]}_#{deb_arch}.deb"
    when :RPM
      "fullstaq-ruby-#{package_version[:id]}#{variant[:package_suffix]}-rev#{package_version[:package_revision]}-#{sanitize_distro_name_for_rpm(distro[:name])}.#{rpm_arch}.rpm"
    else
      raise "Unsupported package format: #{distro[:package_format].inspect}"
    end
  end

  def ruby_source_artifact_name(ruby_version)
    "ruby-src-#{ruby_version}"
  end

  def ruby_package_artifact_name(ruby_package_version, distribution, variant)
    "ruby-pkg_#{ruby_package_version[:id]}_#{distribution[:name]}_#{variant[:name]}"
  end

  def ruby_package_artifact_names
    result = []
    distributions.each do |distribution|
      ruby_package_versions_for_distro(distribution).each do |ruby_package_version|
        variants.each do |variant|
          result << ruby_package_artifact_name(ruby_package_version, distribution, variant)
        end
      end
    end
    result
  end

private
  def config_file_path
    config_file_path ||= ENV.fetch('CONFIG', "#{GeneralSupport::ROOT}/config.yml")
  end

  def autodetect_package_format(environment)
    dockerfile = File.read("#{GeneralSupport::ROOT}/environments/#{environment}/Dockerfile",
      encoding: 'utf-8')
    if dockerfile =~ /(yum|dnf) install/
      :RPM
    else
      :DEB
    end
  end

  def sanitize_distro_name_for_rpm(distro_name)
    distro_name.gsub('-', '')
  end

  def infer_ruby_minor_version_from_full_version(ruby_minor_version)
    ruby_minor_version.split('.')[0..1].join('.')
  end

  def recursively_symbolize_keys(thing)
    case thing
    when Array
      thing.map do |entry|
        recursively_symbolize_keys(entry)
      end
    when Hash
      result = {}
      thing.each_pair do |k, v|
        result[k.to_sym] = recursively_symbolize_keys(v)
      end
      result
    else
      thing
    end
  end

  def determine_test_image_for(distro_name)
    dockerfile = File.read("#{GeneralSupport::ROOT}/environments/#{distro_name}/Dockerfile",
      encoding: 'utf-8')
    dockerfile =~ /FROM (.+)/
    $1
  end

  def deb_arch
    @deb_arch ||= begin
      if on_macos?
        # Assuming macOS with Docker for Mac
        return 'amd64'
      end

      arch = cpu_architecture
      case arch
      when 'x86'
        'i386'
      when 'x86_64'
        'amd64'
      else
        arch
      end
    end
  end

  def rpm_arch
    @rpm_arch ||= begin
      if on_macos?
        # Assuming macOS with Docker for Mac
        return 'x86_64'
      end

      arch = cpu_architecture
      case arch
      when 'x86'
        'i686'
      else
        arch
      end
    end
  end

  def on_macos?
    RbConfig::CONFIG['target_os'] =~ /darwin/ && File.exist?('/usr/bin/sw_vers')
  end

  def cpu_architecture
    @cpu_architecture ||= begin
      arch = `uname -p`.strip
      # On some systems 'uname -p' returns something like
      # 'Intel(R) Pentium(R) M processor 1400MHz' or
      # 'Intel(R)_Xeon(R)_CPU___________X7460__@_2.66GHz'.
      if arch == "unknown" || arch =~ / / || arch =~ /Hz$/
        arch = `uname -m`.strip
      end
      if arch =~ /^i.86$/
        'x86'
      elsif arch == 'amd64'
        'x86_64'
      else
        arch
      end
    end
  end
end
