require 'r10k/source'
require 'r10k/util/basedir'
require 'r10k/errors'
require 'set'

module R10K
  # A deployment models the entire state of the configuration that a Puppet
  # master can use. It contains a set of sources that can produce environments
  # and manages the contents of directories where environments are deployed.
  #
  # @api private
  class Deployment

    require 'r10k/deployment/environment'
    require 'r10k/deployment/basedir'
    require 'r10k/deployment/source'
    require 'r10k/deployment/config'

    # Generate a deployment object based on a config
    #
    # @param path [String] The path to the deployment config
    # @return [R10K::Deployment] The deployment loaded with the given config
    def self.load_config(path)
      config = R10K::Deployment::Config.new(path)
      new(config)
    end

    # @!attribute [r] config
    #   @return [R10K::Deployment::Config]
    attr_reader :config

    def initialize(config)
      @config = config
    end

    def preload!
      sources.each(&:preload!)
    end

    # @deprecated
    alias fetch_sources preload!

    # Lazily load all sources
    #
    # This instantiates the @_sources instance variable, but should not be
    # used directly as it could be legitimately unset if we're doing lazy
    # loading.
    #
    # @return [Array<R10K::Source::Base>] All repository sources
    #   specified in the config
    def sources
      load_sources if @_sources.nil?
      @_sources
    end

    # Lazily load all environments
    #
    # This instantiates the @_environments instance variable, but should not be
    # used directly as it could be legitimately unset if we're doing lazy
    # loading.
    #
    # @return [Array<R10K::Environment::Base>] All enviroments across
    #   all sources
    def environments
      load_environments if @_environments.nil?
      @_environments
    end

    # @return [Array<String>] The paths used by all contained sources
    def paths
      paths_and_sources.keys
    end

    # @return [Hash<String, Array<R10K::Source::Base>]
    def paths_and_sources
      pathmap = Hash.new { |h, k| h[k] = [] }
      sources.each { |source| pathmap[source.basedir] << source }
      pathmap
    end

    # Remove unmanaged content from all source paths
    def purge!
      paths_and_sources.each_pair do |path, sources_at_path|
        R10K::Util::Basedir.new(path, sources_at_path).purge!
      end
    end

    def validate!
      hash = {}
      sources.each do |source|
        source.environments.each do |environment|
          if hash.key?(environment.path)
            osource, oenvironment = hash[environment.path]
            msg = ""
            msg << "Environment collision at #{environment.path} between "
            msg << "#{source.name}:#{environment.name} and #{osource.name}:#{oenvironment.name}"
            raise R10K::R10KError, msg
          else
            hash[environment.path] = [source, environment]
          end
        end
      end
    end

    def accept(visitor)
      visitor.visit(:deployment, self) do
        sources.each do |env|
          env.accept(visitor)
        end
      end
    end

    private

    def load_sources
      sources = @config.setting(:sources)
      @_sources = sources.map do |(name, hash)|
        R10K::Source.from_hash(name, hash)
      end
    end

    def load_environments
      @_environments = []
      sources.each do |source|
        @_environments += source.environments
      end
    end
  end
end