require 'active_support/all'

module JsonApiClient
  module Query
    class Builder

      attr_reader :klass
      delegate :key_formatter, to: :klass

      def initialize(klass, opts = {})
        @klass             = klass
        @primary_key       = opts.fetch( :primary_key, nil )
        @pagination_params = opts.fetch( :pagination_params, {} )
        @path_params       = opts.fetch( :path_params, {} )
        @additional_params = opts.fetch( :additional_params, {} )
        @filters           = opts.fetch( :filters, {} )
        @includes          = opts.fetch( :includes, [] )
        @orders            = opts.fetch( :orders, [] )
        @fields            = opts.fetch( :fields, [] )
      end

      def where(conditions = {})
        # pull out any path params here
        path_conditions = conditions.slice(*klass.prefix_params)
        unpathed_conditions = conditions.except(*klass.prefix_params)

        _new_scope( path_params: path_conditions, filters: unpathed_conditions )
      end

      def order(*args)
        _new_scope( orders: parse_orders(*args) )
      end

      def includes(*tables)
        _new_scope( includes: parse_related_links(*tables) )
      end

      def select(*fields)
        _new_scope( fields: parse_fields(*fields) )
      end

      def paginate(conditions = {})
        scope = _new_scope
        scope = scope.page(conditions[:page]) if conditions[:page]
        scope = scope.per(conditions[:per_page]) if conditions[:per_page]
        scope
      end

      def page(number)
        _new_scope( pagination_params: { klass.paginator.page_param => number || 1 } )
      end

      def per(size)
        _new_scope( pagination_params: { klass.paginator.per_page_param => size } )
      end

      def with_params(more_params)
        _new_scope( additional_params: more_params )
      end

      def first
        paginate(page: 1, per_page: 1).to_a.first
      end

      def last
        paginate(page: 1, per_page: 1).pages.last.to_a.last
      end

      def build(attrs = {})
        klass.new @path_params.merge(attrs.with_indifferent_access)
      end

      def create(attrs = {})
        klass.create @path_params.merge(attrs.with_indifferent_access)
      end

      def params
        filter_params
          .merge(pagination_params)
          .merge(includes_params)
          .merge(order_params)
          .merge(select_params)
          .merge(primary_key_params)
          .merge(path_params)
          .merge(additional_params)
      end

      def to_a
        @to_a ||= _fetch
      end
      alias all to_a

      def find(args = {})
        if klass.raise_on_blank_find_param && args.blank?
          raise Errors::NotFound, nil, 'blank .find param'
        end

        case args
        when Hash
          scope = where(args)
        else
          scope = _new_scope( primary_key: args )
        end

        scope._fetch
      end

      def method_missing(method_name, *args, &block)
        to_a.send(method_name, *args, &block)
      end

      def hash
        [
          klass,
          params
        ].hash
      end

      def ==(other)
        return false unless other.is_a?(self.class)

        hash == other.hash
      end
      alias_method :eql?, :==

      protected

      def _fetch
        klass.requestor.get(params)
      end

      private

      def _new_scope( opts = {} )
        self.class.new( @klass,
             primary_key:       opts.fetch( :primary_key, @primary_key ),
             pagination_params: @pagination_params.merge( opts.fetch( :pagination_params, {} ) ),
             path_params:       @path_params.merge( opts.fetch( :path_params, {} ) ),
             additional_params: @additional_params.deep_merge( opts.fetch( :additional_params, {} ) ),
             filters:           @filters.merge( opts.fetch( :filters, {} ) ),
             includes:          @includes + opts.fetch( :includes, [] ),
             orders:            @orders + opts.fetch( :orders, [] ),
             fields:            @fields + opts.fetch( :fields, [] ) )
      end

      def path_params
        @path_params.empty? ? {} : {path: @path_params}
      end

      def additional_params
        @additional_params
      end

      def primary_key_params
        return {} unless @primary_key

        @primary_key.is_a?(Array) ?
          {klass.primary_key.to_s.pluralize.to_sym => @primary_key.join(",")} :
          {klass.primary_key => @primary_key}
      end

      def pagination_params
        if klass.paginator.ancestors.include?(Paginating::Paginator)
          # Original Paginator inconsistently wraps pagination params here. Keeping
          # default behavior for now so as not to break backward compatibility.
          @pagination_params.empty? ? {} : {page: @pagination_params}
        else
          @pagination_params
        end
      end

      def includes_params
        @includes.empty? ? {} : {include: @includes.join(",")}
      end

      def filter_params
        @filters.empty? ? {} : {filter: @filters}
      end

      def order_params
        @orders.empty? ? {} : {sort: @orders.join(",")}
      end

      def select_params
        if @fields.empty?
          {}
        else
          field_result = Hash.new { |h,k| h[k] = [] }
          @fields.each do |field|
            if field.is_a? Hash
              field.each do |k,v|
                field_result[k.to_s] << v
                field_result[k.to_s] = field_result[k.to_s].flatten
              end
            else
              field_result[klass.table_name] << field
            end
          end
          field_result.each { |k,v| field_result[k] = v.join(',') }
          {fields: field_result}
        end
      end

      def parse_related_links(*tables)
        Utils.parse_includes(klass, *tables)
      end

      def parse_orders(*args)
        args.map do |arg|
          case arg
          when Hash
            arg.map do |k, v|
              operator = (v == :desc ? "-" : "")
              "#{operator}#{k}"
            end
          else
            "#{arg}"
          end
        end.flatten
      end

      def parse_fields(*fields)
        fields = fields.split(',') if fields.is_a? String
        fields.map do |field|
          case field
          when Hash
            field.each do |k,v|
              field[k] = parse_fields(v)
            end
            field
          else
            Array(field).flatten.map { |i| i.to_s.split(",") }.flatten.map(&:strip)
          end
        end.flatten
      end

    end
  end
end
