text: require_relative "../constants"
  require_relative "../errors"
  require_relative "../helpers"
  
  module VagrantPlugins
    module Ansible
      module Provisioner
  
        # This class is a base class where the common functionality shared between
        # both Ansible provisioners are stored.
        # This is **not an actual provisioner**.
        # Instead, {Host} (ansible) or {Guest} (ansible_local) should be used.
  
        class Base < Vagrant.plugin("2", :provisioner)
  
          RANGE_PATTERN = %r{(?:\[[a-z]:[a-z]\]|\[[0-9]+?:[0-9]+?\])}.freeze
  
          ANSIBLE_PARAMETER_NAMES = {
            Ansible::COMPATIBILITY_MODE_V1_8 => {
              ansible_host: "ansible_ssh_host",
              ansible_password: "ansible_ssh_pass",
              ansible_port: "ansible_ssh_port",
              ansible_user: "ansible_ssh_user",
              ask_become_pass: "ask-sudo-pass",
              become: "sudo",
              become_user: "sudo-user",
            },
            Ansible::COMPATIBILITY_MODE_V2_0 => {
              ansible_host: "ansible_host",
              ansible_password: "ansible_password",
              ansible_port: "ansible_port",
              ansible_user: "ansible_user",
              ask_become_pass: "ask-become-pass",
              become: "become",
              become_user: "become-user",
            }
          }
  
          protected
  
          def initialize(machine, config)
            super
            @control_machine = nil
  
            @command_arguments = []
            @environment_variables = {}
            @inventory_machines = {}
            @inventory_path = nil
  
            @gathered_version_stdout = nil
            @gathered_version_major = nil
            @gathered_version = nil
          end
  
          def set_and_check_compatibility_mode
            begin
              set_gathered_ansible_version(gather_ansible_version)
            rescue StandardError => e
              # Nothing to do here, as the fallback on safe compatibility_mode is done below
              @logger.error("Error while gathering the ansible version: #{e.to_s}")
            end
  
            if @gathered_version_major
              if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO
                detect_compatibility_mode
              elsif @gathered_version_major.to_i < 2 && config.compatibility_mode == Ansible::COMPATIBILITY_MODE_V2_0
                # A better version comparator will be needed
                # when more compatibility modes come... but so far let's keep it simple!
                raise Ansible::Errors::AnsibleCompatibilityModeConflict,
                  ansible_version: @gathered_version,
                  system: @control_machine,
                  compatibility_mode: config.compatibility_mode
              end
            end
  
            if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO
              config.compatibility_mode = Ansible::SAFE_COMPATIBILITY_MODE
  
              @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected",
                compatibility_mode: config.compatibility_mode,
                gathered_version: @gathered_version_stdout) +
              "\n")
            end
  
            unless Ansible::COMPATIBILITY_MODES.slice(1..-1).include?(config.compatibility_mode)
              raise Ansible::Errors::AnsibleProgrammingError,
                message: "The config.compatibility_mode must be correctly set at this stage!",
                details: "config.compatibility_mode: '#{config.compatibility_mode}'"
            end
  
            @lexicon = ANSIBLE_PARAMETER_NAMES[config.compatibility_mode]
          end
  
          def check_files_existence
            check_path_is_a_file(config.playbook, :playbook)
  
            check_path_exists(config.inventory_path, :inventory_path) if config.inventory_path
            check_path_is_a_file(config.config_file, :config_file) if config.config_file
            check_path_is_a_file(config.extra_vars[1..-1], :extra_vars) if has_an_extra_vars_file_argument
            check_path_is_a_file(config.galaxy_role_file, :galaxy_role_file) if config.galaxy_role_file
            check_path_is_a_file(config.vault_password_file, :vault_password_file) if config.vault_password_file
          end
  
          def get_environment_variables_for_shell_execution
            shell_env_vars = []
            @environment_variables.each_pair do |k, v|
              if k =~ /ANSIBLE_SSH_ARGS|ANSIBLE_ROLES_PATH|ANSIBLE_CONFIG/
                shell_env_vars << "#{k}='#{v}'"
              else
                shell_env_vars << "#{k}=#{v}"
              end
            end
            shell_env_vars
          end
  
          def ansible_galaxy_command_for_shell_execution
            command_values = {
              role_file: "'#{get_galaxy_role_file}'",
              roles_path: "'#{get_galaxy_roles_path}'"
            }
  
            shell_command = get_environment_variables_for_shell_execution
  
            shell_command << config.galaxy_command % command_values
  
            shell_command.flatten.join(' ')
          end
  
          def ansible_playbook_command_for_shell_execution
            shell_command = get_environment_variables_for_shell_execution
  
            shell_command << config.playbook_command
  
            shell_args = []
            @command_arguments.each do |arg|
              if arg =~ /(--start-at-task|--limit)=(.+)/
                shell_args << %Q(#{$1}="#{$2}")
              elsif arg =~ /(--extra-vars)=(.+)/
                shell_args << %Q(%s=%s) % [$1, $2.shellescape]
              else
                shell_args << arg
              end
            end
  
            shell_command << shell_args
  
            # Add the raw arguments at the end, to give them the highest precedence
            shell_command << config.raw_arguments if config.raw_arguments
  
            shell_command << config.playbook
  
            shell_command.flatten.join(' ')
          end
  
          def prepare_common_command_arguments
            # By default we limit by the current machine,
            # but this can be overridden by the `limit` option.
            if config.limit
              @command_arguments << "--limit=#{Helpers::as_list_argument(config.limit)}"
            else
              @command_arguments << "--limit=#{@machine.name}"
            end
  
            @command_arguments << "--inventory-file=#{inventory_path}"
            @command_arguments << "--extra-vars=#{extra_vars_argument}" if config.extra_vars
            @command_arguments << "--#{@lexicon[:become]}" if config.become
            @command_arguments << "--#{@lexicon[:become_user]}=#{config.become_user}" if config.become_user
            @command_arguments << "#{verbosity_argument}" if verbosity_is_enabled?
            @command_arguments << "--vault-password-file=#{config.vault_password_file}" if config.vault_password_file
            @command_arguments << "--tags=#{Helpers::as_list_argument(config.tags)}" if config.tags
            @command_arguments << "--skip-tags=#{Helpers::as_list_argument(config.skip_tags)}" if config.skip_tags
            @command_arguments << "--start-at-task=#{config.start_at_task}" if config.start_at_task
          end
  
          def prepare_common_environment_variables
            # Ensure Ansible output isn't buffered so that we receive output
            # on a task-by-task basis.
            @environment_variables["PYTHONUNBUFFERED"] = 1
  
            # When Ansible output is piped in Vagrant integration, its default colorization is
            # automatically disabled and the only way to re-enable colors is to use ANSIBLE_FORCE_COLOR.
            @environment_variables["ANSIBLE_FORCE_COLOR"] = "true" if @machine.env.ui.color?
            # Setting ANSIBLE_NOCOLOR is "unnecessary" at the moment, but this could change in the future
            # (e.g. local provisioner [GH-2103], possible change in vagrant/ansible integration, etc.)
            @environment_variables["ANSIBLE_NOCOLOR"] = "true" if !@machine.env.ui.color?
  
            # Use ANSIBLE_ROLES_PATH to tell ansible-playbook where to look for roles
            # (there is no equivalent command line argument in ansible-playbook)
            @environment_variables["ANSIBLE_ROLES_PATH"] = get_galaxy_roles_path if config.galaxy_roles_path
  
            prepare_ansible_config_environment_variable
          end
  
          def prepare_ansible_config_environment_variable
            @environment_variables["ANSIBLE_CONFIG"] = config.config_file if config.config_file
          end
  
          # Auto-generate "safe" inventory file based on Vagrantfile,
          # unless inventory_path is explicitly provided
          def inventory_path
            if config.inventory_path
              config.inventory_path
            else
              @inventory_path ||= generate_inventory
            end
          end
  
          def get_inventory_host_vars_string(machine_name)
            # In Ruby, Symbol and String values are different, but
            # Vagrant has to unify them for better user experience.
            vars = config.host_vars[machine_name.to_sym]
            if !vars
              vars = config.host_vars[machine_name.to_s]
            end
            s = nil
            if vars.is_a?(Hash)
              s = vars.each.collect {
                |k, v|
                  if v.is_a?(String) && v.include?(' ') && !v.match(/^('|")[^'"]+('|")$/)
                    v = %Q('#{v}')
                  end
                  "#{k}=#{v}"
                }.join(" ")
            elsif vars.is_a?(Array)
              s = vars.join(" ")
            elsif vars.is_a?(String)
              s = vars
            end
            if s and !s.empty? then s else nil end
          end
  
          def generate_inventory
            inventory = "# Generated by Vagrant\n\n"
  
            # This "abstract" step must fill the @inventory_machines list
            # and return the list of supported host(s)
            inventory += generate_inventory_machines
  
            inventory += generate_inventory_groups
  
            # This "abstract" step must create the inventory file and
            # return its location path
            # TODO: explain possible race conditions, etc.
            @inventory_path = ship_generated_inventory(inventory)
          end
  
          # Write out groups information.
          # All defined groups will be included, but only supported
          # machines and defined child groups will be included.
          def generate_inventory_groups
            groups_of_groups = {}
            defined_groups = []
            group_vars = {}
            inventory_groups = ""
  
            # Verify if host range patterns exist and warn
            if config.groups.any? { |gm| gm.to_s[RANGE_PATTERN] }
              @machine.ui.warn(I18n.t("vagrant.provisioners.ansible.ansible_host_pattern_detected"))
            end
  
            config.groups.each_pair do |gname, gmembers|
              if gname.is_a?(Symbol)
                gname = gname.to_s
              end
  
              if gmembers.is_a?(String)
                gmembers = gmembers.split(/\s+/)
              elsif gmembers.is_a?(Hash)
                gmembers = gmembers.each.collect{ |k, v| "#{k}=#{v}" }
              elsif !gmembers.is_a?(Array)
                gmembers = []
              end
  
              if gname.end_with?(":children")
                groups_of_groups[gname] = gmembers
                defined_groups << gname.sub(/:children$/, '')
              elsif gname.end_with?(":vars")
                group_vars[gname] = gmembers
              else
                defined_groups << gname
                inventory_groups += "\n[#{gname}]\n"
                gmembers.each do |gm|
                  # TODO : Expand and validate host range patterns
                  # against @inventory_machines list before adding them
                  # otherwise abort with an error message
                  if gm[RANGE_PATTERN]
                    inventory_groups += "#{gm}\n"
                  end
                  inventory_groups += "#{gm}\n" if @inventory_machines.include?(gm.to_sym)
                end
              end
            end
  
            defined_groups.uniq!
            groups_of_groups.each_pair do |gname, gmembers|
              inventory_groups += "\n[#{gname}]\n"
              gmembers.each do |gm|
                inventory_groups += "#{gm}\n" if defined_groups.include?(gm)
              end
            end
  
            group_vars.each_pair do |gname, gmembers|
              if defined_groups.include?(gname.sub(/:vars$/, "")) || gname == "all:vars"
                inventory_groups += "\n[#{gname}]\n" + gmembers.join("\n") + "\n"
              end
            end
  
            return inventory_groups
          end
  
          def has_an_extra_vars_file_argument
            config.extra_vars && config.extra_vars.kind_of?(String) && config.extra_vars =~ /^@.+$/
          end
  
          def extra_vars_argument
            if has_an_extra_vars_file_argument
              # A JSON or YAML file is referenced.
              config.extra_vars
            else
              # Expected to be a Hash after config validation.
              config.extra_vars.to_json
            end
          end
  
          def get_galaxy_role_file
            Helpers::expand_path_in_unix_style(config.galaxy_role_file, get_provisioning_working_directory)
          end
  
          def get_galaxy_roles_path
            base_dir = get_provisioning_working_directory
            if config.galaxy_roles_path
              Helpers::expand_path_in_unix_style(config.galaxy_roles_path, base_dir)
            else
              playbook_path = Helpers::expand_path_in_unix_style(config.playbook, base_dir)
              File.join(Pathname.new(playbook_path).parent, 'roles')
            end
          end
  
          def ui_running_ansible_command(name, command)
            @machine.ui.detail I18n.t("vagrant.provisioners.ansible.running_#{name}")
            if verbosity_is_enabled?
              # Show the ansible command in use
              @machine.env.ui.detail command
            end
          end
  
          def verbosity_is_enabled?
            config.verbose && !config.verbose.to_s.empty?
          end
  
          def verbosity_argument
            if config.verbose.to_s =~ /^-?(v+)$/
              "-#{$+}"
            else
              # safe default, in case input strays
              '-v'
            end
          end
  
          private
  
          def detect_compatibility_mode
            if !@gathered_version_major || config.compatibility_mode != Ansible::COMPATIBILITY_MODE_AUTO
              raise Ansible::Errors::AnsibleProgrammingError,
                message: "The detect_compatibility_mode() function shouldn't have been called!",
                details: %Q(config.compatibility_mode: '#{config.compatibility_mode}'
  gathered version major number: '#{@gathered_version_major}'
  gathered version stdout version:
  #{@gathered_version_stdout})
            end
  
            if @gathered_version_major.to_i <= 1
              config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V1_8
            else
              config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V2_0
            end
  
            @logger.info(I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning",
              compatibility_mode: config.compatibility_mode,
              ansible_version: @gathered_version) +
            "\n")
          end
  
          def set_gathered_ansible_version(stdout_output)
            @gathered_version_stdout = stdout_output
            if !@gathered_version_stdout.empty?
              first_line = @gathered_version_stdout.lines[0]
              ansible_version_pattern = first_line.match(/(^ansible\s+)(.+)$/)
              if ansible_version_pattern
                _, @gathered_version, _ = ansible_version_pattern.captures
                if @gathered_version
                  @gathered_version_major = @gathered_version.match(/^(\d)\..+$/).captures[0].to_i
                end
              end
            end
          end
  
        end
      end
    end
  end
  