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

require 'monitor'
require 'contrast/components/logger'
require 'contrast/components/scope'
require 'contrast/utils/patching/policy/patch_utils'

require 'contrast/agent'
require 'contrast/logger/log'
require 'contrast/agent/patching/policy/method_policy'
require 'contrast/agent/patching/policy/patch_status'
require 'contrast/agent/patching/policy/trigger_node'

# assess
require 'contrast/agent/assess/policy/policy'
require 'contrast/agent/assess/policy/propagation_method'
require 'contrast/agent/assess/policy/source_method'
require 'contrast/agent/assess/policy/trigger_method'

# protect
require 'contrast/agent/protect/policy/policy'

# inventory
require 'contrast/agent/inventory/policy/policy'

module Contrast
  module Agent
    module Patching
      module Policy
        # This is how we patch into our customer's code. It provides a way to
        # track which classes we need to patch into and, once we've woven,
        # provides a map for which methods our renamed functions need to call
        # and how.
        module Patch
          extend Contrast::Utils::Patching::PatchUtils

          class << self
            include Contrast::Agent::Assess::Policy::SourceMethod
            include Contrast::Agent::Assess::Policy::PropagationMethod
            include Contrast::Agent::Assess::Policy::TriggerMethod
            include Contrast::Components::Logger::InstanceMethods
            include Contrast::Components::Scope::InstanceMethods

            POLICIES = [
              Contrast::Agent::Assess::Policy::Policy,
              Contrast::Agent::Inventory::Policy::Policy,
              Contrast::Agent::Protect::Policy::Policy
            ].cs__freeze

            def enter_method_scope! method_policy
              contrast_enter_method_scopes!(method_policy.scopes_to_enter)
            end

            def exit_method_scope! method_policy
              contrast_exit_method_scopes!(method_policy.scopes_to_exit)
            end

            # @param mod [Module] the module in which the patch should be
            #   placed.
            # @param methods [Array(Symbol)] all the instance or singleton
            #   methods in this clazz.
            # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
            #   the policy that applies to the method to be patched.
            # @return [Boolean] if patched, either by this invocation or a
            #   previous, or not
            def instrument_with_alias mod, methods, method_policy
              cs_method_name = build_method_name(mod, method_policy.method_name)
              # we've already patched this class, don't do it again
              return true if methods.include?(cs_method_name)

              # that method is within Contrast definition so it should be skipped
              method = mod.instance_method(method_policy.method_name) if method_policy.instance_method
              method = mod.singleton_method(method_policy.method_name) unless method_policy.instance_method
              return true if method.owner <= Contrast

              begin
                contrast_define_method(mod, method_policy, cs_method_name)
              rescue NameError => e
                # This shouldn't happen anymore, but just in case calling alias
                # results in a NameError, we'll be safe here.
                logger.error(
                    'Attempted to alias a method on a Module that doesn\'t respond to it.',
                    e,
                    module: mod.cs__name,
                    method: method_policy.method_name)
                return false
              end
              true
            end

            # @param mod [Module] the module in which the patch should be
            #   placed.
            # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
            #   the policy that applies to the method to be patched.
            # @return [Boolean] if patched, either by this invocation or a
            #   previous, or not
            def instrument_with_prepend mod, method_policy
              contrast_prepend_method(mod, method_policy)
            end

            # @param unbound_method [UnboundMethod] An unbound method, that doesn't reference its binding.
            #   This method executes C hooking code.
            def register_c_hook unbound_method
              # current binding is as meaningless as any other.  but we need something
              unbound_method.bind_call(self)
            end

            # @param target_module_name [String] Fully-qualified module name, as string, to which the C patch applies.
            # @param unbound_method [UnboundMethod] An unbound method, to be patched into target_module.
            # @param impl [Symbol] Strategy for applying the patch: { :alias_instance, :alias_singleton, or :prepend }:
            #   :alias_instance     -> alias instance method of module
            #   :alias_singleton    -> alias instance method of singleton class of module
            #                          (equivalent to :alias, where `module = module.singleton class`)
            #                          (this is a.k.a. "class-method patch")
            #   :prepend            -> prepend instance method of module
            #   :prepending singleton -> prepend singleton method of module
            # @return [Symbol] new alias for the underlying method (presumably, so the patched method can call it)
            def register_c_patch target_module_name, unbound_method, impl = :alias_instance
              # These could be set as AfterLoadPatches.
              method_name = unbound_method.name.to_sym # rubocop:disable Security/Module/Name -- ruby built in attribute.
              underlying_method_name = underlying_method_name(method_name, impl)

              target_module = Module.cs__const_get(target_module_name)
              target_module = target_module.cs__singleton_class if %i[prepend_singleton alias_singleton].include?(impl)

              visibility = if target_module.private_instance_methods(false).include?(method_name)
                             :private
                           elsif target_module.protected_instance_methods(false).include?(method_name)
                             :protected
                           elsif target_module.public_instance_methods(false).include?(method_name)
                             :public
                           else
                             raise(NoMethodError,
                                   <<~ERR)
                                     Tried to register a C-defined #{ impl } patch for \
                                     #{ target_module_name }##{ method_name }, but can't find :#{ method_name }.
                                   ERR
                           end

              reflect_implementation(impl, target_module, unbound_method, visibility)
              # Ougai::Logger.create_item_with_2args calls Hash#[]=, so we
              # can't invoke this logging method or we'll seg fault as we'd
              # change the method definition mid-call
              # if method_name != :[]= &&
              #   Contrast::Agent::Logger.defined!
              #   logger.trace(
              #       'Registered C-defined patch',
              #       implementation: impl,
              #       module: target_mod,
              #       method: method_name,
              #       visibility: visibility)
              # end
              underlying_method_name
            end

            # @param impl [Symbol] Strategy for applying the patch: { :alias_instance, :alias_singleton, or :prepend }:
            # @param target_module [Module] The targeted module
            # @param unbound_method [UnboundMethod] An unbound method, to be patched into target_module.
            # @param visibility [Symbol] method visibility
            def reflect_implementation impl, target_module, unbound_method, visibility
              method_name = unbound_method.name.to_sym # rubocop:disable Security/Module/Name -- ruby built in attribute.
              underlying_method_name = underlying_method_name(method_name, impl)

              case impl
              when :alias_instance, :alias_singleton
                # Core to patching. Ignore define method usage cop.
                # rubocop:disable Performance/Kernel/DefineMethod
                unless target_module.instance_methods(false).include?(underlying_method_name)
                  # alias_method may be private
                  target_module.send(:alias_method, underlying_method_name, method_name)
                  target_module.send(:define_method, method_name, unbound_method.bind(target_module))
                end
                target_module.send(visibility, method_name) # e.g., module.private(:my_method)
              when :prepend_instance, :prepend_singleton
                prepending_module = Module.new
                prepending_module.send(:define_method, method_name, unbound_method.bind(target_module))
                prepending_module.send(visibility, method_name)

                # This prepends to the singleton class (it patches a class method)
                target_module.prepend(prepending_module)
                # rubocop:enable Performance/Kernel/DefineMethod
              end
            end

            # @return [Boolean]
            def skip_contrast_analysis?
              return true if in_contrast_scope?
              return false unless defined?(Contrast::Agent::REQUEST_TRACKER)
              return false unless Contrast::Agent::REQUEST_TRACKER.current
              return true unless Contrast::Agent::REQUEST_TRACKER.current.analyze_request?

              false
            end

            # Skip if we should skip_contrast_analysis?, sampling says to ignore this
            # request, or assess has been disabled.
            #
            # @return [Boolean]
            def skip_assess_analysis?
              return true if skip_contrast_analysis?

              !ASSESS&.enabled?
            end

            def underlying_method_name method_name, impl
              return method_name.to_sym if %i[prepend_instance prepend_singleton].include?(impl)

              build_unbound_method_name(method_name).to_sym
            end
          end
        end
      end
    end
  end
end

require 'cs__contrast_patch/cs__contrast_patch'