# frozen_string_literal: true

# Packaging using Peter Bonivart's pkgutil program.
Puppet::Type.type(:package).provide :pkgutil, :parent => :sun, :source => :sun do
  desc "Package management using Peter Bonivart's ``pkgutil`` command on Solaris."

  pkgutil_bin = "pkgutil"
  if FileTest.executable?("/opt/csw/bin/pkgutil")
    pkgutil_bin = "/opt/csw/bin/pkgutil"
  end

  confine 'os.family' => :solaris

  has_command(:pkguti, pkgutil_bin) do
    environment :HOME => ENV.fetch('HOME', nil)
  end

  def self.healthcheck
    unless Puppet::FileSystem.exist?("/var/opt/csw/pkgutil/admin")
      Puppet.notice _("It is highly recommended you create '/var/opt/csw/pkgutil/admin'.")
      Puppet.notice _("See /var/opt/csw/pkgutil")
    end

    correct_wgetopts = false
    ["/opt/csw/etc/pkgutil.conf", "/etc/opt/csw/pkgutil.conf"].each do |confpath|
      File.open(confpath) do |conf|
        conf.each_line { |line| correct_wgetopts = true if line =~ /^\s*wgetopts\s*=.*(-nv|-q|--no-verbose|--quiet)/ }
      end
    end
    unless correct_wgetopts
      Puppet.notice _("It is highly recommended that you set 'wgetopts=-nv' in your pkgutil.conf.")
    end
  end

  def self.instances(hash = {})
    healthcheck

    # Use the available pkg list (-a) to work out aliases
    aliases = {}
    availlist.each do |pkg|
      aliases[pkg[:name]] = pkg[:alias]
    end

    # The -c pkglist lists installed packages
    pkginsts = []
    output = pkguti(["-c"])
    parse_pkglist(output).each do |pkg|
      pkg.delete(:avail)
      pkginsts << new(pkg)

      # Create a second instance with the alias if it's different
      pkgalias = aliases[pkg[:name]]
      next unless pkgalias and pkg[:name] != pkgalias

      apkg = pkg.dup
      apkg[:name] = pkgalias
      pkginsts << new(apkg)
    end

    pkginsts
  end

  # Turns a pkgutil -a listing into hashes with the common alias, full
  # package name and available version
  def self.availlist
    output = pkguti ["-a"]

    output.split("\n").filter_map do |line|
      next if line =~ /^common\s+package/ # header of package list
      next if noise?(line)

      if line =~ /\s*(\S+)\s+(\S+)\s+(.*)/
        { :alias => Regexp.last_match(1), :name => Regexp.last_match(2), :avail => Regexp.last_match(3) }
      else
        Puppet.warning _("Cannot match %{line}") % { line: line }
      end
    end
  end

  # Turn our pkgutil -c listing into a hash for a single package.
  def pkgsingle(resource)
    # The --single option speeds up the execution, because it queries
    # the package management system for one package only.
    command = ["-c", "--single", resource[:name]]
    self.class.parse_pkglist(run_pkgutil(resource, command), { :justme => resource[:name] })
  end

  # Turn our pkgutil -c listing into a bunch of hashes.
  def self.parse_pkglist(output, hash = {})
    output = output.split("\n")

    if output[-1] == "Not in catalog"
      Puppet.warning _("Package not in pkgutil catalog: %{package}") % { package: hash[:justme] }
      return nil
    end

    list = output.filter_map do |line|
      next if line =~ /installed\s+catalog/ # header of package list
      next if noise?(line)

      pkgsplit(line)
    end

    if hash[:justme]
      # Single queries may have been for an alias so return the name requested
      if list.any?
        list[-1][:name] = hash[:justme]
        list[-1]
      end
    else
      list.reject! { |h| h[:ensure] == :absent }
      list
    end
  end

  # Identify common types of pkgutil noise as it downloads catalogs etc
  def self.noise?(line)
    return true if line =~ /^#/
    return true if line =~ /^Checking integrity / # use_gpg
    return true if line =~ /^gpg: /               # gpg verification
    return true if line =~ /^=+> /                # catalog fetch
    return true if line =~ /\d+:\d+:\d+ URL:/     # wget without -q

    false
  end

  # Split the different lines into hashes.
  def self.pkgsplit(line)
    if line =~ /\s*(\S+)\s+(\S+)\s+(.*)/
      hash = {}
      hash[:name] = Regexp.last_match(1)
      hash[:ensure] = if Regexp.last_match(2) == "notinst"
                        :absent
                      else
                        Regexp.last_match(2)
                      end
      hash[:avail] = Regexp.last_match(3)

      if hash[:avail] =~ /^SAME\s*$/
        hash[:avail] = hash[:ensure]
      end

      # Use the name method, so it works with subclasses.
      hash[:provider] = name

      hash
    else
      Puppet.warning _("Cannot match %{line}") % { line: line }
      nil
    end
  end

  def run_pkgutil(resource, *args)
    # Allow source to be one or more URLs pointing to a repository that all
    # get passed to pkgutil via one or more -t options
    if resource[:source]
      sources = [resource[:source]].flatten
      pkguti(*[sources.map { |src| ["-t", src] }, *args].flatten)
    else
      pkguti(*args.flatten)
    end
  end

  def install
    run_pkgutil @resource, "-y", "-i", @resource[:name]
  end

  # Retrieve the version from the current package file.
  def latest
    hash = pkgsingle(@resource)
    hash[:avail] if hash
  end

  def query
    hash = pkgsingle(@resource)
    hash || { :ensure => :absent }
  end

  def update
    run_pkgutil @resource, "-y", "-u", @resource[:name]
  end

  def uninstall
    run_pkgutil @resource, "-y", "-r", @resource[:name]
  end
end
