# dnfmodule - A puppet package provider for DNF modules
#
# Installing a module:
#  package { 'postgresql':
#   provider => 'dnfmodule',
#   ensure   => '9.6',  # install a specific stream
#   flavor   => 'client',  # install a specific profile
# }


require_relative '../../../puppet/provider/package'

Puppet::Type.type(:package).provide :dnfmodule, :parent => :dnf do

  has_feature :installable, :uninstallable, :versionable, :supports_flavors, :disableable
  #has_feature :upgradeable
  # it's not (yet) feasible to make this upgradeable since module streams don't
  # always have matching version types (i.e. idm has streams DL1 and client,
  # other modules have semver streams, others have string streams... we cannot
  # programatically determine a latest version for ensure => 'latest'

  commands :dnf => '/usr/bin/dnf'

  def self.current_version
    @current_version ||= dnf('--version').split.first
  end

  def self.prefetch(packages)
    if Puppet::Util::Package.versioncmp(current_version, '3.0.1') < 0
      raise Puppet::Error, _("Modules are not supported on DNF versions lower than 3.0.1")
    end
    super
  end

  def self.instances
    packages = []
    cmd = "#{command(:dnf)} module list -d 0 -e #{error_level}"
    execute(cmd).each_line do |line|
      # select only lines with actual packages since DNF clutters the output
      next unless line =~ /\[[eix]\][, ]/
      line.gsub!(/\[d\]/, '')  # we don't care about the default flag

      flavor = if line.include?('[i]')
                 line.split('[i]').first.split.last
               else
                 :absent
               end

      packages << new(
        name: line.split[0],
        ensure: if line.include?('[x]')
                  :disabled
                else
                  line.split[1]
                end,
        flavor: flavor,
        provider: name
      )
    end
    packages
  end

  def query
    pkg = self.class.instances.find do |package|
            @resource[:name] == package.name
          end
    pkg ? pkg.properties : nil
  end

  # to install specific streams and profiles:
  # $ dnf module install module-name:stream/profile
  # $ dnf module install perl:5.24/minimal
  # if unspecified, they will be defaulted (see [d] param in dnf module list output)
  def install
    # ensure we start fresh (remove existing stream)
    uninstall unless [:absent, :purged].include?(@property_hash[:ensure])

    args = @resource[:name].dup
    case @resource[:ensure]
    when true, false, Symbol
      # pass
    else
      args << ":#{@resource[:ensure]}"
    end
    args << "/#{@resource[:flavor]}" if @resource[:flavor]

    if @resource[:enable_only] == true
      enable(args)
    else
      begin
        execute([command(:dnf), 'module', 'install', '-d', '0', '-e', self.class.error_level, '-y', args])
      rescue Puppet::ExecutionFailure => e
        # module has no default profile and no profile was requested, so just enable the stream
        # DNF versions prior to 4.2.8 do not need this workaround
        # see https://bugzilla.redhat.com/show_bug.cgi?id=1669527
        if @resource[:flavor] == nil && e.message =~ /^(?:missing|broken) groups or modules: #{Regexp.quote(@resource[:name])}$/
          enable(args)
        else
          raise
        end
      end
    end
  end

  # should only get here when @resource[ensure] is :disabled
  def insync?(is)
    if resource[:ensure] == :disabled
      # in sync only if package is already disabled
      pkg = self.class.instances.find do |package|
        @resource[:name] == package.name && package.properties[:ensure] == :disabled
      end
      return true if pkg
    end
    return false
  end

  def enable(args = @resource[:name])
    execute([command(:dnf), 'module', 'enable', '-d', '0', '-e', self.class.error_level, '-y', args])
  end

  def uninstall
    execute([command(:dnf), 'module', 'remove', '-d', '0', '-e', self.class.error_level, '-y', @resource[:name]])
    reset  # reset module to the default stream
  end

  def disable(args = @resource[:name])
    execute([command(:dnf), 'module', 'disable', '-d', '0', '-e', self.class.error_level, '-y', args])
  end

  def reset
    execute([command(:dnf), 'module', 'reset', '-d', '0', '-e', self.class.error_level, '-y', @resource[:name]])
  end

  def flavor
    @property_hash[:flavor]
  end

  def flavor=(value)
    install if flavor != @resource.should(:flavor)
  end
end
