# frozen_string_literal: true

module Bolt
  class Inventory
    # This class represents the active state of a target within the inventory.
    class Target
      attr_reader :name, :uri, :safe_name, :target_alias

      def initialize(target_data, inventory)
        unless target_data['name'] || target_data['uri']
          raise Bolt::Inventory::ValidationError.new("Target must have either a name or uri", nil)
        end

        @logger = Logging.logger[inventory]

        # If the target isn't mentioned by any groups, it won't have a uri or
        # name and we will use the target_name as both
        @uri = target_data['uri']
        @uri_obj = self.class.parse_uri(@uri)

        # If the target has a name, use that as the safe name. Otherwise, turn
        # the uri into a safe name by omitting the password.
        if target_data['name']
          @name = target_data['name']
          @safe_name = target_data['name']
        else
          @name = @uri
          @safe_name = @uri_obj.omit(:password).to_str.sub(%r{^//}, '')
        end

        @config = target_data['config'] || {}
        @vars = target_data['vars'] || {}
        @facts = target_data['facts'] || {}
        @features = target_data['features'] || Set.new
        @options = target_data['options'] || {}
        @plugin_hooks = target_data['plugin_hooks'] || {}
        # When alias is specified in a plan, the key will be `target_alias`, when
        # alias is specified in inventory the key will be `alias`.
        @target_alias = target_data['target_alias'] || target_data['alias'] || []

        @inventory = inventory

        validate
      end

      def vars
        group_cache['vars'].merge(@vars)
      end

      # This method isn't actually an accessor and we want the name to
      # correspond to the Puppet function
      # rubocop:disable Naming/AccessorMethodName
      def set_var(var_hash)
        @vars.merge!(var_hash)
      end
      # rubocop:enable Naming/AccessorMethodName

      def facts
        Bolt::Util.deep_merge(group_cache['facts'], @facts)
      end

      def add_facts(new_facts = {})
        @facts = Bolt::Util.deep_merge(@facts, new_facts)
      end

      def features
        group_cache['features'] + @features
      end

      def set_feature(feature, value = true)
        if value
          @features << feature
        else
          @features.delete(feature)
        end
      end

      def plugin_hooks
        # Merge plugin_hooks from the config file with any defined by the group
        # or assigned dynamically to the target
        @inventory.config.plugin_hooks.merge(group_cache['plugin_hooks']).merge(@plugin_hooks)
      end

      def set_config(key_or_key_path, value)
        if key_or_key_path.empty?
          @config = value
        else
          *path, key = Array(key_or_key_path)
          location = path.inject(@config) do |working_object, p|
            working_object[p] ||= {}
          end
          location[key] = value
        end
        invalidate_config_cache!
      end

      def invalidate_group_cache!
        @group_cache = nil
        # The config cache depends on the group cache, so invalidate it as well
        invalidate_config_cache!
      end

      def invalidate_config_cache!
        @transport_config_cache = nil
      end

      # Computing the transport config is expensive as it requires cloning the
      # base config, so we cache the effective config
      def transport_config_cache
        if @transport_config_cache.nil?
          merged_config = Bolt::Util.deep_merge(group_cache['config'], @config)
          # Use the base config to ensure we handle the config validation and
          # munging correctly
          config = @inventory.config.deep_clone
          config.update_from_inventory(merged_config)
          config.validate
          @transport_config_cache = {
            'transport' => config.transport_conf[:transport],
            'transports' => config.transport_conf[:transports]
          }
        end

        @transport_config_cache
      end

      # Validate the target. This implicitly also primes the group and config
      # caches and resolves any config references in the target's groups.
      def validate
        unless name.ascii_only?
          raise Bolt::Inventory::ValidationError.new("Target name must be ASCII characters: #{@name}", nil)
        end

        unless transport.nil? || Bolt::TRANSPORTS.include?(transport.to_sym)
          raise Bolt::UnknownTransportError.new(transport, uri)
        end
      end

      def host
        @uri_obj.hostname || transport_config['host']
      end

      def port
        @uri_obj.port || transport_config['port']
      end

      def protocol
        transport
      end

      def transport
        @uri_obj.scheme || transport_config_cache['transport']
      end

      def user
        Addressable::URI.unencode_component(@uri_obj.user) || transport_config['user']
      end

      def password
        Addressable::URI.unencode_component(@uri_obj.password) || transport_config['password']
      end

      def options
        transport_config.dup
      end

      # We only want to look up transport config keys for the configured
      # transport
      def transport_config
        transport_config_cache['transports'][transport.to_sym]
      end

      def config
        Bolt::Util.deep_merge(group_cache['config'], @config)
      end

      def group_cache
        if @group_cache.nil?
          group_data = @inventory.group_data_for(@name)

          unless group_data && group_data['config']
            @logger.debug("Did not find config for #{self} in inventory")
          end

          group_data ||= {
            'config' => {},
            'vars' => {},
            'facts' => {},
            'features' => Set.new,
            'options' => {},
            'plugin_hooks' => {},
            'target_alias' => []
          }

          # This should be handled by `get_targets`
          if @name == 'localhost'
            group_data = Bolt::Inventory.localhost_defaults(group_data)
          end

          @group_cache = group_data
        end

        @group_cache
      end

      def to_s
        @safe_name
      end

      def self.parse_uri(string)
        require 'addressable/uri'
        if string.nil?
          Addressable::URI.new
        # Forbid empty uri
        elsif string.empty?
          raise Bolt::ParseError, "Could not parse target URI: URI is empty string"
        elsif string =~ %r{^[^:]+://}
          Addressable::URI.parse(string)
        else
          # Initialize with an empty scheme to ensure we parse the hostname correctly
          Addressable::URI.parse("//#{string}")
        end
      rescue Addressable::URI::InvalidURIError => e
        raise Bolt::ParseError, "Could not parse target URI: #{e.message}"
      end
    end
  end
end