require 'puppet/util/inifile'

Puppet::Type.type(:yumrepo).provide(:inifile) do
  desc <<-EOD
    Manage yum repo configurations by parsing yum INI configuration files.

    ### Fetching instances

    When fetching repo instances, directory entries in '/etc/yum/repos.d',
    '/etc/yum.repos.d', and the directory optionally specified by the reposdir
    key in '/etc/yum.conf' will be checked. If a given directory does not exist it
    will be ignored. In addition, all sections in '/etc/yum.conf' aside from
    'main' will be created as sections.

    ### Storing instances

    When creating a new repository, a new section will be added in the first
    yum repo directory that exists. The custom directory specified by the
    '/etc/yum.conf' reposdir property is checked first, followed by
    '/etc/yum/repos.d', and then '/etc/yum.repos.d'. If none of these exist, the
    section will be created in '/etc/yum.conf'.
  EOD

  PROPERTIES = Puppet::Type.type(:yumrepo).validproperties

  # Retrieve all providers based on existing yum repositories
  #
  # @api public
  # @return [Array<Puppet::Provider>] providers generated from existing yum
  #   repository definitions.
  def self.instances
    instances = []

    virtual_inifile.each_section do |section|
      # Ignore the 'main' section in yum.conf since it's not a repository.
      next if section.name == "main"

      attributes_hash = {:name => section.name, :ensure => :present, :provider => :yumrepo}

      section.entries.each do |key, value|
        key = key.to_sym
        if valid_property?(key)
          attributes_hash[key] = value
        elsif key == :name
          attributes_hash[:descr] = value
        end
      end
      instances << new(attributes_hash)
    end

    instances
  end

  # Match catalog type instances to provider instances.
  #
  # @api public
  # @param resources [Array<Puppet::Type::Yumrepo>] Resources to prefetch.
  # @return [void]
  def self.prefetch(resources)
    repos = instances
    resources.each_key do |name|
      if provider = repos.find { |repo| repo.name == name }
        resources[name].provider = provider
      end
    end
  end

  # Return a list of existing directories that could contain repo files.
  #
  # @api private
  # @param conf [String] Configuration file to look for directories in.
  # @param dirs [Array<String>] Default locations for yum repos.
  # @return [Array<String>] All present directories that may contain yum repo configs.
  def self.reposdir(conf='/etc/yum.conf', dirs=['/etc/yum.repos.d', '/etc/yum/repos.d'])
    reposdir = find_conf_value('reposdir', conf)
    # Use directories in reposdir if they are set instead of default
    if reposdir
      # Follow the code from the yum/config.py
      dirs = reposdir.gsub!("\n", ' ')
      dirs = reposdir.gsub!(',', ' ')
      dirs = reposdir.split
    end
    dirs.select! { |dir| Puppet::FileSystem.exist?(dir) }
    if dirs.empty?
      Puppet.debug('No yum directories were found on the local filesystem')
    end

    dirs
  end

  # Used for testing only
  # @api private
  def self.clear
    @virtual = nil
  end

  # Helper method to look up specific values in ini style files.
  #
  # @api private
  # @param value [String] Value to look for in the configuration file.
  # @param conf [String] Configuration file to check for value.
  # @return [String] The value of a looked up key from the configuration file.
  def self.find_conf_value(value, conf='/etc/yum.conf')
    if Puppet::FileSystem.exist?(conf)
      file = Puppet::Util::IniConfig::PhysicalFile.new(conf)
      file.read
      if (main = file.get_section('main'))
        main[value]
      end
    end
  end

  # Enumerate all files that may contain yum repository configs.
  # '/etc/yum.conf' is always included.
  #
  # @api private
  # @return [Array<String>
  def self.repofiles
    files = ["/etc/yum.conf"]
    reposdir.each do |dir|
      Dir.glob("#{dir}/*.repo").each do |file|
        files << file
      end
    end

    files
  end

  # Build a virtual inifile by reading in numerous .repo files into a single
  # virtual file to ease manipulation.
  # @api private
  # @return [Puppet::Util::IniConfig::File] The virtual inifile representing
  #   multiple real files.
  def self.virtual_inifile
    unless @virtual
      @virtual = Puppet::Util::IniConfig::File.new
      self.repofiles.each do |file|
        @virtual.read(file) if Puppet::FileSystem.file?(file)
      end
    end
    return @virtual
  end

  # Is the given key a valid type property?
  #
  # @api private
  # @param key [String] The property to look up.
  # @return [Boolean] Returns true if the property is defined in the type.
  def self.valid_property?(key)
    PROPERTIES.include?(key)
  end

  # Return an existing INI section or create a new section in the default location
  #
  # The default location is determined based on what yum repo directories
  # and files are present. If /etc/yum.conf has a value for 'reposdir' then that
  # is preferred. If no such INI property is found then the first default yum
  # repo directory that is present is used. If no default directories exist then
  # /etc/yum.conf is used.
  #
  # @param name [String] Section name to lookup in the virtual inifile.
  # @return [Puppet::Util::IniConfig] The IniConfig section
  def self.section(name)
    result = self.virtual_inifile[name]
    # Create a new section if not found.
    unless result
      dirs = reposdir()
      if dirs.empty?
        # If no repo directories are present, default to using yum.conf.
        path = '/etc/yum.conf'
      else
        # The ordering of reposdir is [defaults, custom], and we want to use
        # the custom directory if present.
        path = File.join(dirs.last, "#{name}.repo")
      end
      result = self.virtual_inifile.add_section(name, path)
    end
    result
  end

  # Save all yum repository files and force the mode to 0644
  # @api private
  # @return [void]
  def self.store
    inifile = self.virtual_inifile
    inifile.store

    target_mode = 0644
    inifile.each_file do |file|
      current_mode = Puppet::FileSystem.stat(file).mode & 0777
      unless current_mode == target_mode
        Puppet.info "changing mode of #{file} from %03o to %03o" % [current_mode, target_mode]
        Puppet::FileSystem.chmod(target_mode, file)
      end
    end
  end

  # Create a new section for the given repository and set all the specified
  # properties in the section.
  #
  # @api public
  # @return [void]
  def create
    @property_hash[:ensure] = :present

    new_section = current_section

    # We fetch a list of properties from the type, then iterate
    # over them, avoiding ensure.  We're relying on .should to
    # check if the property has been set and should be modified,
    # and if so we set it in the virtual inifile.
    PROPERTIES.each do |property|
      next if property == :ensure

      if value = @resource.should(property)
        self.send("#{property}=", value)
      end
    end
  end

  # Does the given repository already exist?
  #
  # @api public
  # @return [Boolean]
  def exists?
    @property_hash[:ensure] == :present
  end

  # Mark the given repository section for destruction.
  #
  # The actual removal of the section will be handled by {#flush} after the
  # resource has been fully evaluated.
  #
  # @api public
  # @return [void]
  def destroy
    # Flag file for deletion on flush.
    current_section.destroy=(true)

    @property_hash.clear
  end

  # Finalize the application of the given resource.
  #
  # @api public
  # @return [void]
  def flush
    self.class.store
  end

  # Generate setters and getters for our INI properties.
  PROPERTIES.each do |property|
    # The ensure property uses #create, #exists, and #destroy we can't generate
    # meaningful setters and getters for this
    next if property == :ensure

    define_method(property) do
      get_property(property)
    end

    define_method("#{property}=") do |value|
      set_property(property, value)
    end
  end

  # Map the yumrepo 'descr' type property to the 'name' INI property.
  def descr
    if ! @property_hash.has_key?(:descr)
      @property_hash[:descr] = current_section['name']
    end
    value = @property_hash[:descr]
    value.nil? ? :absent : value
  end

  def descr=(value)
    value = (value == :absent ? nil : value)
    current_section['name'] = value
    @property_hash[:descr] = value
  end

  private

  def get_property(property)
    if ! @property_hash.has_key?(property)
      @property_hash[property] = current_section[property.to_s]
    end
    value = @property_hash[property]
    value.nil? ? :absent : value
  end

  def set_property(property, value)
    value = (value == :absent ? nil : value)
    current_section[property.to_s] = value
    @property_hash[property] = value
  end

  def section(name)
    self.class.section(name)
  end

  def current_section
    self.class.section(self.name)
  end
end
