# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

module Contrast
  module Agent
    module Patching
      module Policy
        # This class is used to map each method to the trigger node that applies to it
        module MethodPolicyExtend
          # Given a Contrast::Agent::Patching::Policy::ModulePolicy, parse
          # out its information for the given method in order to construct a
          # Contrast::Agent::Patching::Policy::MethodPolicy
          #
          # @param method_name [Symbol] the name of the method for this policy
          # @param module_policy [Contrast::Agent::Patching::Policy::ModulePolicy]
          #   the entire policy for this module
          # @param instance_method [Boolean] true if this method is an
          #   instance method
          # @return [Contrast::Agent::Patching::Policy::MethodPolicy]
          def build_method_policy method_name, module_policy, instance_method
            source_node =      find_method_node(module_policy.source_nodes, method_name, instance_method)
            propagation_node = find_method_node(module_policy.propagator_nodes, method_name, instance_method)
            trigger_node =     find_method_node(module_policy.trigger_nodes, method_name, instance_method)
            protect_node =     find_method_node(module_policy.protect_nodes, method_name, instance_method)
            inventory_node =   find_method_node(module_policy.inventory_nodes, method_name, instance_method)
            deadzone_node =    find_method_node(module_policy.deadzone_nodes, method_name, instance_method)
            method_visibility = find_visibility(source_node, propagation_node, trigger_node, protect_node,
                                                inventory_node, deadzone_node)
            method_policy = MethodPolicy.new({
                                                 method_name: method_name,
                                                 method_visibility: method_visibility,
                                                 instance_method: instance_method,
                                                 source_node: source_node,
                                                 propagation_node: propagation_node,
                                                 trigger_node: trigger_node,
                                                 protect_node: protect_node,
                                                 inventory_node: inventory_node,
                                                 deadzone_node: deadzone_node
                                             })

            return method_policy unless check_method_policy_nodes_empty?(source_node, propagation_node, trigger_node,
                                                                         protect_node, inventory_node, deadzone_node)

            create_new_node(module_policy, method_policy) if module_policy.deadzone_nodes&.any?
            method_policy
          end

          def find_method_node nodes, method_name, is_instance_method
            return unless nodes

            nodes.find do |node|
              node.instance_method? == is_instance_method && node.method_name == method_name
            end
          end

          def find_visibility *nodes
            nodes.find { |node| node }&.method_visibility
          end

          def check_method_policy_nodes_empty?(source_node,
                                               propagation_node,
                                               trigger_node,
                                               protect_node,
                                               inventory_node,
                                               deadzone_node)
            return false unless source_node.nil? && propagation_node.nil? && trigger_node.nil? && protect_node.nil? &&
                inventory_node.nil? && deadzone_node.nil?

            true
          end

          private

          def create_new_node module_policy, method_policy
            return if module_policy.deadzone_nodes.empty?

            module_policy.deadzone_nodes.map do |node|
              next unless node.method_name.nil?

              klass = Module.cs__const_get(node.class_name)
              next unless it_defined?(klass, method_policy.method_name)

              new_node = set_new_node(method_policy, klass, node)
              method_policy.instance_variable_set(:@method_visibility, new_node.method_visibility)
              method_policy.instance_variable_set(:@deadzone_node, node)
              module_policy.deadzone_nodes << new_node
              break unless method_policy.deadzone_node.nil?
            end
          end

          # Helper method for creating new node
          #
          # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
          #   used to map each method to the trigger node that applies to it
          # @param klass [String] classname
          # @param node [Contrast::Agent::Patching::Policy::PolicyNode]
          # @return @_set_new_node [Contrast::Agent::Deadzone::Policy::DeadzoneNode]
          def set_new_node method_policy, klass, node
            new_node = {}
            new_node['instance_method'] = method_policy.instance_method
            new_node['method_visibility'] =
              klass.private_method_defined?(method_policy.method_name) ? 'private' : 'public'
            new_node['method_name'] = method_policy.method_name
            new_node['class_name'] = node.class_name
            @_set_new_node = Contrast::Agent::Deadzone::Policy::DeadzoneNode.new(new_node)
          end

          def it_defined? klass, method_name
            klass.instance_methods(false).include?(method_name) ||
                klass.private_instance_methods(false).include?(method_name) ||
                klass.singleton_methods(false).include?(method_name)
          end
        end
      end
    end
  end
end