require 'jsonapi/operation'
require 'jsonapi/paginator'

module JSONAPI
  class RequestParser
    attr_accessor :fields, :include, :filters, :sort_criteria, :errors, :operations,
                  :resource_klass, :context, :paginator, :source_klass, :source_id,
                  :include_directives, :params, :warnings, :server_error_callbacks

    def initialize(params = nil, options = {})
      @params = params
      @context = options[:context]
      @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
      @errors = []
      @warnings = []
      @operations = []
      @fields = {}
      @filters = {}
      @sort_criteria = nil
      @source_klass = nil
      @source_id = nil
      @include_directives = nil
      @paginator = nil
      @id = nil
      @server_error_callbacks = options.fetch(:server_error_callbacks, [])

      setup_action(@params)
    end

    def setup_action(params)
      return if params.nil?

      @resource_klass ||= Resource.resource_for(params[:controller]) if params[:controller]

      setup_action_method_name = "setup_#{params[:action]}_action"
      if respond_to?(setup_action_method_name)
        send(setup_action_method_name, params)
      end
    rescue ActionController::ParameterMissing => e
      @errors.concat(JSONAPI::Exceptions::ParameterMissing.new(e.param).errors)
    end

    def setup_index_action(params)
      parse_fields(params[:fields])
      parse_include_directives(params[:include])
      set_default_filters
      parse_filters(params[:filter])
      parse_sort_criteria(params[:sort])
      parse_pagination(params[:page])
      add_find_operation
    end

    def setup_get_related_resource_action(params)
      initialize_source(params)
      parse_fields(params[:fields])
      parse_include_directives(params[:include])
      set_default_filters
      parse_filters(params[:filter])
      parse_sort_criteria(params[:sort])
      parse_pagination(params[:page])
      add_show_related_resource_operation(params[:relationship])
    end

    def setup_get_related_resources_action(params)
      initialize_source(params)
      parse_fields(params[:fields])
      parse_include_directives(params[:include])
      set_default_filters
      parse_filters(params[:filter])
      parse_sort_criteria(params[:sort])
      parse_pagination(params[:page])
      add_show_related_resources_operation(params[:relationship])
    end

    def setup_show_action(params)
      parse_fields(params[:fields])
      parse_include_directives(params[:include])
      @id = params[:id]
      add_show_operation
    end

    def setup_show_relationship_action(params)
      add_show_relationship_operation(params[:relationship], params.require(@resource_klass._as_parent_key))
    end

    def setup_create_action(params)
      parse_fields(params[:fields])
      parse_include_directives(params[:include])
      parse_add_operation(params.require(:data))
    end

    def setup_create_relationship_action(params)
      parse_modify_relationship_action(params, :add)
    end

    def setup_update_relationship_action(params)
      parse_modify_relationship_action(params, :update)
    end

    def setup_update_action(params)
      parse_fields(params[:fields])
      parse_include_directives(params[:include])
      parse_replace_operation(params.require(:data), params[:id])
    end

    def setup_destroy_action(params)
      parse_remove_operation(params)
    end

    def setup_destroy_relationship_action(params)
      parse_modify_relationship_action(params, :remove)
    end

    def parse_modify_relationship_action(params, modification_type)
      relationship_type = params.require(:relationship)
      parent_key = params.require(@resource_klass._as_parent_key)
      relationship = @resource_klass._relationship(relationship_type)

      # Removals of to-one relationships are done implicitly and require no specification of data
      data_required = !(modification_type == :remove && relationship.is_a?(JSONAPI::Relationship::ToOne))

      if data_required
        data = params.fetch(:data)
        object_params = { relationships: { format_key(relationship.name) => { data: data } } }
        verified_params = parse_params(object_params, updatable_fields)

        parse_arguments = [verified_params, relationship, parent_key]
      else
        parse_arguments = [params, relationship, parent_key]
      end

      send(:"parse_#{modification_type}_relationship_operation", *parse_arguments)
    end

    def initialize_source(params)
      @source_klass = Resource.resource_for(params.require(:source))
      @source_id = @source_klass.verify_key(params.require(@source_klass._as_parent_key), @context)
    end

    def parse_pagination(page)
      paginator_name = @resource_klass._paginator
      @paginator = JSONAPI::Paginator.paginator_for(paginator_name).new(page) unless paginator_name == :none
    rescue JSONAPI::Exceptions::Error => e
      @errors.concat(e.errors)
    end

    def parse_fields(fields)
      return if fields.nil?

      extracted_fields = {}

      # Extract the fields for each type from the fields parameters
      if fields.is_a?(ActionController::Parameters)
        fields.each do |field, value|
          resource_fields = value.split(',') unless value.nil? || value.empty?
          extracted_fields[field] = resource_fields
        end
      else
        fail JSONAPI::Exceptions::InvalidFieldFormat.new
      end

      # Validate the fields
      extracted_fields.each do |type, values|
        underscored_type = unformat_key(type)
        extracted_fields[type] = []
        begin
          if type != format_key(type)
            fail JSONAPI::Exceptions::InvalidResource.new(type)
          end
          type_resource = Resource.resource_for(@resource_klass.module_path + underscored_type.to_s)
        rescue NameError
          @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
        rescue JSONAPI::Exceptions::InvalidResource => e
          @errors.concat(e.errors)
        end

        if type_resource.nil?
          @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
        else
          unless values.nil?
            valid_fields = type_resource.fields.collect { |key| format_key(key) }
            values.each do |field|
              if valid_fields.include?(field)
                extracted_fields[type].push unformat_key(field)
              else
                @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, field).errors)
              end
            end
          else
            @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, 'nil').errors)
          end
        end
      end

      @fields = extracted_fields.deep_transform_keys { |key| unformat_key(key) }
    end

    def check_include(resource_klass, include_parts)
      relationship_name = unformat_key(include_parts.first)

      relationship = resource_klass._relationship(relationship_name)
      if relationship && format_key(relationship_name) == include_parts.first
        unless include_parts.last.empty?
          check_include(Resource.resource_for(resource_klass.module_path + relationship.class_name.to_s.underscore), include_parts.last.partition('.'))
        end
      else
        @errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
                                                               include_parts.first).errors)
      end
    end

    def parse_include_directives(include)
      return if include.nil?

      unless JSONAPI.configuration.allow_include
        fail JSONAPI::Exceptions::ParametersNotAllowed.new([:include])
      end

      included_resources = CSV.parse_line(include)
      return if included_resources.nil?

      include = []
      included_resources.each do |included_resource|
        check_include(@resource_klass, included_resource.partition('.'))
        include.push(unformat_key(included_resource).to_s)
      end

      @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, include)
    end

    def parse_filters(filters)
      return unless filters

      unless JSONAPI.configuration.allow_filter
        fail JSONAPI::Exceptions::ParametersNotAllowed.new([:filter])
      end

      unless filters.class.method_defined?(:each)
        @errors.concat(JSONAPI::Exceptions::InvalidFiltersSyntax.new(filters).errors)
        return
      end

      filters.each do |key, value|
        filter = unformat_key(key)
        if @resource_klass._allowed_filter?(filter)
          @filters[filter] = value
        else
          @errors.concat(JSONAPI::Exceptions::FilterNotAllowed.new(filter).errors)
        end
      end
    end

    def set_default_filters
      @resource_klass._allowed_filters.each do |filter, opts|
        next if opts[:default].nil? || !@filters[filter].nil?
        @filters[filter] = opts[:default]
      end
    end

    def parse_sort_criteria(sort_criteria)
      return unless sort_criteria.present?

      unless JSONAPI.configuration.allow_sort
        fail JSONAPI::Exceptions::ParametersNotAllowed.new([:sort])
      end

      @sort_criteria = CSV.parse_line(URI.unescape(sort_criteria)).collect do |sort|
        if sort.start_with?('-')
          sort_criteria = { field: unformat_key(sort[1..-1]).to_s }
          sort_criteria[:direction] = :desc
        else
          sort_criteria = { field: unformat_key(sort).to_s }
          sort_criteria[:direction] = :asc
        end

        check_sort_criteria(@resource_klass, sort_criteria)
        sort_criteria
      end
    end

    def check_sort_criteria(resource_klass, sort_criteria)
      sort_field = sort_criteria[:field]
      sortable_fields = resource_klass.sortable_fields(context)

      unless sortable_fields.include? sort_field.to_sym
        @errors.concat(JSONAPI::Exceptions::InvalidSortCriteria
                         .new(format_key(resource_klass._type), sort_field).errors)
      end
    end

    def add_find_operation
      @operations.push JSONAPI::Operation.new(:find,
        @resource_klass,
        context: @context,
        filters: @filters,
        include_directives: @include_directives,
        sort_criteria: @sort_criteria,
        paginator: @paginator,
        fields: @fields
      )
    end

    def add_show_operation
      @operations.push JSONAPI::Operation.new(:show,
        @resource_klass,
        context: @context,
        id: @id,
        include_directives: @include_directives,
        fields: @fields
      )
    end

    def add_show_relationship_operation(relationship_type, parent_key)
      @operations.push JSONAPI::Operation.new(:show_relationship,
        @resource_klass,
        context: @context,
        relationship_type: relationship_type,
        parent_key: @resource_klass.verify_key(parent_key)
      )
    end

    def add_show_related_resource_operation(relationship_type)
      @operations.push JSONAPI::Operation.new(:show_related_resource,
        @resource_klass,
        context: @context,
        relationship_type: relationship_type,
        source_klass: @source_klass,
        source_id: @source_id,
        fields: @fields,
        include_directives: @include_directives
      )
    end

    def add_show_related_resources_operation(relationship_type)
      @operations.push JSONAPI::Operation.new(:show_related_resources,
        @resource_klass,
        context: @context,
        relationship_type: relationship_type,
        source_klass: @source_klass,
        source_id: @source_id,
        filters: @source_klass.verify_filters(@filters, @context),
        sort_criteria: @sort_criteria,
        paginator: @paginator,
        fields: @fields,
        include_directives: @include_directives
      )
    end

    # TODO: Please remove after `createable_fields` is removed
    # :nocov:
    def creatable_fields
      if @resource_klass.respond_to?(:createable_fields)
        creatable_fields = @resource_klass.createable_fields(@context)
      else
        creatable_fields = @resource_klass.creatable_fields(@context)
      end
    end
    # :nocov:

    def parse_add_operation(data)
      Array.wrap(data).each do |params|
        verify_type(params[:type])

        data = parse_params(params, creatable_fields)
        @operations.push JSONAPI::Operation.new(:create_resource,
          @resource_klass,
          context: @context,
          data: data,
          fields: @fields,
          include_directives: @include_directives
        )
      end
    rescue JSONAPI::Exceptions::Error => e
      @errors.concat(e.errors)
    end

    def verify_type(type)
      if type.nil?
        fail JSONAPI::Exceptions::ParameterMissing.new(:type)
      elsif unformat_key(type).to_sym != @resource_klass._type
        fail JSONAPI::Exceptions::InvalidResource.new(type)
      end
    end

    def parse_to_one_links_object(raw)
      if raw.nil?
        return {
          type: nil,
          id: nil
        }
      end

      if !(raw.is_a?(Hash) || raw.is_a?(ActionController::Parameters)) ||
         raw.keys.length != 2 || !(raw.key?('type') && raw.key?('id'))
        fail JSONAPI::Exceptions::InvalidLinksObject.new
      end

      {
        type: unformat_key(raw['type']).to_s,
        id: raw['id']
      }
    end

    def parse_to_many_links_object(raw)
      fail JSONAPI::Exceptions::InvalidLinksObject.new if raw.nil?

      links_object = {}
      if raw.is_a?(Array)
        raw.each do |link|
          link_object = parse_to_one_links_object(link)
          links_object[link_object[:type]] ||= []
          links_object[link_object[:type]].push(link_object[:id])
        end
      else
        fail JSONAPI::Exceptions::InvalidLinksObject.new
      end
      links_object
    end

    def parse_params(params, allowed_fields)
      verify_permitted_params(params, allowed_fields)

      checked_attributes = {}
      checked_to_one_relationships = {}
      checked_to_many_relationships = {}

      params.each do |key, value|
        case key.to_s
        when 'relationships'
          value.each do |link_key, link_value|
            param = unformat_key(link_key)
            relationship = @resource_klass._relationship(param)

            if relationship.is_a?(JSONAPI::Relationship::ToOne)
              checked_to_one_relationships[param] = parse_to_one_relationship(link_value, relationship)
            elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
              parse_to_many_relationship(link_value, relationship) do |result_val|
                checked_to_many_relationships[param] = result_val
              end
            end
          end
        when 'id'
          checked_attributes['id'] = unformat_value(:id, value)
        when 'attributes'
          value.each do |key, value|
            param = unformat_key(key)
            checked_attributes[param] = unformat_value(param, value)
          end
        end
      end

      return {
        'attributes' => checked_attributes,
        'to_one' => checked_to_one_relationships,
        'to_many' => checked_to_many_relationships
      }.deep_transform_keys { |key| unformat_key(key) }
    end

    def parse_to_one_relationship(link_value, relationship)
      if link_value.nil?
        linkage = nil
      else
        linkage = link_value[:data]
      end

      links_object = parse_to_one_links_object(linkage)
      if !relationship.polymorphic? && links_object[:type] && (links_object[:type].to_s != relationship.type.to_s)
        fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
      end

      unless links_object[:id].nil?
        resource = self.resource_klass || Resource
        relationship_resource = resource.resource_for(unformat_key(links_object[:type]).to_s)
        relationship_id = relationship_resource.verify_key(links_object[:id], @context)
        if relationship.polymorphic?
          { id: relationship_id, type: unformat_key(links_object[:type].to_s) }
        else
          relationship_id
        end
      else
        nil
      end
    end

    def parse_to_many_relationship(link_value, relationship, &add_result)
      if link_value.is_a?(Array) && link_value.length == 0
        linkage = []
      elsif (link_value.is_a?(Hash) || link_value.is_a?(ActionController::Parameters))
        linkage = link_value[:data]
      else
        fail JSONAPI::Exceptions::InvalidLinksObject.new
      end

      links_object = parse_to_many_links_object(linkage)

      # Since we do not yet support polymorphic to_many relationships we will raise an error if the type does not match the
      # relationship's type.
      # ToDo: Support Polymorphic relationships

      if links_object.length == 0
        add_result.call([])
      else
        if links_object.length > 1 || !links_object.has_key?(unformat_key(relationship.type).to_s)
          fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
        end

        links_object.each_pair do |type, keys|
          relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(type).to_s)
          add_result.call relationship_resource.verify_keys(keys, @context)
        end
      end
    end

    def unformat_value(attribute, value)
      value_formatter = JSONAPI::ValueFormatter.value_formatter_for(@resource_klass._attribute_options(attribute)[:format])
      value_formatter.unformat(value)
    end

    def verify_permitted_params(params, allowed_fields)
      formatted_allowed_fields = allowed_fields.collect { |field| format_key(field).to_sym }
      params_not_allowed = []

      params.each do |key, value|
        case key.to_s
        when 'relationships'
          value.keys.each do |links_key|
            unless formatted_allowed_fields.include?(links_key.to_sym)
              params_not_allowed.push(links_key)
              unless JSONAPI.configuration.raise_if_parameters_not_allowed
                value.delete links_key
              end
            end
          end
        when 'attributes'
          value.each do |attr_key, attr_value|
            unless formatted_allowed_fields.include?(attr_key.to_sym)
              params_not_allowed.push(attr_key)
              unless JSONAPI.configuration.raise_if_parameters_not_allowed
                value.delete attr_key
              end
            end
          end
        when 'type'
        when 'id'
          unless formatted_allowed_fields.include?(:id)
            params_not_allowed.push(:id)
            unless JSONAPI.configuration.raise_if_parameters_not_allowed
              params.delete :id
            end
          end
        else
          params_not_allowed.push(key)
        end
      end

      if params_not_allowed.length > 0
        if JSONAPI.configuration.raise_if_parameters_not_allowed
          fail JSONAPI::Exceptions::ParametersNotAllowed.new(params_not_allowed)
        else
          params_not_allowed_warnings = params_not_allowed.map do |key|
            JSONAPI::Warning.new(code: JSONAPI::PARAM_NOT_ALLOWED,
                                 title: 'Param not allowed',
                                 detail: "#{key} is not allowed.")
          end
          self.warnings.concat(params_not_allowed_warnings)
        end
      end
    end

    # TODO: Please remove after `updateable_fields` is removed
    # :nocov:
    def updatable_fields
      if @resource_klass.respond_to?(:updateable_fields)
        @resource_klass.updateable_fields(@context)
      else
        @resource_klass.updatable_fields(@context)
      end
    end
    # :nocov:

    def parse_add_relationship_operation(verified_params, relationship, parent_key)
      if relationship.is_a?(JSONAPI::Relationship::ToMany)
        @operations.push JSONAPI::Operation.new(:create_to_many_relationship,
          resource_klass,
          context: @context,
          resource_id: parent_key,
          relationship_type: relationship.name,
          data: verified_params[:to_many].values[0]
        )
      end
    end

    def parse_update_relationship_operation(verified_params, relationship, parent_key)
      options = {
        context: @context,
        resource_id: parent_key,
        relationship_type: relationship.name
      }

      if relationship.is_a?(JSONAPI::Relationship::ToOne)
        if relationship.polymorphic?
          options[:key_value] = verified_params[:to_one].values[0][:id]
          options[:key_type] = verified_params[:to_one].values[0][:type]

          operation_type = :replace_polymorphic_to_one_relationship
        else
          options[:key_value] = verified_params[:to_one].values[0]
          operation_type = :replace_to_one_relationship
        end
      elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
        unless relationship.acts_as_set
          fail JSONAPI::Exceptions::ToManySetReplacementForbidden.new
        end
        options[:data] = verified_params[:to_many].values[0]
        operation_type = :replace_to_many_relationship
      end

      @operations.push JSONAPI::Operation.new(operation_type, resource_klass, options)
    end

    def parse_single_replace_operation(data, keys, id_key_presence_check_required: true)
      fail JSONAPI::Exceptions::MissingKey.new if data[:id].nil?

      key = data[:id].to_s
      if id_key_presence_check_required && !keys.include?(key)
        fail JSONAPI::Exceptions::KeyNotIncludedInURL.new(key)
      end

      data.delete(:id) unless keys.include?(:id)

      verify_type(data[:type])

      @operations.push JSONAPI::Operation.new(:replace_fields,
        @resource_klass,
        context: @context,
        resource_id: key,
        data: parse_params(data, updatable_fields),
        fields: @fields,
        include_directives: @include_directives
      )
    end

    def parse_replace_operation(data, keys)
      if data.is_a?(Array)
        fail JSONAPI::Exceptions::CountMismatch if keys.count != data.count

        data.each do |object_params|
          parse_single_replace_operation(object_params, keys)
        end
      else
        parse_single_replace_operation(data, [keys],
                                       id_key_presence_check_required: keys.present?)
      end

    rescue JSONAPI::Exceptions::Error => e
      @errors.concat(e.errors)
    end

    def parse_remove_operation(params)
      keys = parse_key_array(params.require(:id))

      keys.each do |key|
        @operations.push JSONAPI::Operation.new(:remove_resource,
          @resource_klass,
          context: @context,
          resource_id: key
        )
      end
    rescue JSONAPI::Exceptions::Error => e
      @errors.concat(e.errors)
    end

    def parse_remove_relationship_operation(params, relationship, parent_key)
      operation_base_args = [resource_klass].push(
        context: @context,
        resource_id: parent_key,
        relationship_type: relationship.name
      )

      if relationship.is_a?(JSONAPI::Relationship::ToMany)
        keys = params[:to_many].values[0]
        keys.each do |key|
          operation_args = operation_base_args.dup
          operation_args[1] = operation_args[1].merge(associated_key: key)
          @operations.push JSONAPI::Operation.new(:remove_to_many_relationship,
            *operation_args
          )
        end
      else
        @operations.push JSONAPI::Operation.new(:remove_to_one_relationship,
          *operation_base_args
        )
      end
    end

    def parse_key_array(raw)
      @resource_klass.verify_keys(raw.split(/,/), context)
    end

    def format_key(key)
      @key_formatter.format(key)
    end

    def unformat_key(key)
      unformatted_key = @key_formatter.unformat(key)
      unformatted_key.nil? ? nil : unformatted_key.to_sym
    end
  end
end