require 'active_support/core_ext/module'

module Bearcat
  class ApiArray
    include Enumerable

    attr_reader :raw_response, :members

    def self.process_response(response, api_client)
      if response.body.is_a?(Array)
        self.new(response, api_client)
      elsif key = array_key(response)
        self.new(response, api_client, key)
      else
        make_indifferent(response.body)
      end
    end

    def self.make_indifferent(thing)
      if thing.is_a?(Array)
        thing.map { |v| make_indifferent(v) }
      elsif thing.is_a?(Hash)
        thing.with_indifferent_access
      else
        thing
      end
    end

    def initialize(response, api_client, array_key = nil)
      @api_client = api_client
      @raw_response = response
      @array_key = array_key
      @page_count = nil

      case response.status
      when 200..206
        @members = process_body(response)
        init_pages
      end
    end

    delegate :status, :headers, to: :raw_response

    def method
      raw_response.env[:method]
    end

    def [](i)
      @members[i]
    end

    def last
      @members.last
    end

    def each(&block)
      @members.each { |member| block.call(member) }
    end

    def pages?
      @link_hash['next'] || @link_hash['prev']
    end

    def page_count
      return nil unless @link_hash['last']

      uri = URI.parse(@link_hash['last'])
      params = CGI.parse(uri.query)
      params.dig('page', 0)&.to_i || 1
    end

    %w[next prev first last].each do |rel|
      define_method :"#{rel}_page" do
        load_page(rel)
      end
    end

    def each_page(page_count = nil, &block)
      return to_enum(:each_page, page_count) unless block_given?

      if pages?
        iterate_pages(page_count) do |page_response|
          @members = process_body(page_response)
          break unless @members.present?
          block.call(@members)
        end
      else
        block.call(@members)
      end
    end

    def all_pages_each(page_count = nil, &block)
      return to_enum(:all_pages_each, page_count) unless block_given?

      each_page(page_count) do |page|
        page.each &block
      end
    end

    def all_pages!(page_count = nil)
      if pages?
        @members = []
        iterate_pages(page_count) do |page_response|
          page_members = process_body(page_response)
          break unless page_members.present?
          @members.concat(page_members)
        end
        @link_hash = {}
      end
      self
    end

    protected

    def init_pages(link_header = headers[:link])
      @link_hash = {}
      if headers.has_key? 'Link'

        links = link_header.split(/,\s?/)

        links.each do |link|
          link_parts = link.split(/;\s?/)
          url = link_parts.shift.strip.gsub(/^<|>$/, '')
          rel = link_parts.find { |part| part.gsub('"', '').split('=').first == 'rel' }
          @link_hash[rel.gsub('"', '').split('=').last] = url
        end
      end
    end

    def get_page(url, params = {})
      params['per_page'] = @page_count unless params.key?('per_page') || !@page_count

      parsed_url = URI.parse(url)
      p = parse_url_params(parsed_url)
      u = url.gsub("?#{parsed_url.query}", '')

      # merge params
      p.merge!(params)

      @api_client.connection.send(:get) do |r|
        r.url(u, p)
      end
    end

    def load_page(rel)
      if @link_hash.has_key? rel
        response = get_page(@link_hash[rel])
        self.class.process_response(response, @api_client)
      end
    end

    def iterate_pages(per_page = @page_count)
      return to_enum(:iterate_pages, per_page) unless block_given?

      if per_page.present? && per_page != per_page_count && @link_hash['first']
        @page_count = per_page
        @raw_response = response = get_page(@link_hash['first'])
        yield response
        init_pages
      else
        yield @raw_response
      end

      while @link_hash['next']
        @raw_response = response = get_page(@link_hash['next'])
        yield response
        init_pages
      end
    end

    def per_page_count
      url = raw_response.env[:url]
      query_params = parse_url_params(url)
      query_params[:per_page]&.to_i
    end

    def parse_url_params(url)
      url = URI.parse(url) if url.is_a?(String)
      p = CGI.parse(url.query || '')
      p.default = nil

      # strip value out of array if value is an array and key doesn't have [] (parameter is not an array parameter)
      p.each { |k, v| p[k] = v.first if v.is_a?(Array) && k !~ /\[\]$/ }
      # remove [] from key names, this is copied from rails' {}.transform_keys!
      p.keys.each { |k| p[k.delete('[]')] = p.delete(k) }

      p.with_indifferent_access
    end

    #TODO: This is a quick fix for JSONAPI responses if we need to do this for anything else we need to do this a better way
    def self.array_key(response)
      key = nil
      if response.env[:method] == :get
        path = response.env[:url].path
        if path.match(/.*\/(courses||groups)\/\d+\/conferences/)
          key = 'conferences'
        elsif path.match(/.*\/accounts\/(?:(?:\d+~)?\d+|self)\/terms/)
          key = 'enrollment_terms'
        end
      end
      key
    end

    def process_body(response)
      if response.body.is_a?(Array)
        self.class.make_indifferent(response.body)
      elsif response.body.is_a?(Hash) && @array_key
        self.class.make_indifferent(response.body[@array_key])
      end
    end
  end
end