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

require 'contrast/framework/base_support'
require 'contrast/framework/rack/patch/support'
require 'contrast/utils/duck_utils'

module Contrast
  module Framework
    module Rack
      # Used when Rack is present to define framework specific behavior. For
      # now, the only part of this implemented is the Patch Support.
      module Support
        RACK_REQUEST_PATH = 'REQUEST_PATH'
        RACK_SERVER_NAME = 'SERVER_NAME'

        extend Contrast::Framework::BaseSupport
        extend Contrast::Framework::Rack::Patch::Support
        class << self
          def detection_class
            'Rack'
          end

          # @return [String] the Rack version
          def version
            ::Rack.version
          rescue StandardError
            ''
          end

          # @return [String] the Rack application name
          def application_name
            'Rack Application'
          end

          def application_root
            Dir.pwd
          end

          # @return [String] the server type
          def server_type
            'Rack'
          end

          # Find all the predefined routes for this application
          #
          # Extracting the Rack application routes is not trivial. Routes are evaluated dynamically
          # when a request comes in, so they are not loaded before and stored in a data structure
          # available somewhere. This mean that route discovery is only available through the rack map,
          # but this is limited as not showing the actual method (GET, POST, etc...). For now The Agent
          # will use only the current_route_coverage for Rack applications.
          #
          # @return [Array<Contrast::Agent::Reporting::DiscoveredRoute>]
          # @raise [NoMethodError] raises error if subclass does not implement this method
          def collect_routes
            # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless defined?(Rack)
            # Rack::URLMap is used for mapping different rack apps to different paths.
            # The Rack app could be separated into smaller rack applications.
            # Rack::Builder is another option.
            # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless rack_map

            # This method is disabled for now, as it is not returning the actual routes. Code is left for as
            # comment for future reference.
            #
            # routes = []
            # rack_map.any? do |path, meta|
            #   routes << Contrast::Agent::Reporting::DiscoveredRoute.from_rack_route(meta[1], meta[0], path)
            # end
            # routes
            Contrast::Utils::ObjectShare::EMPTY_ARRAY
          end

          # Given the current request - return a RouteCoverage object

          # @param request [Contrast::Agent::Request] a contrast tracked request.
          # @param _controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base.
          # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
          def current_route_coverage request, _controller = nil, full_route = nil
            method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...

            full_route ||= request.env.fetch(::Rack::PATH_INFO, nil)
            full_route = request.env.fetch(RACK_REQUEST_PATH, nil) if Contrast::Utils::DuckUtils.empty_duck?(full_route)
            return unless method

            # If we are here and have method but the route is "" we might be expecting the home page.
            full_route = '/' if Contrast::Utils::DuckUtils.empty_duck?(full_route) &&
                request.env.fetch(RACK_SERVER_NAME, nil)

            route_coverage = Contrast::Agent::Reporting::RouteCoverage.new
            # We might not have controller, or even if there is defined one, it could not bare the name of the
            # route to match as an object, it could be one router class with base controller with several methods
            # describing each class, search for final controller might be resource heavy, and not efficient.
            # For now to identify the controller the Agent will use the route name, this may lead to recording
            # of false routes, but it is better than nothing. If route do no match a pattern it is a good practice
            # to notify the user by displaying a not found page, in a sense this is a exercise of the application, but
            # not correctly recorded controller name. Try to see if there is a define Rack::URLMap, and use it first.
            mapped_controller = rack_map[full_route]&.last
            final_controller = mapped_controller || full_route
            route_coverage.attach_rack_based_data(final_controller, method, nil, full_route)
            route_coverage
          end

          # Try and get map of Rack application { "path" => ["pattern", "controller"] }.
          #
          # @return [Hash<String, Array<String>>] the rack map
          def rack_map
            rack_map = {}
            maps = ObjectSpace.each_object(::Rack::URLMap).to_a
            maps.any? do |map|
              mapping = map.instance_variable_get(:@mapping)
              mapping.any? do |arr|
                path = arr[1]
                pattern = arr[2]
                controller = arr[3]&.cs__class&.cs__name
                rack_map[path] = [pattern, controller] if path&.cs__is_a?(String) && controller
              end
            end
            rack_map
          rescue StandardError
            {}
          end

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