# frozen_string_literal: true

require 'litany/mixins'
require 'litany/secret_property'

module Litany
  # Methods that are used to help define resources
  module ResourceDSL

    module InstanceMethods
      extend Delegator

      delegate_to proc { project }, :settings, singleton: false
      delegate_to proc { project.settings }, :disable, :enable, :fetch, :set, :set?, singleton: false

      def inside?(*filters)
        filters = filters.flatten
        filters.include?(project.stage) || filters.include?(active_environment&.name)
      end

      def outside?(*filters)
        !inside?(*filters)
      end

      def inside(*filters)
        yield if inside?(*filters)
      end

      def outside(*filters)
        yield if outside?(*filters)
      end
    end

    class << self
      def extended(target)
        target.include(InstanceMethods)
      end
    end

    # @!visibility private
    # Helper to determine if a value is valid for a given property.
    # @param target [Object] accepts any type of attribute to compare the `value` against.
    # @param value [Object] the `value` to be validated, exact behavior depends on the type of`target`.
    def _valid_value?(target, value)
      value = value.to_json if value.is_a?(ResourceReference) || value.is_a?(SecretPropertyValue)

      case target
        when Array, Set
          target.collect { |t| _valid_value?(t, value) }.any?
        when Class, Module
          value.is_a? target
        when nil, NilClass
          value.nil?
        when Range
          target.include? value
        when Regexp
          if value.is_a?(String) || value.is_a?(Symbol)
            target =~ value
          else
            target == value
          end
        when Proc
          _valid_value?(target.call, value)
        else
          target == value
      end
    end

    # @!visibility private
    # Just like {_valid_value?} but it accepts an Array of values to validate, all values must validate to return `true`.
    # @param (see #_valid_value?)
    def _valid_values?(target, values)
      values.all? { |value| _valid_value?(target, value) }
    end

    def automatic_children_resources
      @automatic_children_resources ||= Set.new
    end

    def cfn_type(type)
      raise 'You may only set cfn_type on a subclass of Resource' unless subclass_of? Resource
      define_method(:cfn_type) { type }
    end

    def child_resource(klass, name, automatic: true)
      raise "Received #{klass.inspect} instead of a child class of MetaResource." unless klass.subclass_of? MetaResource

      is_set_name = :"set_#{name}?"
      ivar_name = :"@#{name}"

      # Prevent dev error of re-declaring properties
      raise "Duplicate property named `#{name}` defined on #{inspect}." if properties.include? name
      properties << name
      automatic_children_resources << name if automatic == true

      # Helpers
      define_method(is_set_name) { instance_variable_defined? ivar_name }

      # Getter
      define_method(name) do |&block|
        if send(is_set_name)
          resource = instance_variable_get(ivar_name)
        else
          resource_name = klass.child_resource_name(self, send(:name))
          resource = instance_variable_set(ivar_name, klass.new(resource_name, self))
          child_resource_hook(resource) if respond_to?(:child_resource_hook) && [true, :on_access].include?(automatic)
        end
        resource.config self, &block
      end
    end

    def child_resource_name(parent, base)
      base
    end

    def finalize_resource_definition
      @automatic_children_resources = ancestors.collect { |ancestor| ancestor.automatic_children_resources if ancestor.respond_to?(:automatic_children_resources) }.compact.reduce(&:merge)
      @outputs = ancestors.collect { |ancestor| ancestor.outputs if ancestor.respond_to?(:outputs) }.compact.reduce(&:merge)
      @properties = ancestors.collect { |ancestor| ancestor.properties if ancestor.respond_to?(:properties) }.compact.reduce(&:merge)
      @validators = ancestors.collect { |ancestor| ancestor.validators if ancestor.respond_to?(:validators) }.compact.reduce(&:merge)
    end

    # Defines a special property type that represents a boolean state.
    # @param name [Symbol] name of the new flag
    # @param default_value [Boolean] what value to use for the flag if neither assertion statement is invoked.
    # @param inherited: [Boolean] If this property will, when not explicitly set, inherit the value from it's parent resource tree.
    # @return [void]
    # @!macro [attach] flag_dsl
    #    @!group Properties
    #    @!method $1?
    #       Tests if the flag is currently true or false.
    #       @return [Boolean]
    #    @!group Properties
    #    @!method $1
    #       Asserts that the $1 flag should be set to true. (Default: $2)
    #       @return [true]
    #    @!group Properties
    #    @!method not_$1
    #       Asserts that the $1 flag should be set to false. (Default: $2)
    #       @return [false]
    #    @!group Properties
    #    @!method set_$1?
    #       Tests if the flag has been explicitly set or if {$1?} will return the default value.
    #       @return [Boolean]
    #       @!endgroup
    def flag(name, default_value, inherited: false)
      # Defines a specific type of property which is meant to be true/false.

      raise "Invalid default_value `#{default_value}` for `#{name}`, must be either `true` or `false`." unless [true, false].include?(default_value)

      # Prevent dev error of re-declaring properties
      raise "Duplicate property named `#{name}` defined on #{inspect}." if properties.include? name
      properties << name

      # Reused names
      is_set_name = :"set_#{name}?"
      ivar_name = :"@#{name}"
      assert_name = :"#{name}"
      negate_name = :"not_#{name}"
      getter_name = :"#{name}?"

      # Helper methods
      define_method(is_set_name) do |include_inheritance = inherited|
        set_locally = instance_variable_defined?(ivar_name)
        return set_locally unless include_inheritance

        inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(is_set_name) }
        set_locally || (inheritable_parent.nil? ? false : inheritable_parent.send(is_set_name))
      end

      # assertion
      define_method(assert_name) do
        instance_variable_set ivar_name, true
      end

      # negation
      define_method(negate_name) do
        instance_variable_set ivar_name, false
      end

      # getter
      define_method(getter_name) do
        if inherited && !send(is_set_name, false)
          inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(getter_name) }
          return inheritable_parent.send(getter_name) unless inheritable_parent.nil?
        end

        send(is_set_name) ? instance_variable_get(ivar_name) : default_value
      end
    end

    def id_validator(regex = nil)
      return instance_variable_get(:@id_validator) if regex.nil?
      raise "Overriding id_validator on `#{inspect}`. Old: #{id_validator}, New: #{regex}" if instance_variable_defined?(:@id_validator)
      raise "You must supply a regex to `id_validator`, received #{regex}." unless regex.is_a?(Regexp)
      @id_validator = regex
    end

    def output(attr = :__ref__, &block)
      raise "Duplicate output #{attr} for #{inspect}" if outputs.has_key?(attr)
      outputs[attr] = ResourceOutput.new(self, attr, &block)
    end

    def outputs
      @outputs ||= {}
    end

    def parent_resource(name)
      alias_method name, :parent_resource
    end

    def property(name, default_value, valid_values = nil, inherited: false, secret: false, &munger)
      # This method defines a variety of helpers related to the implementation of property methods.

      # Prevent dev error of re-declaring properties
      raise "Duplicate property named `#{name}` defined on #{inspect}." if properties.include? name
      properties << name

      # Reused names
      is_set_name = :"set_#{name}?"
      ivar_name = :"@#{name}"
      munger_name = :"munge_#{name}"
      validator_name = :"validate_#{name}"

      # Helper methods
      attr_writer name

      define_method(is_set_name) do |include_inheritance = inherited|
        set_locally = instance_variable_defined?(ivar_name)
        return set_locally unless include_inheritance

        inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(is_set_name) }
        set_locally || (inheritable_parent.nil? ? false : inheritable_parent.send(is_set_name))
      end

      define_method(munger_name) do |value|
        munged_value = munger.nil? ? value : instance_exec(value, &munger)
        munged_value || value
      end

      validator(name) do |value = nil|
        valid = valid_values.nil? || self.class._valid_value?(valid_values, value || send(name))
        error "Property `#{name}` doesn't allow invalid value `#{value.inspect}`. Value must be one of the following: #{valid_values}." unless valid
        valid
      end

      # Chef-like setter + getter
      define_method(name) do |value = nil|
        # Inheritance of the value from a possible parent.
        if value.nil? && inherited && !send(is_set_name, false)
          inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(name) }
          return inheritable_parent.send(name) unless inheritable_parent.nil?
        end

        # Standard getter
        return send(is_set_name) ? instance_variable_get(ivar_name) : default_value if value.nil?

        # Setter
        value = send(munger_name, value)
        raise "Unable to validate property #{name} on #{self.class}." unless send(validator_name, value)

        value = secret ? SecretPropertyValue.new(name, value) : value
        instance_variable_set ivar_name, value
      end
    end

    def property_collection(singular, default_values, valid_values = nil, required: true, inherited: false, &munger)
      # Prevent dev error of re-declaring properties
      raise "Duplicate property named `#{singular}` defined on #{inspect}." if properties.include? singular

      properties << singular

      plural = singular.pluralize
      raise "Irregular pluralization detected on #{singular}" if plural == singular

      # Reused names
      is_set_name = :"set_#{plural}?"
      ivar_name = :"@#{plural}"
      munger_name = :"munge_#{plural}"
      validator_name = :"validate_#{plural}"

      # Helper methods
      attr_writer singular

      define_method(is_set_name) do |include_inheritance = inherited|
        set_locally = instance_variable_defined?(ivar_name) && !instance_variable_get(ivar_name).empty?
        return set_locally unless include_inheritance

        inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(is_set_name) }
        set_locally || (inheritable_parent.nil? ? false : inheritable_parent.send(is_set_name))
      end

      define_method(munger_name) do |value|
        munged_value = munger.nil? ? value : instance_exec(value, &munger)
        munged_value || value
      end

      validator(plural) do |value = nil|
        if !value.nil?
          valid = valid_values.nil? || self.class._valid_value?(valid_values, value)
          error "Property `#{singular}` doesn't allow invalid value `#{value.inspect}`. Value must be one of the following: #{valid_values}." unless valid
          valid
        elsif required == true && send(plural).empty?
          error "Property collection `#{plural}` is required to have at least one value."
          false
        else
          values = send(plural)
          valid = valid_values.nil? || values.empty? || self.class._valid_values?(valid_values, values)
          unless valid
            error "Property collection `#{plural}` has one or more invalid values."
            error "Valid values are: #{valid_values.inspect}.  #{plural} is set to #{values.inspect}."
          end
          valid
        end
      end

      # Collection getter
      define_method(plural) do |include_inherited = inherited|
        if include_inherited && !send(is_set_name, false)
          inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(plural) }
          return inheritable_parent.send(plural) unless inheritable_parent.nil?
        end

        send(is_set_name, include_inherited) ? instance_variable_get(ivar_name) : instance_variable_set(ivar_name, [] + default_values)
      end

      # Value Adder
      define_method(singular) do |*values|
        collection = send(plural, false)

        values.each do |value|
          value = send(munger_name, value)
          return value.each { |v| __send__(singular, v) } if value.is_a?(Array) and !valid_values.include?(Array)

          raise "Added property value `#{value}` to `#{plural}` multiple times on `#{singular}`." if collection.include?(value)
          raise "Unable to validate property #{singular} on #{self.class}." unless send(validator_name, value)
          collection << value
        end
      end
    end

    def properties
      @properties ||= Set.new
    end

    def resource_collection(klass, singular, required: true)
      plural = singular.pluralize
      raise "Irregular pluralization detected on #{singular}" if plural == singular

      is_set_name = :"set_#{plural}?"
      ivar_name = :"@#{plural}"

      # Prevent dev error of re-declaring properties
      raise "Duplicate property named `#{singular}` defined on #{inspect}." if properties.include? singular
      properties << singular

      # Helpers
      define_method(is_set_name) { instance_variable_defined?(ivar_name) && !instance_variable_get(ivar_name).empty? }

      validator(plural) do |value = nil|
        valid = required == false || (send(is_set_name) && !send(plural).empty?)
        error "Resource collection `#{plural}` is required to have at least one member." unless valid
        valid
      end

      # Collection getter
      define_method(plural) do
        instance_variable_defined?(ivar_name) ? instance_variable_get(ivar_name) : instance_variable_set(ivar_name, [])
      end

      # Instance getter
      define_method(singular) do |name, &block|
        resource = klass.new(name, self)

        collection = send(plural)
        unless collection.include? resource
          collection << resource
          child_resource_hook(resource) if respond_to? :child_resource_hook
        end

        resource.config self, &block
      end
    end

    def resource_reference(klass, singular, required: true, inherited: false)
      is_set_name = :"set_#{singular}?"
      ivar_name = :"@#{singular}"

      # Prevent dev error of re-declaring properties
      raise "Duplicate property named `#{singular}` defined on #{inspect}." if properties.include? singular
      properties << singular

      # Helper methods
      attr_writer singular

      define_method(is_set_name) do |include_inheritance = inherited|
        set_locally = instance_variable_defined?(ivar_name)
        return set_locally && !instance_variable_get(ivar_name).nil? unless include_inheritance

        inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(is_set_name) }
        set_locally || (inheritable_parent.nil? ? false : inheritable_parent.send(is_set_name))
      end

      validator(singular) do |value = nil|
        valid = required == false || send(is_set_name)
        error "Resource reference `#{singular}` is required to have a value." unless valid
        valid
      end

      # Chef-Style Getter/Setter
      define_method(singular) do |name = nil, &block|
        if name.nil? && inherited && !send(is_set_name, false)
          inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(singular) }
          return inheritable_parent.send(singular) unless inheritable_parent.nil?
        end

        resource = instance_variable_get(ivar_name)
        return resource if name.nil?

        referenced_resource = case name
          when String
            klass.validate_resource_id(name)
            name
          when klass
            name
          else
            klass.new(name)
        end

        raise "Attempted to re-assign resource reference for #{singular}.  Old: #{resource.name}, New: #{referenced_resource}" unless resource.nil? || resource == referenced_resource
        raise "Attempted to provide configuration block for resource reference to `#{singular}`. Resource references do not support configuration." unless block.nil?

        instance_variable_set(ivar_name, referenced_resource)
      end
    end

    def resource_references(klass, singular, required: true, inherited: false)
      plural = singular.pluralize
      raise "Irregular pluralization detected on #{singular}" if plural == singular

      is_set_name = :"set_#{plural}?"
      ivar_name = :"@#{plural}"

      # Prevent dev error of re-declaring properties
      raise "Duplicate property named `#{singular}` defined on #{inspect}." if properties.include? singular
      properties << singular

      # Helpers
      define_method(is_set_name) do |include_inheritance = inherited|
        set_locally = instance_variable_defined?(ivar_name) && !instance_variable_get(ivar_name).empty?
        return set_locally unless include_inheritance

        inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(is_set_name) }
        set_locally || (inheritable_parent.nil? ? false : inheritable_parent.send(is_set_name))
      end

      validator(plural) do |value = nil|
        valid = required == false || (send(is_set_name) && !send(plural).empty?)
        error "Resource reference `#{plural}` is required to have at least one value." unless valid
        valid
      end

      # Collection getter
      define_method(plural) do |include_inherited = inherited|
        if include_inherited && !send(is_set_name, false)
          inheritable_parent = resource_tree.reverse.find { |parent| parent != self && parent.respond_to?(plural) }
          return inheritable_parent.send(plural) unless inheritable_parent.nil?
        end

        send(is_set_name, include_inherited) ? instance_variable_get(ivar_name) : instance_variable_set(ivar_name, [])
      end

      # Chef-Style Instance Setter
      define_method(singular) do |*names, &block|
        raise "Attempted to provide configuration block for resource references to `#{plural}`. Resource references do not support configuration." unless block.nil?
        collection = send(plural, false)

        names.each do |name|
          resource = case name
            when Array
              debug "Received array of values for #{singular}; assigning each individually. Array: #{name.inspect}"
              name.each { |n| __send__(singular, n) }
              return
            when String
              klass.validate_resource_id(name)
              name
            when klass
              name
            else
              klass.new(name)
          end

          raise "Added resource reference `#{resource}` to `#{plural}` multiple times." if collection.include?(resource)
          collection << resource
        end
      end
    end

    def tag_key(key)
      raise 'You may only set cfn_type on a subclass of Resource' unless subclass_of? Resource
      define_method(:tag_key) { key }
    end

    def validate_resource_id(id)
      raise "You can't validate an id for a meta resource." unless subclass_of?(Resource)
      raise "Attempting to validate id `#{id}` for `#{inspect}`, but no id validator is set." if id_validator.nil?
      raise "Unable to validate resource id `#{id}`.  Must match #{id_validator} for #{inspect}" unless id_validator.match(id)
    end

    def validator(name, &validator)
      validator_name = :"validate_#{name}"
      validators << validator_name

      raise "Duplicate validator `#{validator_name}` declared on #{inspect}." if self.class.instance_methods(false).include?(validator_name)

      define_method(validator_name) do |*args|
        valid = instance_exec(*args, &validator)
        [true, false].include?(valid) ? valid : true
      end
    end

    def validators
      @validators ||= Set.new
    end
  end
end
