# Whole new package, so include pack stuff
require 'puppet/provider/package'

Puppet::Type.type(:package).provide :portupgrade, :parent => Puppet::Provider::Package do
  include Puppet::Util::Execution

  desc "Support for FreeBSD's ports using the portupgrade ports management software.
    Use the port's full origin as the resource name. eg (ports-mgmt/portupgrade)
    for the portupgrade port."

  ## has_features is usually autodetected based on defs below.
  # has_features :installable, :uninstallable, :upgradeable

  commands :portupgrade   => "/usr/local/sbin/portupgrade",
  :portinstall   => "/usr/local/sbin/portinstall",
  :portversion   => "/usr/local/sbin/portversion",
  :portuninstall => "/usr/local/sbin/pkg_deinstall",
  :portinfo      => "/usr/sbin/pkg_info"

  ## Activate this only once approved by someone important.
  # defaultfor :operatingsystem => :freebsd

  # Remove unwanted environment variables.
  %w{INTERACTIVE UNAME}.each do |var|
    if ENV.include?(var)
      ENV.delete(var)
    end
  end

  ######## instances sub command (builds the installed packages list)

  def self.instances
    Puppet.debug "portupgrade.rb Building packages list from installed ports"

    # regex to match output from pkg_info
    regex = %r{^(\S+)-([^-\s]+):(\S+)$}
    # Corresponding field names
    fields = [:portname, :ensure, :portorigin]
    # define Temporary hash used, packages array of hashes
    hash = Hash.new
    packages = []

    # exec command
    cmdline = ["-aoQ"]
    begin
      output = portinfo(*cmdline)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new(output, $!)
    end

    # split output and match it and populate temp hash
    output.split("\n").each { |data|
      # reset hash to nil for each line
      hash.clear
      if match = regex.match(data)
        # Output matched regex
        fields.zip(match.captures) { |field, value|
          hash[field] = value
        }

        # populate the actual :name field from the :portorigin
        # Set :provider to this object name
        hash[:name] = hash[:portorigin]
        hash[:provider] = self.name

        # Add to the full packages listing
        packages << new(hash)
      else
        # unrecognised output from pkg_info
        Puppet.debug "portupgrade.Instances() - unable to match output: #{data}"
      end
    }

    # return the packages array of hashes
    return packages
  end

  ######## Installation sub command

  def install
    Puppet.debug "portupgrade.install() - Installation call on #{@resource[:name]}"
    # -M: yes, we're a batch, so don't ask any questions
    cmdline = ["-M BATCH=yes", @resource[:name]]

    # FIXME: it's possible that portinstall prompts for data so locks up.
    begin
      output = portinstall(*cmdline)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new(output, $!)
    end

    if output =~ /\*\* No such /
      raise Puppet::ExecutionFailure, "Could not find package #{@resource[:name]}"
    end

    # No return code required, so do nil to be clean
    return nil
  end

  ######## Latest subcommand (returns the latest version available, or current version if installed is latest)

  def latest
    Puppet.debug "portupgrade.latest() - Latest check called on #{@resource[:name]}"
    # search for latest version available, or return current version.
    # cmdline = "portversion -v <portorigin>", returns "<portname> <code> <stuff>"
    # or "** No matching package found: <portname>"
    cmdline = ["-v", @resource[:name]]

    begin
      output = portversion(*cmdline)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new(output, $!)
    end

    # Check: output format.
    if output =~ /^\S+-([^-\s]+)\s+(\S)\s+(.*)/
      installedversion = $1
      comparison = $2
      otherdata = $3

      # Only return a new version number when it's clear that there is a new version
      # all others return the current version so no unexpected 'upgrades' occur.
      case comparison
      when "=", ">"
        Puppet.debug "portupgrade.latest() - Installed package is latest (#{installedversion})"
        return installedversion
      when "<"
        # "portpkg-1.7_5 < needs updating (port has 1.14)"
        # "portpkg-1.7_5 < needs updating (port has 1.14) (=> 'newport/pkg')
        if otherdata =~ /\(port has (\S+)\)/
          newversion = $1
          Puppet.debug "portupgrade.latest() - Installed version needs updating to (#{newversion})"
          return newversion
        else
          Puppet.debug "portupgrade.latest() - Unable to determine new version from (#{otherdata})"
          return installedversion
        end
      when "?", "!", "#"
        Puppet.debug "portupgrade.latest() - Comparison Error reported from portversion (#{output})"
        return installedversion
      else
        Puppet.debug "portupgrade.latest() - Unknown code from portversion output (#{output})"
        return installedversion
      end

    else
      # error: output not parsed correctly, error out with nil.
      # Seriously - this section should never be called in a perfect world.
      # as verification that the port is installed has already happened in query.
      if output =~ /^\*\* No matching package /
        raise Puppet::ExecutionFailure, "Could not find package #{@resource[:name]}"
      else
        # Any other error (dump output to log)
        raise Puppet::ExecutionFailure, "Unexpected output from portversion: #{output}"
      end

      # Just in case we still are running, return nil
      return nil
    end

    # At this point normal operation has finished and we shouldn't have been called.
    # Error out and let the admin deal with it.
    raise Puppet::Error, "portversion.latest() - fatal error with portversion: #{output}"
  end

  ###### Query subcommand - return a hash of details if exists, or nil if it doesn't.
  # Used to make sure the package is installed

  def query
    Puppet.debug "portupgrade.query() - Called on #{@resource[:name]}"

    cmdline = ["-qO", @resource[:name]]
    begin
      output = portinfo(*cmdline)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new(output, $!)
    end

    # Check: if output isn't in the right format, return nil
    if output =~ /^(\S+)-([^-\s]+)/
      # Fill in the details
      hash = Hash.new
      hash[:portorigin] = self.name
      hash[:portname]   = $1
      hash[:ensure]     = $2

      # If more details are required, then we can do another pkg_info
      # query here and parse out that output and add to the hash
      # return the hash to the caller
      return hash
    else
      Puppet.debug "portupgrade.query() - package (#{@resource[:name]}) not installed"
      return nil
    end
  end

  ####### Uninstall command

  def uninstall
    Puppet.debug "portupgrade.uninstall() - called on #{@resource[:name]}"
    # Get full package name from port origin to uninstall with
    cmdline = ["-qO", @resource[:name]]
    begin
      output = portinfo(*cmdline)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new(output, $!)
    end

    if output =~ /^(\S+)/
      # output matches, so uninstall it
      portuninstall $1
    end
  end

  ######## Update/upgrade command

  def update
    Puppet.debug "portupgrade.update() - called on (#{@resource[:name]})"

    cmdline = ["-qO", @resource[:name]]
    begin
      output = portinfo(*cmdline)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new(output, $!)
    end

    if output =~ /^(\S+)/
      # output matches, so upgrade the software
      cmdline = ["-M BATCH=yes", $1]
      begin
        output = portupgrade(*cmdline)
      rescue Puppet::ExecutionFailure
        raise Puppet::Error.new(output, $!)
      end
    end
  end

  ## EOF
end
