# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require "log4r"

require_relative "mixin_provisioners"

module Vagrant
  module Action
    module Builtin
      # This class will run the configured provisioners against the
      # machine.
      #
      # This action should be placed BEFORE the machine is booted so it
      # can do some setup, and then run again (on the return path) against
      # a running machine.
      class Provision
        include MixinProvisioners

        def initialize(app, env)
          @app             = app
          @logger          = Log4r::Logger.new("vagrant::action::builtin::provision")
        end

        def call(env)
          @env = env

          # Tracks whether we were configured to provision
          config_enabled = true
          config_enabled = env[:provision_enabled] if env.key?(:provision_enabled)

          # Check if we already provisioned, and if so, disable the rest
          provision_enabled = true

          ignore_sentinel = true
          if env.key?(:provision_ignore_sentinel)
            ignore_sentinel = env[:provision_ignore_sentinel]
          end
          if ignore_sentinel
            @logger.info("Ignoring sentinel check, forcing provision")
          end

          @logger.info("Checking provisioner sentinel file...")
          sentinel_path = env[:machine].data_dir.join("action_provision")
          update_sentinel = false
          if sentinel_path.file?
            # The sentinel file is in the format of "version:data" so that
            # we can remain backwards compatible with previous sentinels.
            # Versions so far:
            #
            #   Vagrant < 1.5.0: A timestamp. The weakness here was that
            #     if it wasn't cleaned up, it would incorrectly not provision
            #     new machines.
            #
            #   Vagrant >= 1.5.0: "1.5:ID", where ID is the machine ID.
            #     We compare both so we know whether it is a new machine.
            #
            contents = sentinel_path.read.chomp
            parts    = contents.split(":", 2)

            if parts.length == 1
              @logger.info("Old-style sentinel found! Not provisioning.")
              provision_enabled = false if !ignore_sentinel
              update_sentinel = true
            elsif parts[0] == "1.5" && parts[1] == env[:machine].id.to_s
              @logger.info("Sentinel found! Not provisioning.")
              provision_enabled = false if !ignore_sentinel
            else
              @logger.info("Sentinel found with another machine ID. Removing.")
              sentinel_path.unlink
            end
          end

          # Store the value so that other actions can use it
          env[:provision_enabled] = provision_enabled if !env.key?(:provision_enabled)

          # Ask the provisioners to modify the configuration if needed
          provisioner_instances(env).each do |p, _|
            p.configure(env[:machine].config)
          end

          # Continue, we need the VM to be booted.
          @app.call(env)

          # If we're configured to not provision, notify the user and stop
          if !config_enabled
            env[:ui].info(I18n.t("vagrant.actions.vm.provision.disabled_by_config"))
            return
          end

          # If we're not provisioning because of the sentinel, tell the user
          # but continue trying for the "always" provisioners
          if !provision_enabled
            env[:ui].info(I18n.t("vagrant.actions.vm.provision.disabled_by_sentinel"))
          end

          # Write the sentinel if we have to
          if update_sentinel || !sentinel_path.file?
            @logger.info("Writing provisioning sentinel so we don't provision again")
            sentinel_path.open("w") do |f|
              f.write("1.5:#{env[:machine].id}")
            end
          end

          type_map = provisioner_type_map(env)
          provisioner_instances(env).each do |p, options|
            type_name = type_map[p]

            if options[:run] == :never
              next if env[:provision_types].nil? || !env[:provision_types].include?(options[:name])
            else
              next if env[:provision_types] && \
                !env[:provision_types].include?(type_name) && \
                !env[:provision_types].include?(options[:name])

              # Don't run if sentinel is around and we're not always running
              next if !provision_enabled && options[:run] != :always
            end

            name = type_name
            if options[:name]
              name = "#{options[:name]} (#{type_name})"
            end

            env[:ui].info(I18n.t(
              "vagrant.actions.vm.provision.beginning",
              provisioner: name))

            env[:hook].call(:provisioner_run, env.merge(
              callable: method(:run_provisioner),
              provisioner: p,
              provisioner_name: type_name,
            ))
          end
        end

        # This is pulled out into a separate method so that users can
        # subclass and implement custom behavior if they'd like to work around
        # this step.
        def run_provisioner(env)
          env[:provisioner].provision
        end
      end
    end
  end
end
