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

require 'contrast/agent/patching/policy/method_policy'

module Contrast
  module Agent
    module Patching
      module Policy
        # This class is used to map a class to all policy nodes utilizing that
        # class. It should be initialized using the create_module_policy method
        # rather than new.
        class ModulePolicy
          class << self
            # Given the name of a module, create a :ModulePolicy for it using the Policy of each supported feature.
            #
            # @param module_name [String] the name of the module to which the policy applies.
            # @return [Contrast::Agent::Patching::Policy::ModulePolicy]
            def create_module_policy module_name
              module_policy = Contrast::Agent::Patching::Policy::ModulePolicy.new
              module_policy.source_nodes =      nodes_for_module(
                  Contrast::Agent::Assess::Policy::Policy.instance.sources, module_name)
              module_policy.propagator_nodes =  nodes_for_module(
                  Contrast::Agent::Assess::Policy::Policy.instance.propagators, module_name)
              module_policy.trigger_nodes =     nodes_for_module(
                  Contrast::Agent::Assess::Policy::Policy.instance.triggers, module_name)
              module_policy.protect_nodes =     nodes_for_module(
                  Contrast::Agent::Protect::Policy::Policy.instance.triggers, module_name)
              module_policy.inventory_nodes =   nodes_for_module(
                  Contrast::Agent::Inventory::Policy::Policy.instance.triggers, module_name)
              module_policy.deadzone_nodes =    nodes_for_module(
                  Contrast::Agent::Deadzone::Policy::Policy.instance.deadzones, module_name)
              module_policy
            end

            # Find any of the given patchers that match this class' names.
            # Always returns an array, even if it's empty.
            #
            # @param nodes [Array(Contrast::Agent::Patching::Policy::PolicyNode)]
            #   an array of nodes
            # @param class_name [String] the class to filter the nodes on
            # @return [Array<Contrast::Agent::Patching::Policy::PolicyNode>]
            #   Subset of nodes which apply to the given class
            def nodes_for_module nodes, class_name
              nodes.select { |node| class_name == node.class_name }
            end
          end

          attr_accessor :source_nodes, :propagator_nodes, :trigger_nodes, :inventory_nodes, :protect_nodes,
                        :deadzone_nodes

          def empty?
            return false if source_nodes.any?
            return false if propagator_nodes.any?
            return false if trigger_nodes.any?
            return false if inventory_nodes.any?
            return false if protect_nodes.any?
            return false if deadzone_nodes.any?

            true
          end

          # The number of expected patches for this policy is the sum of unique
          # targeted methods for the Module to which this policy applies.
          #
          # @return [Integer] count of methods to be patched
          def num_expected_patches
            @_num_expected_patches ||= begin
              instance_methods = Set.new
              singleton_methods = Set.new
              sort_method_names(source_nodes, instance_methods, singleton_methods)
              sort_method_names(propagator_nodes, instance_methods, singleton_methods)
              sort_method_names(trigger_nodes, instance_methods, singleton_methods)
              sort_method_names(inventory_nodes, instance_methods, singleton_methods)
              sort_method_names(protect_nodes, instance_methods, singleton_methods)
              sort_method_names(deadzone_nodes, instance_methods, singleton_methods)
              instance_methods.length + singleton_methods.length
            end
          end

          private

          def sort_method_names nodes, instance_methods, singleton_methods
            nodes.each do |node|
              if node.instance_method?
                instance_methods << node.method_name
              else
                singleton_methods << node.method_name
              end
            end
          end
        end
      end
    end
  end
end