require 'r10k/util/setopts'
require 'r10k/deployment'
require 'r10k/logging'
require 'r10k/action/visitor'
require 'r10k/action/base'
require 'r10k/action/deploy/deploy_helpers'
require 'json'

module R10K
  module Action
    module Deploy
      class Environment < R10K::Action::Base

        include R10K::Action::Deploy::DeployHelpers

        attr_reader :force

        def initialize(opts, argv, settings = nil)
          settings ||= {}
          @purge_levels = settings.fetch(:deploy, {}).fetch(:purge_levels, [])
          @user_purge_whitelist = settings.fetch(:deploy, {}).fetch(:purge_whitelist, [])
          @generate_types = settings.fetch(:deploy, {}).fetch(:generate_types, false)

          super

          # @force here is used to make it easier to reason about
          @force = !@no_force
          @argv = @argv.map { |arg| arg.gsub(/\W/,'_') }
        end

        def call
          @visit_ok = true

          expect_config!
          deployment = R10K::Deployment.new(@settings)
          check_write_lock!(@settings)

          deployment.accept(self)
          @visit_ok
        end

        include R10K::Action::Visitor

        private

        def visit_deployment(deployment)
          # Ensure that everything can be preloaded. If we cannot preload all
          # sources then we can't fully enumerate all environments which
          # could be dangerous. If this fails then an exception will be raised
          # and execution will be halted.
          deployment.preload!
          deployment.validate!

          undeployable = undeployable_environment_names(deployment.environments, @argv)
          if !undeployable.empty?
            @visit_ok = false
            logger.error _("Environment(s) \'%{environments}\' cannot be found in any source and will not be deployed.") % {environments: undeployable.join(", ")}
          end

          yield

          if @purge_levels.include?(:deployment)
            logger.debug("Purging unmanaged environments for deployment...")
            deployment.purge!
          end
        ensure
          if (postcmd = @settings[:postrun])
            if postcmd.grep('$modifiedenvs').any?
              envs = deployment.environments.map { |e| e.dirname }
              envs.reject! { |e| !@argv.include?(e) } if @argv.any?
              postcmd = postcmd.map { |e| e.gsub('$modifiedenvs', envs.join(' ')) }
            end
            subproc = R10K::Util::Subprocess.new(postcmd)
            subproc.logger = logger
            subproc.execute
          end
        end

        def visit_source(source)
          yield
        end

        def visit_environment(environment)
          if !(@argv.empty? || @argv.any? { |name| environment.dirname == name })
            logger.debug1(_("Environment %{env_dir} does not match environment name filter, skipping") % {env_dir: environment.dirname})
            return
          end

          started_at = Time.new
          @environment_ok = true

          status = environment.status
          logger.info _("Deploying environment %{env_path}") % {env_path: environment.path}

          environment.sync
          logger.info _("Environment %{env_dir} is now at %{env_signature}") % {env_dir: environment.dirname, env_signature: environment.signature}

          if status == :absent || @puppetfile
            if status == :absent
              logger.debug(_("Environment %{env_dir} is new, updating all modules") % {env_dir: environment.dirname})
            end

            previous_ok = @visit_ok
            @visit_ok = true
            yield
            @environment_ok = @visit_ok
            @visit_ok &&= previous_ok
          end

          if @purge_levels.include?(:environment)
            if @visit_ok
              logger.debug("Purging unmanaged content for environment '#{environment.dirname}'...")
              environment.purge!(:recurse => true, :whitelist => environment.whitelist(@user_purge_whitelist))
            else
              logger.debug("Not purging unmanaged content for environment '#{environment.dirname}' due to prior deploy failures.")
            end
          end

          if @generate_types
            if @environment_ok
              logger.debug("Generating puppet types for environment '#{environment.dirname}'...")
              environment.generate_types!
            else
              logger.debug("Not generating puppet types for environment '#{environment.dirname}' due to puppetfile failures.")
            end
          end

          write_environment_info!(environment, started_at, @visit_ok)
        end

        def visit_puppetfile(puppetfile)
          puppetfile.load(@opts[:'default-branch-override'])

          yield

          if @purge_levels.include?(:puppetfile)
            logger.debug("Purging unmanaged Puppetfile content for environment '#{puppetfile.environment.dirname}'...")
            puppetfile.purge!
          end
        end

        def visit_module(mod)
          logger.info _("Deploying %{origin} content %{path}") % {origin: mod.origin, path: mod.path}
          mod.sync(force: @force)
        end

        def write_environment_info!(environment, started_at, success)
          module_deploys = []
          begin
            environment.modules.each do |mod|
              name = mod.name
              version = mod.version
              sha = mod.repo.head rescue nil
              module_deploys.push({:name => name, :version => version, :sha => sha})
            end
          rescue
            logger.debug("Unable to get environment module deploy data for .r10k-deploy.json at #{environment.path}")
          end

          # make this file write as atomic as possible in pure ruby
          final   = "#{environment.path}/.r10k-deploy.json"
          staging = "#{environment.path}/.r10k-deploy.json~"
          File.open(staging, 'w') do |f|
            deploy_info = environment.info.merge({
              :started_at => started_at,
              :finished_at => Time.new,
              :deploy_success => success,
              :module_deploys => module_deploys,
            })

            f.puts(JSON.pretty_generate(deploy_info))
          end
          FileUtils.mv(staging, final)
        end

        def undeployable_environment_names(environments, expected_names)
          if expected_names.empty?
            []
          else
            known_names = environments.map(&:dirname)
            expected_names - known_names
          end
        end

        def allowed_initialize_opts
          super.merge(puppetfile: :self,
                      cachedir: :self,
                      'no-force': :self,
                      'generate-types': :self,
                      'puppet-path': :self,
                      'default-branch-override': :self)
        end
      end
    end
  end
end