# frozen_string_literal: true

require_relative '../../../puppet/provider/package'
require 'fileutils'

Puppet::Type.type(:package).provide :portage, :parent => Puppet::Provider::Package do
  desc "Provides packaging support for Gentoo's portage system.

    This provider supports the `install_options` and `uninstall_options` attributes, which allows command-line
    flags to be passed to emerge. These options should be specified as an array where each element is either a string or a hash."

  has_features :install_options, :purgeable, :reinstallable, :uninstall_options, :versionable, :virtual_packages

  {
    :emerge => '/usr/bin/emerge',
    :eix => '/usr/bin/eix',
    :qatom_bin => '/usr/bin/qatom',
    :update_eix => '/usr/bin/eix-update',
  }.each_pair do |name, path|
    has_command(name, path) do
      environment :HOME => '/'
    end
  end

  confine 'os.family' => :gentoo

  defaultfor 'os.family' => :gentoo

  def self.instances
    result_format = eix_result_format
    result_fields = eix_result_fields

    limit = eix_limit
    version_format = eix_version_format
    slot_versions_format = eix_slot_versions_format
    installed_versions_format = eix_installed_versions_format
    installable_versions_format = eix_install_versions_format
    begin
      eix_file = File.directory?('/var/cache/eix') ? '/var/cache/eix/portage.eix' : '/var/cache/eix'
      update_eix unless FileUtils.uptodate?(eix_file, %w[/usr/bin/eix /usr/portage/metadata/timestamp])

      search_output = nil
      Puppet::Util.withenv :EIX_LIMIT => limit, :LASTVERSION => version_format, :LASTSLOTVERSIONS => slot_versions_format, :INSTALLEDVERSIONS => installed_versions_format, :STABLEVERSIONS => installable_versions_format do
        search_output = eix(*(eix_search_arguments + ['--installed']))
      end

      packages = []
      search_output.each_line do |search_result|
        match = result_format.match(search_result)

        next unless match

        package = {}
        result_fields.zip(match.captures) do |field, value|
          package[field] = value unless !value or value.empty?
        end
        package[:provider] = :portage
        packages << new(package)
      end

      packages
    rescue Puppet::ExecutionFailure => detail
      raise Puppet::Error, detail
    end
  end

  def install
    should = @resource.should(:ensure)
    cmd = %w[]
    name = qatom[:category] ? "#{qatom[:category]}/#{qatom[:pn]}" : qatom[:pn]
    name = qatom[:pfx] + name if qatom[:pfx]
    name = name + '-' + qatom[:pv] if qatom[:pv]
    name = name + '-' + qatom[:pr] if qatom[:pr]
    name = name + ':' + qatom[:slot] if qatom[:slot]
    cmd << '--update' if [:latest].include?(should)
    cmd += install_options if @resource[:install_options]
    cmd << name
    emerge(*cmd)
  end

  def uninstall
    should = @resource.should(:ensure)
    cmd = %w[--rage-clean]
    name = qatom[:category] ? "#{qatom[:category]}/#{qatom[:pn]}" : qatom[:pn]
    name = qatom[:pfx] + name if qatom[:pfx]
    name = name + '-' + qatom[:pv] if qatom[:pv]
    name = name + '-' + qatom[:pr] if qatom[:pr]
    name = name + ':' + qatom[:slot] if qatom[:slot]
    cmd += uninstall_options if @resource[:uninstall_options]
    cmd << name
    if [:purged].include?(should)
      Puppet::Util.withenv :CONFIG_PROTECT => "-*" do
        emerge(*cmd)
      end
    else
      emerge(*cmd)
    end
  end

  def reinstall
    install
  end

  def update
    install
  end

  def qatom
    output_format = qatom_output_format
    result_format = qatom_result_format
    result_fields = qatom_result_fields
    @atom ||= begin
      package_info = {}
      # do the search
      should = @resource[:ensure]
      case should
      # The terms present, absent, purged, installed, latest in :ensure
      # resolve as Symbols, and we do not need specific package version in this case
      when true, false, Symbol
        search = @resource[:name]
      else
        search = '=' + @resource[:name] + '-' + should.to_s
      end
      search_output = qatom_bin(*[search, '--format', output_format])
      # verify if the search found anything
      match = result_format.match(search_output)
      if match
        result_fields.zip(match.captures) do |field, value|
          # some fields can be empty or (null) (if we are not passed a category in the package name for instance)
          if value == '(null)' || value == '<unset>'
            package_info[field] = nil
          elsif !value or value.empty?
            package_info[field] = nil
          else
            package_info[field] = value
          end
        end
      end
      @atom = package_info
    rescue Puppet::ExecutionFailure => detail
      raise Puppet::Error, detail
    end
  end

  def qatom_output_format
    '"[%[CATEGORY]] [%[PN]] [%[PV]] [%[PR]] [%[SLOT]] [%[pfx]] [%[sfx]]"'
  end

  def qatom_result_format
    /^"\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\](.*)"$/
  end

  def qatom_result_fields
    [:category, :pn, :pv, :pr, :slot, :pfx, :sfx]
  end

  def self.get_sets
    @sets ||= @sets = emerge(*['--list-sets'])
  end

  def query
    limit = self.class.eix_limit
    result_format = self.class.eix_result_format
    result_fields = self.class.eix_result_fields

    version_format = self.class.eix_version_format
    slot_versions_format = self.class.eix_slot_versions_format
    installed_versions_format = self.class.eix_installed_versions_format
    installable_versions_format = self.class.eix_install_versions_format
    search_field = qatom[:category] ? '--category-name' : '--name'
    search_value = qatom[:category] ? "#{qatom[:category]}/#{qatom[:pn]}" : qatom[:pn]

    @eix_result ||= begin
      # package sets
      package_sets = []
      self.class.get_sets.each_line do |package_set|
        package_sets << package_set.to_s.strip
      end

      if @resource[:name] =~ /^@/
        if package_sets.include?(@resource[:name][1..].to_s)
          return({ :name => (@resource[:name]).to_s, :ensure => '9999', :version_available => nil, :installed_versions => nil, :installable_versions => "9999," })
        end
      end

      eix_file = File.directory?('/var/cache/eix') ? '/var/cache/eix/portage.eix' : '/var/cache/eix'
      update_eix unless FileUtils.uptodate?(eix_file, %w[/usr/bin/eix /usr/portage/metadata/timestamp])

      search_output = nil
      Puppet::Util.withenv :EIX_LIMIT => limit, :LASTVERSION => version_format, :LASTSLOTVERSIONS => slot_versions_format, :INSTALLEDVERSIONS => installed_versions_format, :STABLEVERSIONS => installable_versions_format do
        search_output = eix(*(self.class.eix_search_arguments + ['--exact', search_field, search_value]))
      end

      packages = []
      search_output.each_line do |search_result|
        match = result_format.match(search_result)

        next unless match

        package = {}
        result_fields.zip(match.captures) do |field, value|
          package[field] = value unless !value or value.empty?
        end
        # dev-lang python [3.4.5] [3.5.2] [2.7.12:2.7,3.4.5:3.4] [2.7.12:2.7,3.4.5:3.4,3.5.2:3.5] https://www.python.org/ An interpreted, interactive, object-oriented programming language
        # version_available is what we CAN install / update to
        # ensure is what is currently installed
        # This DOES NOT choose to install/upgrade or not, just provides current info
        # prefer checking versions to slots as versions are finer grained
        search = qatom[:pv]
        search = search + '-' + qatom[:pr] if qatom[:pr]
        if search
          package[:version_available] = eix_get_version_for_versions(package[:installable_versions], search)
          package[:ensure] = eix_get_version_for_versions(package[:installed_versions], search)
        elsif qatom[:slot]
          package[:version_available] = eix_get_version_for_slot(package[:slot_versions_available], qatom[:slot])
          package[:ensure] = eix_get_version_for_slot(package[:installed_slots], qatom[:slot])
        end

        package[:ensure] = package[:ensure] || :absent
        packages << package
      end

      case packages.size
      when 0
        raise Puppet::Error, _("No package found with the specified name [%{name}]") % { name: @resource[:name] }
      when 1
        @eix_result = packages[0]
      else
        raise Puppet::Error, _("More than one package with the specified name [%{search_value}], please use the category parameter to disambiguate") % { search_value: search_value }
      end
    rescue Puppet::ExecutionFailure => detail
      raise Puppet::Error, detail
    end
  end

  def latest
    query[:version_available]
  end

  private

  def eix_get_version_for_versions(versions, target)
    # [2.7.10-r1,2.7.12,3.4.3-r1,3.4.5,3.5.2] 3.5.2
    return nil if versions.nil?

    versions = versions.split(',')
    # [2.7.10-r1 2.7.12 3.4.3-r1 3.4.5 3.5.2]
    versions.find { |version| version == target }
    # 3.5.2
  end

  private

  def eix_get_version_for_slot(versions_and_slots, slot)
    # [2.7.12:2.7 3.4.5:3.4 3.5.2:3.5] 3.5
    return nil if versions_and_slots.nil?

    versions_and_slots = versions_and_slots.split(',')
    # [2.7.12:2.7 3.4.5:3.4 3.5.2:3.5]
    versions_and_slots.map! { |version_and_slot| version_and_slot.split(':') }
    # [2.7.12: 2.7
    #  3.4.5:  3.4
    #  3.5.2:  3.5]
    version_for_slot = versions_and_slots.find { |version_and_slot| version_and_slot.last == slot }
    # [3.5.2:  3.5]
    version_for_slot.first if version_for_slot
    # 3.5.2
  end

  def self.eix_search_format
    "'<category> <name> [<installedversions:LASTVERSION>] [<bestversion:LASTVERSION>] [<installedversions:LASTSLOTVERSIONS>] [<installedversions:INSTALLEDVERSIONS>] [<availableversions:STABLEVERSIONS>] [<bestslotversions:LASTSLOTVERSIONS>] <homepage> <description>\n'"
  end

  def self.eix_result_format
    /^(\S+)\s+(\S+)\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+\[(\S*)\]\s+(\S+)\s+(.*)$/
  end

  def self.eix_result_fields
    # ensure:[3.4.5], version_available:[3.5.2], installed_slots:[2.7.12:2.7,3.4.5:3.4], installable_versions:[2.7.10-r1,2.7.12,3.4.3-r1,3.4.5,3.5.2] slot_versions_available:[2.7.12:2.7,3.4.5:3.4,3.5.2:3.5]
    [:category, :name, :ensure, :version_available, :installed_slots, :installed_versions, :installable_versions, :slot_versions_available, :vendor, :description]
  end

  def self.eix_version_format
    '{last}<version>{}'
  end

  def self.eix_slot_versions_format
    '{!first},{}<version>:<slot>'
  end

  def self.eix_installed_versions_format
    '{!first},{}<version>'
  end

  def self.eix_install_versions_format
    '{!first}{!last},{}{}{isstable}<version>{}'
  end

  def self.eix_limit
    '0'
  end

  def self.eix_search_arguments
    ['--nocolor', '--pure-packages', '--format', eix_search_format]
  end

  def install_options
    join_options(@resource[:install_options])
  end

  def uninstall_options
    join_options(@resource[:uninstall_options])
  end
end
