module Grape
  # An Endpoint is the proxy scope in which all routing
  # blocks are executed. In other words, any methods
  # on the instance level of this class may be called
  # from inside a `get`, `post`, etc.
  class Endpoint
    include Grape::DSL::Settings
    include Grape::DSL::InsideRoute

    attr_accessor :block, :source, :options
    attr_reader :env, :request, :headers, :params

    class << self
      def new(*args, &block)
        self == Endpoint ? Class.new(Endpoint).new(*args, &block) : super
      end

      def before_each(new_setup = false, &block)
        @before_each ||= []
        if new_setup == false
          return @before_each unless block_given?
          @before_each << block
        else
          @before_each = [new_setup]
        end
      end

      def run_before_each(endpoint)
        superclass.run_before_each(endpoint) unless self == Endpoint
        before_each.each { |blk| blk.call(endpoint) if blk.respond_to?(:call) }
      end

      # @api private
      #
      # Create an UnboundMethod that is appropriate for executing an endpoint
      # route.
      #
      # The unbound method allows explicit calls to +return+ without raising a
      # +LocalJumpError+. The method will be removed, but a +Proc+ reference to
      # it will be returned. The returned +Proc+ expects a single argument: the
      # instance of +Endpoint+ to bind to the method during the call.
      #
      # @param [String, Symbol] method_name
      # @return [Proc]
      # @raise [NameError] an instance method with the same name already exists
      def generate_api_method(method_name, &block)
        if method_defined?(method_name)
          raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name")
        end

        define_method(method_name, &block)
        method = instance_method(method_name)
        remove_method(method_name)

        proc do |endpoint_instance|
          ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
            method.bind(endpoint_instance).call
          end
        end
      end
    end

    # Create a new endpoint.
    # @param new_settings [InheritableSetting] settings to determine the params,
    #   validations, and other properties from.
    # @param options [Hash] attributes of this endpoint
    # @option options path [String or Array] the path to this endpoint, within
    #   the current scope.
    # @option options method [String or Array] which HTTP method(s) can be used
    #   to reach this endpoint.
    # @option options route_options [Hash]
    # @note This happens at the time of API definition, so in this context the
    # endpoint does not know if it will be mounted under a different endpoint.
    # @yield a block defining what your API should do when this endpoint is hit
    def initialize(new_settings, options = {}, &block)
      require_option(options, :path)
      require_option(options, :method)

      self.inheritable_setting = new_settings.point_in_time_copy

      route_setting(:saved_declared_params, namespace_stackable(:declared_params))
      route_setting(:saved_validations, namespace_stackable(:validations))

      namespace_stackable(:representations, []) unless namespace_stackable(:representations)
      namespace_inheritable(:default_error_status, 500) unless namespace_inheritable(:default_error_status)

      @options = options

      @options[:path] = Array(options[:path])
      @options[:path] << '/' if options[:path].empty?

      @options[:method] = Array(options[:method])
      @options[:route_options] ||= {}

      @lazy_initialize_lock = Mutex.new
      @lazy_initialized = nil
      @block = nil

      @status = nil
      @file = nil
      @body = nil
      @proc = nil

      return unless block_given?

      @source = block
      @block = self.class.generate_api_method(method_name, &block)
    end

    # Update our settings from a given set of stackable parameters. Used when
    # the endpoint's API is mounted under another one.
    def inherit_settings(namespace_stackable)
      inheritable_setting.route[:saved_validations] += namespace_stackable[:validations]
      parent_declared_params = namespace_stackable[:declared_params]

      if parent_declared_params
        inheritable_setting.route[:declared_params] ||= []
        inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten)
      end

      endpoints && endpoints.each { |e| e.inherit_settings(namespace_stackable) }
    end

    def require_option(options, key)
      raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
    end

    def method_name
      [options[:method],
       Namespace.joined_space(namespace_stackable(:namespace)),
       (namespace_stackable(:mount_path) || []).join('/'),
       options[:path].join('/')]
        .join(' ')
    end

    def routes
      @routes ||= endpoints ? endpoints.collect(&:routes).flatten : to_routes
    end

    def reset_routes!
      endpoints.each(&:reset_routes!) if endpoints
      @namespace = nil
      @routes = nil
    end

    def mount_in(router)
      if endpoints
        endpoints.each { |e| e.mount_in(router) }
      else
        reset_routes!
        routes.each do |route|
          methods = [route.request_method]
          if !namespace_inheritable(:do_not_route_head) && route.request_method == Grape::Http::Headers::GET
            methods << Grape::Http::Headers::HEAD
          end
          methods.each do |method|
            unless route.request_method.to_s.upcase == method
              route = Grape::Router::Route.new(method, route.origin, route.attributes.to_h)
            end
            router.append(route.apply(self))
          end
        end
      end
    end

    def to_routes
      route_options = prepare_default_route_attributes
      map_routes do |method, path|
        path = prepare_path(path)
        params = merge_route_options(route_options.merge(suffix: path.suffix))
        route = Router::Route.new(method, path.path, params)
        route.apply(self)
      end.flatten
    end

    def prepare_routes_requirements
      endpoint_requirements = options[:route_options][:requirements] || {}
      all_requirements = (namespace_stackable(:namespace).map(&:requirements) << endpoint_requirements)
      all_requirements.reduce({}) do |base_requirements, single_requirements|
        base_requirements.merge!(single_requirements)
      end
    end

    def prepare_default_route_attributes
      {
        namespace: namespace,
        version: prepare_version,
        requirements: prepare_routes_requirements,
        prefix: namespace_inheritable(:root_prefix),
        anchor: options[:route_options].fetch(:anchor, true),
        settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations),
        forward_match: options[:forward_match]
      }
    end

    def prepare_version
      version = namespace_inheritable(:version) || []
      return if version.empty?
      version.length == 1 ? version.first.to_s : version
    end

    def merge_route_options(**default)
      options[:route_options].clone.reverse_merge(**default)
    end

    def map_routes
      options[:method].map { |method| options[:path].map { |path| yield method, path } }
    end

    def prepare_path(path)
      path_settings = inheritable_setting.to_hash[:namespace_stackable].merge(inheritable_setting.to_hash[:namespace_inheritable])
      Path.prepare(path, namespace, path_settings)
    end

    def namespace
      @namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace))
    end

    def call(env)
      lazy_initialize!
      dup.call!(env)
    end

    def call!(env)
      env[Grape::Env::API_ENDPOINT] = self
      @env = env
      @app.call(env)
    end

    # Return the collection of endpoints within this endpoint.
    # This is the case when an Grape::API mounts another Grape::API.
    def endpoints
      options[:app].endpoints if options[:app] && options[:app].respond_to?(:endpoints)
    end

    def equals?(e)
      (options == e.options) && (inheritable_setting.to_hash == e.inheritable_setting.to_hash)
    end

    protected

    def run
      ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
        @header = {}
        @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
        @params = @request.params
        @headers = @request.headers

        cookies.read(@request)
        self.class.run_before_each(self)
        run_filters befores, :before

        if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS])
          raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options?
          header 'Allow', allowed_methods
          response_object = ''
          status 204
        else
          run_filters before_validations, :before_validation
          run_validators validations, request
          run_filters after_validations, :after_validation
          response_object = @block ? @block.call(self) : nil
        end

        run_filters afters, :after
        cookies.write(header)

        # status verifies body presence when DELETE
        @body ||= response_object

        # The Body commonly is an Array of Strings, the application instance itself, or a File-like object
        response_object = file || [body]
        [status, header, response_object]
      end
    end

    def build_stack(helpers)
      stack = Grape::Middleware::Stack.new

      stack.use Rack::Head
      stack.use Class.new(Grape::Middleware::Error),
                helpers: helpers,
                format: namespace_inheritable(:format),
                content_types: namespace_stackable_with_hash(:content_types),
                default_status: namespace_inheritable(:default_error_status),
                rescue_all: namespace_inheritable(:rescue_all),
                rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions),
                default_error_formatter: namespace_inheritable(:default_error_formatter),
                error_formatters: namespace_stackable_with_hash(:error_formatters),
                rescue_options: namespace_stackable_with_hash(:rescue_options) || {},
                rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers) || {},
                base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers) || {},
                all_rescue_handler: namespace_inheritable(:all_rescue_handler)

      stack.concat namespace_stackable(:middleware)

      if namespace_inheritable(:version)
        stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
                  versions: namespace_inheritable(:version) ? namespace_inheritable(:version).flatten : nil,
                  version_options: namespace_inheritable(:version_options),
                  prefix: namespace_inheritable(:root_prefix),
                  mount_path: namespace_stackable(:mount_path).first
      end

      stack.use Grape::Middleware::Formatter,
                format: namespace_inheritable(:format),
                default_format: namespace_inheritable(:default_format) || :txt,
                content_types: namespace_stackable_with_hash(:content_types),
                formatters: namespace_stackable_with_hash(:formatters),
                parsers: namespace_stackable_with_hash(:parsers)

      builder = stack.build
      builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
      builder.to_app
    end

    def build_helpers
      helpers = namespace_stackable(:helpers) || []
      Module.new { helpers.each { |mod_to_include| include mod_to_include } }
    end

    private :build_stack, :build_helpers

    def helpers
      lazy_initialize! && @helpers
    end

    def lazy_initialize!
      return true if @lazy_initialized

      @lazy_initialize_lock.synchronize do
        return true if @lazy_initialized

        @helpers = build_helpers.tap { |mod| self.class.send(:include, mod) }
        @app = options[:app] || build_stack(@helpers)

        @lazy_initialized = true
      end
    end

    def run_validators(validator_factories, request)
      validation_errors = []

      validators = validator_factories.map(&:create_validator)

      ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
        validators.each do |validator|
          begin
            validator.validate(request)
          rescue Grape::Exceptions::Validation => e
            validation_errors << e
            break if validator.fail_fast?
          rescue Grape::Exceptions::ValidationArrayErrors => e
            validation_errors += e.errors
            break if validator.fail_fast?
          end
        end
      end

      validation_errors.any? && raise(Grape::Exceptions::ValidationErrors, errors: validation_errors, headers: header)
    end

    def run_filters(filters, type = :other)
      ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
        (filters || []).each { |filter| instance_eval(&filter) }
      end
      post_extension = DSL::InsideRoute.post_filter_methods(type)
      extend post_extension if post_extension
    end

    def befores
      namespace_stackable(:befores) || []
    end

    def before_validations
      namespace_stackable(:before_validations) || []
    end

    def after_validations
      namespace_stackable(:after_validations) || []
    end

    def afters
      namespace_stackable(:afters) || []
    end

    def validations
      route_setting(:saved_validations) || []
    end

    def options?
      options[:options_route_enabled] &&
        env[Grape::Http::Headers::REQUEST_METHOD] == Grape::Http::Headers::OPTIONS
    end
  end
end
