# frozen_string_literal: true

module Humidifier
  class Config
    # The parent class for mapper classes. These classes are used to transform
    # arbitrary attributes coming from the user-provided YAML files into valid
    # CloudFormation props that can then be used in the template. This class
    # provides an easy-to-extend DSL that allows for default attributes
    # specifying custom attributes.
    class Mapper
      # Raised when the attribute given in the file could not be matched to an
      # attribute in the mapper.
      class InvalidResourceAttributeError < Error
        def initialize(clazz, key)
          super("Invalid attribute name given for #{clazz.aws_name}: #{key}")
        end
      end

      # The list of attributes that are common to all resources that need to be
      # handled separately.
      COMMON_ATTRIBUTES = Resource::COMMON_ATTRIBUTES.values

      class << self
        # Defines a custom attribute. The given block will receive the
        # user-provided value for the attribute. The block should return a hash
        # where the keys are valid humidifier properties and the values are
        # valid values for those properties. In the below example, we specify
        # the group attribute which maps to the groups attribute after some
        # transformation.
        #
        #     attribute :group do |group|
        #       groups = GROUPS[group]
        #       groups.any? ? { groups: GROUPS[group] } : {}
        #     end
        def attribute(name, &block)
          define_method(:"attribute_#{name}", &block)
          attribute_methods << name
        end

        # The names of the custom attribute methods.
        def attribute_methods
          @attribute_methods ||= []
        end

        # Defines the default attributes that should be applied to all resources
        # of this type. The given block will be passed the logical resource
        # name that the user specified for the resource. The block should return
        # a hash where the keys are valid humidifier properties and the values
        # are valid values for those properties. In the example below, the
        # user_name property is set based on the logical name.
        #
        #     defaults do |name|
        #       { user_name: name }
        #     end
        def defaults(&block)
          define_method(:attribute_defaults, &block)
        end
      end

      # Builds a humidifier resource using the given humidifier resource class,
      # the logical name for the resource, and the user-specified attributes.
      def resource_for(clazz, name, attributes)
        mapped =
          respond_to?(:attribute_defaults) ? attribute_defaults(name) : {}

        attributes.each do |key, value|
          mapped.merge!(mapped_from(clazz, key, value))
        end

        common_attributes = common_attributes_from(mapped)

        resource = clazz.new(mapped)
        resource.update_attributes(common_attributes)
        resource
      end

      private

      def common_attributes_from(mapped)
        COMMON_ATTRIBUTES.each_with_object({}) do |common_attribute, extract|
          extracted = mapped.delete(common_attribute)
          extract[common_attribute] = extracted if extracted
        end
      end

      def mapped_from(clazz, key, value)
        if self.class.attribute_methods.include?(key.to_sym)
          # The given attribute name has been defined using the `::attribute`
          # DSL method, so send the given value to that method and return the
          # resulting hash.
          public_send(:"attribute_#{key}", value)
        elsif clazz.prop?(key)
          # The given attribute name is a valid property on the resource, so
          # directly map the attribute to the given value.
          { key.to_sym => value }
        elsif clazz.prop?("#{key}_id")
          # The given attribute name corresponds to a property on the resource
          # that takes the ID of another resource (for example, specifying the
          # vpc option in the file when the resource has a vpc_id property). In
          # this case, automatically convert the given value into a
          # CloudFormation reference.
          { "#{key}_id": Humidifier.ref(value) }
        elsif COMMON_ATTRIBUTES.include?(key.to_sym)
          # The given attribute name is one of the attributes common to all
          # resources (for example creation_policy), so map that directly to the
          # given value.
          { key.to_sym => value }
        else
          # The given attribute name did not match one of the valid options, so
          # raise an error to alert the user.
          raise InvalidResourceAttributeError.new(clazz, key)
        end
      end
    end
  end
end
