module Braintree
  module Util
    def self.extract_attribute_as_array(hash, attribute)
      raise UnexpectedError.new("Unprocessable entity due to an invalid request") if hash.nil?
      value = hash.has_key?(attribute) ? hash.delete(attribute) : []
      value.is_a?(Array) ? value : [value]
    end

    def self.hash_to_query_string(hash, namespace = nil)
      hash.collect do |key, value|
        full_key = namespace ? "#{namespace}[#{key}]" : key
        if value.is_a?(Hash)
          hash_to_query_string(value, full_key)
        else
          url_encode(full_key) + "=" + url_encode(value)
        end
      end.sort * "&"
    end

    def self.parse_query_string(qs)
      qs.split("&").inject({}) do |result, couplet|
        pair = couplet.split("=")
        result[CGI.unescape(pair[0]).to_sym] = CGI.unescape(pair[1] || "")
        result
      end
    end

    def self.url_decode(text)
      CGI.unescape text.to_s.to_str
    end

    def self.url_encode(text)
      CGI.escape text.to_s.to_str
    end

    def self.symbolize_keys(hash)
      hash.inject({}) do |new_hash, (key, value)|
        if value.is_a?(Hash)
          value = symbolize_keys(value)
        elsif value.is_a?(Array) && value.all? { |v| v.is_a?(Hash) }
          value = value.map { |v| symbolize_keys(v) }
        end

        new_hash.merge(key.to_sym => value)
      end
    end

    def self.raise_exception_for_status_code(status_code, message=nil)
      case status_code.to_i
      when 401
        raise AuthenticationError
      when 403
        raise AuthorizationError, message
      when 404
        raise NotFoundError
      when 408
        raise RequestTimeoutError
      when 426
        raise UpgradeRequiredError, "Please upgrade your client library."
      when 429
        raise TooManyRequestsError
      when 500
        raise ServerError
      when 503
        raise ServiceUnavailableError
      when 504
        raise GatewayTimeoutError
      else
        raise UnexpectedError, "Unexpected HTTP_RESPONSE #{status_code.to_i}"
      end
    end

    def self.raise_exception_for_graphql_error(response)
      return if !response[:errors]

      for error in response[:errors]
        if error[:extensions] && error[:extensions][:errorClass]
          case error[:extensions][:errorClass]
          when "VALIDATION"
            next # skip raising an error if it is a validation error
          when "AUTHENTICATION"
            raise AuthenticationError
          when "AUTHORIZATION"
            raise AuthorizationError, error[:message]
          when "NOT_FOUND"
            raise NotFoundError
          when "UNSUPPORTED_CLIENT"
            raise UpgradeRequiredError, "Please upgrade your client library."
          when "RESOURCE_LIMIT"
            raise TooManyRequestsError
          when "INTERNAL"
            raise ServerError
          when "SERVICE_AVAILABILITY"
            raise ServiceUnavailableError
          else
            raise UnexpectedError, "Unexpected Response: #{error[:message]}"
          end
        else
         raise UnexpectedError, "Unexpected Response: #{error[:message]}"
        end
      end
    end

    def self.to_big_decimal(decimal)
      case decimal
      when BigDecimal, NilClass
        decimal
      when String
        BigDecimal(decimal)
      else
        raise ArgumentError, "Argument must be a String or BigDecimal"
      end
    end

    def self.inspect_amount(amount)
      amount ? "amount: #{amount.to_s("F").inspect}" : "amount: nil"
    end

    def self.verify_keys(valid_keys, hash)
      invalid_keys = _get_invalid_keys(valid_keys, hash)
      if invalid_keys.any?
        sorted = invalid_keys.sort_by { |k| k.to_s }.join(", ")
        raise ArgumentError, "invalid keys: #{sorted}"
      end
    end

    def self.keys_valid?(valid_keys, hash)
      invalid_keys = _get_invalid_keys(valid_keys, hash)

      !invalid_keys.any?
    end

    def self.replace_key(hash, target_key, replacement_key)
      hash.inject({}) do |new_hash, (key, value)|
        if value.is_a?(Hash)
          value = replace_key(value, target_key, replacement_key)
        end

        key = replacement_key if key == target_key
        new_hash.merge(key => value)
      end
    end

    def self._flatten_valid_keys(valid_keys, namespace = nil)
      valid_keys.inject([]) do |result, key|
        if key.is_a?(Hash)
          full_key = key.keys[0]
          full_key = (namespace ? "#{namespace}[#{full_key}]" : full_key)
          nested_keys = key.values[0]
          if nested_keys.is_a?(Array)
            result += _flatten_valid_keys(nested_keys, full_key)
          else
            result << "#{full_key}[#{nested_keys}]"
          end
        else
          result << (namespace ? "#{namespace}[#{key}]" : key.to_s)
        end
        result
      end.sort
    end

    def self._flatten_hash_keys(element, namespace = nil)
      element = [element] if element.is_a?(String)

      element.inject([]) do |result, (key, value)|
        full_key = (namespace ? "#{namespace}[#{key}]" : key.to_s)
        if value.is_a?(Hash)
          result += _flatten_hash_keys(value, full_key)
        elsif value.is_a?(Array)
          value.each do |item|
            result += _flatten_hash_keys(item, full_key)
          end
        else
          result << full_key
        end
        result
      end.sort
    end

    def self._remove_wildcard_keys(valid_keys, invalid_keys)
      wildcard_keys = valid_keys.select { |k| k.include? "[_any_key_]" }
      return invalid_keys if wildcard_keys.empty?
      wildcard_keys.map! { |wk| wk.sub "[_any_key_]", "" }
      invalid_keys.select do |invalid_key|
        wildcard_keys.all? do |wildcard_key|
          invalid_key.index(wildcard_key) != 0
        end
      end
    end

    def self._get_invalid_keys(valid_keys, hash)
      flattened_valid_keys = _flatten_valid_keys(valid_keys)
      keys = _flatten_hash_keys(hash) - flattened_valid_keys
      _remove_wildcard_keys(flattened_valid_keys, keys)
    end

    module IdEquality
      def ==(other)
        return false unless other.is_a?(self.class)
        id == other.id
      end
    end

    module TokenEquality
      def ==(other)
        return false unless other.is_a?(self.class)
        token == other.token
      end
    end
  end
end
