module SoberSwag
  module Controller
    ##
    # Describe a single controller endpoint.
    class Route
      def initialize(method, action_name, path)
        @method = method
        @path = path
        @action_name = action_name
        @response_serializers = {}
        @response_descriptions = {}
      end

      attr_reader :response_serializers, :response_descriptions, :controller, :method, :path, :action_name

      ##
      # What to parse the request body in to.
      attr_reader :request_body_class
      ##
      # What to parse the request query_params in to
      attr_reader :query_params_class

      ##
      # What to parse the path params into
      attr_reader :path_params_class

      ##
      # Define the request body, using SoberSwag's type-definition scheme.
      # The block passed will be used to define the body of a new sublcass of `base` (defaulted to {SoberSwag::InputObject}.)
      # If you want, you can also define utility methods in here
      def request_body(base = SoberSwag::InputObject, &block)
        @request_body_class = make_input_object!(base, &block)
        action_module.const_set('ResponseBody', @request_body_class)
      end

      ##
      # Does this route have a body defined?
      def request_body?
        !request_body_class.nil?
      end

      ##
      # Define the shape of the query_params parameters, using SoberSwag's type-definition scheme.
      # The block passed is the body of the newly-defined type.
      # You can also include a base type.
      def query_params(base = SoberSwag::InputObject, &block)
        @query_params_class = make_input_object!(base, &block)
        action_module.const_set('QueryParams', @query_params_class)
      end

      ##
      # Does this route have query params defined?
      def query_params?
        !query_params_class.nil?
      end

      ##
      # Define the shape of the *path* parameters, using SoberSwag's type-definition scheme.
      # The block passed will be the body of a new subclass of `base` (defaulted to {SoberSwag::InputObject}).
      # Names of this should match the names in the path template originally passed to {SoberSwag::Controller::Route.new}
      def path_params(base = SoberSwag::InputObject, &block)
        @path_params_class = make_input_object!(base, &block)
        action_module.const_set('PathParams', @path_params_class)
      end

      ##
      # Does this route have path params defined?
      def path_params?
        !path_params_class.nil?
      end

      ##
      # Define the body of the action method in the controller.
      def action(&body)
        return @action if body.nil?

        @action ||= body
      end

      def description(desc = nil)
        return @description if desc.nil?

        @description = desc
      end

      def summary(sum = nil)
        return @summary if sum.nil?

        @summary = sum
      end

      ##
      # The container module for all the constants this will eventually define.
      # Each class generated by this Route will be defined within this module.
      def action_module
        @action_module ||= Module.new
      end

      ##
      # Define a serializer for a response with the given status code.
      # You may either give a serializer you defined elsewhere, or define one inline as if passed to
      # {SoberSwag::OutputObject.define}
      def response(status_code, description, serializer = nil, &block)
        status_key = Rack::Utils.status_code(status_code)

        raise ArgumentError, 'Response defiend!' if @response_serializers.key?(status_key)

        serializer ||= SoberSwag::OutputObject.define(&block)
        response_module.const_set(status_code.to_s.classify, serializer)
        @response_serializers[status_key] = serializer
        @response_descriptions[status_key] = description
      end

      ##
      # What you should call the module of this action in your controller
      def action_module_name
        action_name.to_s.classify
      end

      private

      def response_module
        @response_module ||= Module.new.tap { |m| action_module.const_set(:Response, m) }
      end

      def make_input_object!(base, &block)
        Class.new(base, &block).tap do |e|
          e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
        end
      end
    end
  end
end