# frozen_string_literal: true

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

Puppet::Type.type(:package).provide :dpkg, :parent => Puppet::Provider::Package do
  desc "Package management via `dpkg`.  Because this only uses `dpkg`
    and not `apt`, you must specify the source of any packages you want
    to manage."

  has_feature :holdable, :virtual_packages
  commands :dpkg => "/usr/bin/dpkg"
  commands :dpkg_deb => "/usr/bin/dpkg-deb"
  commands :dpkgquery => "/usr/bin/dpkg-query"

  # Performs a dpkgquery call with a pipe so that output can be processed
  # inline in a passed block.
  # @param args [Array<String>] any command line arguments to be appended to the command
  # @param block expected to be passed on to execpipe
  # @return whatever the block returns
  # @see Puppet::Util::Execution.execpipe
  # @api private
  def self.dpkgquery_piped(*args, &block)
    cmd = args.unshift(command(:dpkgquery))
    Puppet::Util::Execution.execpipe(cmd, &block)
  end

  def self.instances
    packages = []

    # list out all of the packages
    dpkgquery_piped('-W', '--showformat', self::DPKG_QUERY_FORMAT_STRING) do |pipe|
      # now turn each returned line into a package object
      pipe.each_line do |line|
        hash = parse_line(line)
        if hash
          packages << new(hash)
        end
      end
    end

    packages
  end

  private

  # Note: self:: is required here to keep these constants in the context of what will
  # eventually become this Puppet::Type::Package::ProviderDpkg class.
  self::DPKG_QUERY_FORMAT_STRING = "'${Status} ${Package} ${Version}\\n'"
  self::DPKG_QUERY_PROVIDES_FORMAT_STRING = "'${Status} ${Package} ${Version} [${Provides}]\\n'"
  self::FIELDS_REGEX = /^'?(\S+) +(\S+) +(\S+) (\S+) (\S*)$/
  self::FIELDS_REGEX_WITH_PROVIDES = /^'?(\S+) +(\S+) +(\S+) (\S+) (\S*) \[.*\]$/
  self::FIELDS = [:desired, :error, :status, :name, :ensure]

  def self.defaultto_allow_virtual
    false
  end

  # @param line [String] one line of dpkg-query output
  # @return [Hash,nil] a hash of FIELDS or nil if we failed to match
  # @api private
  def self.parse_line(line, regex = self::FIELDS_REGEX)
    hash = nil

    match = regex.match(line)
    if match
      hash = {}

      self::FIELDS.zip(match.captures) do |field, value|
        hash[field] = value
      end

      hash[:provider] = name

      if hash[:status] == 'not-installed'
        hash[:ensure] = :purged
      elsif %w[config-files half-installed unpacked half-configured].include?(hash[:status])
        hash[:ensure] = :absent
      end
      hash[:mark] = hash[:desired] == 'hold' ? :hold : :none
    else
      Puppet.debug("Failed to match dpkg-query line #{line.inspect}")
    end

    hash
  end

  public

  def install
    file = @resource[:source]
    unless file
      raise ArgumentError, _("You cannot install dpkg packages without a source")
    end

    args = []

    if @resource[:configfiles] == :keep
      args << '--force-confold'
    else
      args << '--force-confnew'
    end
    args << '-i' << file

    unhold if properties[:mark] == :hold
    begin
      with_environment { dpkg(*args) }
    ensure
      hold if @resource[:mark] == :hold
    end
  end

  def update
    install
  end

  # Return the version from the package.
  def latest
    source = @resource[:source]
    unless source
      @resource.fail _("Could not update: You cannot install dpkg packages without a source")
    end
    output = dpkg_deb "--show", source
    matches = /^(\S+)\t(\S+)$/.match(output).captures
    warning _("source doesn't contain named package, but %{name}") % { name: matches[0] } unless matches[0].match(Regexp.escape(@resource[:name]))
    matches[1]
  end

  def query
    hash = nil

    # list out our specific package
    begin
      if @resource.allow_virtual?
        output = dpkgquery(
          "-W",
          "--showformat",
          self.class::DPKG_QUERY_PROVIDES_FORMAT_STRING
          # the regex searches for the resource[:name] in the dpkquery result in which the Provides field is also available
          # it will search for the packages only in the brackets ex: [rubygems]
        ).lines.find { |package| package.match(/[\[ ](#{Regexp.escape(@resource[:name])})[\],]/) }
        if output
          hash = self.class.parse_line(output, self.class::FIELDS_REGEX_WITH_PROVIDES)
          Puppet.info("Package #{@resource[:name]} is virtual, defaulting to #{hash[:name]}")
          @resource[:name] = hash[:name]
        end
      end
      output = dpkgquery(
        "-W",
        "--showformat",
        self.class::DPKG_QUERY_FORMAT_STRING,
        @resource[:name]
      )
      hash = self.class.parse_line(output)
    rescue Puppet::ExecutionFailure
      # dpkg-query exits 1 if the package is not found.
      return { :ensure => :purged, :status => 'missing', :name => @resource[:name], :error => 'ok' }
    end

    hash ||= { :ensure => :absent, :status => 'missing', :name => @resource[:name], :error => 'ok' }

    if hash[:error] != "ok"
      raise Puppet::Error, "Package #{hash[:name]}, version #{hash[:ensure]} is in error state: #{hash[:error]}"
    end

    hash
  end

  def uninstall
    with_environment { dpkg "-r", @resource[:name] }
  end

  def purge
    with_environment { dpkg "--purge", @resource[:name] }
  end

  def hold
    Tempfile.open('puppet_dpkg_set_selection') do |tmpfile|
      tmpfile.write("#{@resource[:name]} hold\n")
      tmpfile.flush
      execute([:dpkg, "--set-selections"], :failonfail => false, :combine => false, :stdinfile => tmpfile.path.to_s)
    end
  end

  def unhold
    Tempfile.open('puppet_dpkg_set_selection') do |tmpfile|
      tmpfile.write("#{@resource[:name]} install\n")
      tmpfile.flush
      execute([:dpkg, "--set-selections"], :failonfail => false, :combine => false, :stdinfile => tmpfile.path.to_s)
    end
  end
end
