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

require 'contrast/framework/platform_version'
require 'contrast/framework/rack/support'
require 'contrast/framework/rails/support'
require 'contrast/framework/sinatra/support'
require 'contrast/components/interface'
require 'contrast/utils/class_util'

module Contrast
  module Framework
    # Allows access to framework specific information
    class Manager
      include Contrast::Components::Interface
      access_component :analysis, :logging

      # Order here does matter as the first framework listed will be the first one we pull information from
      # Rack will be a special case that may involve updating some logic to handle only applying Rack if Rails/Sinatra
      # do not exist
      SUPPORTED_FRAMEWORKS = [
        Contrast::Framework::Rails::Support,
        Contrast::Framework::Sinatra::Support,
        Contrast::Framework::Rack::Support
      ].cs__freeze

      def initialize
        @_frameworks = SUPPORTED_FRAMEWORKS.map do |framework_klass|
          next unless enable_framework_support?(framework_klass.detection_class)

          logger.info('Framework detected. Enabling support.', framework: framework_klass.detection_class)
          framework_klass
        end
        @_frameworks.compact!
      end

      # Patches that have to be applied as early as possible to catch calls
      # that happen prior to the first Request, typically those around
      # configuration.
      def before_load_patches!
        @_before_load_patches ||= begin
          SUPPORTED_FRAMEWORKS.each(&:before_load_patches!)
          true
        end
      end

      # Return all the After Load Patches for all the Frameworks we know, even if that Framework hasn't been detected.
      #
      # @return [Set<Contrast::Agent::Patching::Policy::AfterLoadPatch>] the AfterLoadPatches of each framework
      def find_after_load_patches
        patches = Set.new
        SUPPORTED_FRAMEWORKS.each do |framework|
          framework_patches = framework.after_load_patches
          patches.merge(framework_patches) if framework_patches && !framework_patches.empty?
        end
        patches
      end

      def find_route_discovery_data
        routes_for_all_frameworks
      end

      def platform_version
        framework_version = first_framework_result :version, ''

        Contrast::Framework::PlatformVersion.from_string(framework_version)
      end

      def server_type
        first_framework_result :server_type, 'rack'
      end

      def app_name
        first_framework_result :application_name, 'root'
      end

      def app_root
        found = first_framework_result :application_root, nil
        found || ::Rack::Directory.new('').root
      end

      # If we have 0 or n > 1 frameworks, we need to use the default rack request
      #
      # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values
      # of this particular Request
      # @return [::Rack::Request] either a rack request or subclass thereof.
      def retrieve_request env
        return @_frameworks[0].retrieve_request(env) if @_frameworks.length == 1

        ::Rack::Request.new(env)
      end

      # @param env [Hash] the various variables stored by this and other Middlewares to know the state
      #   and values of this particular Request
      # @return [Boolean] true if at least one framework is streaming the response; false if none are streaming
      def streaming? env
        result = false
        @_frameworks.each do |framework|
          result = framework.streaming?(env)
          break if result
        end
        result
      end

      # Iterate through current frameworks and return the current request's route. This will be the first
      # non-nil result.
      #
      # @param request [Contrast::Agent::Request] the current request.
      # @return [Contrast::Api::Dtm::RouteCoverage] the current route as a Dtm.
      def get_route_dtm request
        @_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.reject(&:nil?).first
      end

      private

      def enable_framework_support? klass
        Contrast::Utils::ClassUtil.truly_defined?(klass)
      end

      def routes_for_all_frameworks
        data_for_all_frameworks :collect_routes
      end

      # This returns an array of all data from each framework in a flat, no-nil values array
      #
      # @param method_name [Symbol] the method to call on each FrameworkSupport class
      # @return [Array]
      def data_for_all_frameworks method_name
        data = @_frameworks.flat_map do |framework|
          framework.send(method_name)
        end
        data.compact
      end

      # This returns a single object from the first framework to successfully respond
      #
      # @param method_name [Symbol] the method to call on each FrameworkSupport class
      # @return [Object] - Determined by method to be invoked
      def first_framework_result method_name, default_value
        result = nil
        @_frameworks.each do |framework|
          result = framework.send(method_name)
          break if result
        end
        result || default_value
      end
    end
  end
end