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

cs__scoped_require 'contrast/framework/view_technologies_descriptor'
cs__scoped_require 'contrast/framework/platform_version'
cs__scoped_require 'contrast/framework/base_support'
cs__scoped_require 'contrast/framework/rails_support'
cs__scoped_require 'contrast/framework/sinatra_support'
cs__scoped_require 'contrast/components/interface'
cs__scoped_require 'contrast/utils/class_util'

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

      # 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::RailsSupport,
        Contrast::Framework::SinatraSupport
      ].cs__freeze

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

          logger.debug("Detected framework #{ framework_klass.detection_class } - enabling support")
          framework_klass
        end
        @_frameworks.compact!
      end

      def find_applicable_view_technologies
        scan_views_for_all_frameworks
      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 get_route_dtm request
        result = nil
        @_frameworks.find do |framework_klass|
          # TODO: RUBY-763 Sinatra::Base#call patch adds the Route report
          next if framework_klass == Contrast::Framework::SinatraSupport

          result = framework_klass.current_route(request)
        end
        result
      end

      private

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

      def scan_views_for_all_frameworks
        data_for_all_frameworks :scan_views
      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.compact
        data
      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