module ResoTransport
  Query = Struct.new(:resource) do

    def all(*contexts, &block)
      new_query_context('and')
      instance_eval(&block)
      clear_query_context
      return self
    end

    def any(&block)
      new_query_context('or')
      instance_eval(&block)
      clear_query_context
      return self
    end

    [:eq, :ne, :gt, :ge, :lt, :le].each do |op|
      define_method(op) do |conditions|
        conditions.each_pair do |k,v|
          current_query_context << "#{k} #{op} #{encode_value(k, v)}"
        end
        return self
      end
    end

    def limit(size)
      options[:top] = size
      return self
    end

    def offset(size)
      options[:skip] = size
      return self
    end

    def order(field, dir=nil)
      options[:orderby] = [field, dir].join(" ").strip
      return self
    end

    def include_count
      options[:count] = true
      return self
    end

    def select(*fields)
      os = options.fetch(:select, "").split(",")
      options[:select] = (os + Array(fields)).uniq.join(",")

      return self
    end

    def expand(*names)
      ex = options.fetch(:expand, "").split(",")
      options[:expand] = (ex + Array(names)).uniq.join(",")

      return self
    end

    def count
      p compile_params
      limit(1).include_count
      resp = resource.get(compile_params)
      parsed_body = JSON.parse(resp.body)
      parsed_body.fetch("@odata.count", 0)
    end

    def results
      resp = execute

      if resp[:success]
        resp[:results]
      else
        puts resp[:meta]
        raise "Request Failed"
      end
    end

    def execute
      resp = resource.get(compile_params)
      parsed_body = JSON.parse(resp.body)
      results = Array(parsed_body.delete("value"))

      {
        success: resp.success? && !parsed_body.has_key?("error"),
        meta: parsed_body,
        results: resource.parse(results)
      }
    end

    def new_query_context(context)
      @last_query_context ||= 0
      @current_query_context = @last_query_context + 1
      sub_queries[@current_query_context][:context] = context
    end

    def clear_query_context
      @last_query_context = @current_query_context
      @current_query_context = nil
    end

    def current_query_context
      @current_query_context ||= nil
      sub_queries[@current_query_context || :global][:criteria]
    end

    def options
      @options ||= {}
    end

    def sub_queries
      @sub_queries ||= Hash.new {|h,k| h[k] = { context: 'and', criteria: [] } }
    end

    def compile_filters
      groups = sub_queries.dup
      global = groups.delete(:global)
      filter_groups = groups.values

      filter_chunks = []

      if global && global[:criteria]&.any?
        filter_chunks << global[:criteria].join(" #{global[:context]} ")  
      end

      filter_chunks << filter_groups.map do |g|
        "(#{g[:criteria].join(" #{g[:context]} ")})"
      end.join(" and ")

      filter_chunks.reject {|c| c == ""}.join(" and ")
    end

    def compile_params
      params = {}

      options.each_pair do |k,v|
        params["$#{k}"] = v
      end

      if !sub_queries.empty?
        params["$filter"] = compile_filters
      end

      params
    end

    def encode_value(key, v)
      field = resource.property(key.to_s)
      raise "Couldn't find property #{key} for #{resource.name}" if field.nil?
      field.encode(v)
    end

  end
end
