class LogicalModel
  module RESTActions

    def self.included(base)
      base.send(:include, InstanceMethods)
      base.send(:extend, ClassMethods)
    end

    module InstanceMethods

      def create(params={})
        run_callbacks :save do
          run_callbacks :create do
            _create(params)
          end
        end
      end

      def save
        run_callbacks :save do
          run_callbacks new_record?? :create : :update do
            _save
          end
        end
      end

      def destroy(params={})
        run_callbacks :destroy do
          _destroy(params)
        end
      end

      # @param params [Hash] parameters to be sent to service
      def update(params)
        run_callbacks :save do
          run_callbacks :update do
            _update(params)
          end
        end

      end

      #
      # creates model.
      #
      # returns:
      # @return false if model invalid
      # @return nil if there was a connection problem
      # @return created model ID if successfull
      #
      # @example Usage:
      #   @person = Person.new(params[:person])
      #   @person.create( non_attribute_param: "value" )
      def _create(params = {})
        return false unless valid?

        params = { self.json_root => self.attributes }.merge(params)
        params = self.class.merge_key(params)

        response = nil
        Timeout::timeout(self.class.timeout/1000) do
          response = Typhoeus::Request.post( self.class.resource_uri, :body => params, :timeout => self.class.timeout )
        end
        self.last_response_code = response.code
        if response.code == 201 || response.code == 202
          log_ok(response)
          if self.respond_to?('id=')
            self.id = ActiveSupport::JSON.decode(response.body)["id"]
          else
            true
          end
        elsif response.code == 400
          log_failed(response)
          ws_errors = ActiveSupport::JSON.decode(response.body)["errors"]
          ws_errors.each_key do |k|
            self.errors.add k, ws_errors[k]
          end
          return false
        else
          log_failed(response)
          return nil
        end
      rescue Timeout::Error
        self.class.logger.warn "timeout"
        return nil
      end

      # Updates Objects attributes, this will only send attributes passed as arguments
      #
      #
      # Returns false if Object#valid? is false.
      # Returns updated object if successfull.
      # Returns nil if update failed
      #
      # Usage:
      #   @person.update(params[:person])
      def _update(params)

        self.attributes = params[self.json_root]

        return false unless valid?

        params = self.class.merge_key(params)

        response = nil
        Timeout::timeout(self.class.timeout/1000) do
          response = Typhoeus::Request.put( self.class.resource_uri(id),
                                            :params => params,
                                            :timeout => self.class.timeout )
        end

        if response.code == 200
          log_ok(response)
          return self
        else
          log_failed(response)
          return nil
        end

      rescue Timeout::Error
        self.class.logger.warn("request timed out")
        return nil
      end

      # Saves Objects attributes
      #
      #
      # Returns false if Object#valid? is false.
      # Returns updated object if successfull.
      # Returns nil if update failed
      #
      # Usage:
      #   @person.save
      def _save
        self.attributes = attributes

        return false unless valid?

        sending_params = self.attributes
        sending_params.delete(:id)

        params = { self.json_root => sending_params }
        params = self.class.merge_key(params)
        response = nil
        Timeout::timeout(self.class.timeout/1000) do
          response = Typhoeus::Request.put( self.class.resource_uri(id), :params => params, :timeout => self.class.timeout )
        end
        if response.code == 200
          log_ok(response)
          return self
        else
          log_failed(response)
          return nil
        end
      rescue Timeout::Error
        self.class.logger.warn "timeout"
        return nil
      end

      # Destroy object
      #
      # Usage:
      #   @person.destroy
      def _destroy(params={})
        self.class.delete(self.id,params)
      end

    end

    module ClassMethods

      attr_accessor :enable_delete_multiple

      #  ============================================================================================================
      #  Following methods are API specific.
      #  They assume we are using a RESTfull API.
      #  for get, put, delete :id is expected
      #  for post, put attributes are excepted under class_name directly.
      #    eg. put( {:id => 1, :class_name => {:attr => "new value for attr"}} )
      #
      #  On error (400) a "errors" key is expected in response
      #  ============================================================================================================

      # @param options [Hash] will be forwarded to API
      def async_all(options={})
        options = self.merge_key(options)
        request = Typhoeus::Request.new(resource_uri, :params => options)
        request.on_complete do |response|
          if response.code >= 200 && response.code < 400
            log_ok(response)

            result_set = self.from_json(response.body)
            collection = result_set[:collection]

            yield collection
          else
            log_failed(response)
          end
        end
        self.hydra.queue(request)
      end

      def all(options={})
        result = nil
        self.retries.times do
          begin
            async_all(options){|i| result = i}
            Timeout::timeout(self.timeout/1000) do
              self.hydra.run
            end
            break unless result.nil?
          rescue Timeout::Error
            self.logger.warn("timeout")
            result = nil
          end
        end
        result
      end

      # Asynchronic Pagination
      #  This pagination won't block excecution waiting for result, pagination will be enqueued in Objectr#hydra.
      #
      # Parameters:
      #   @param options [Hash].
      #   Valid options are:
      #   * :page - indicated what page to return. Defaults to 1.
      #   * :per_page - indicates how many records to be returned per page. Defauls to 20
      #   * all other options will be sent in :params to WebService
      #
      # Usage:
      #   Person.async_paginate(:page => params[:page]){|i| result = i}
      def async_paginate(options={})
        options[:page] ||= 1
        options[:per_page] ||= 20

        options = self.merge_key(options)

        request = Typhoeus::Request.new(resource_uri, :params => options)
        request.on_complete do |response|
          if response.code >= 200 && response.code < 400
            log_ok(response)

            result_set = self.from_json(response.body)

            # this paginate is will_paginate's Array pagination
            collection = Kaminari.paginate_array(
                result_set[:collection],
                {
                    :total_count=>result_set[:total],
                    :limit => options[:per_page],
                    :offset => options[:per_page] * ([options[:page], 1].max - 1)
                }
            )

            yield collection
          else
            log_failed(response)
          end
        end
        self.hydra.queue(request)
      end

      #synchronic pagination
      def paginate(options={})
        result = nil
        self.retries.times do
          begin
            async_paginate(options){|i| result = i}
            Timeout::timeout(self.timeout/1000) do
              self.hydra.run
            end
            break unless result.nil?
          rescue Timeout::Error
            self.logger.warn("timeout")
            result = nil
          end
        end
        result
      end

      # Asynchronic Count
      #  This count won't block excecution waiting for result, count will be enqueued in Objectr#hydra.
      #
      # Parameters:
      #   @param options [Hash].
      #   Valid options are:
      #   @option options [Integer] :page - indicated what page to return. Defaults to 1.
      #   @option options [Integer] :per_page - indicates how many records to be returned per page. Defauls to 20
      #   @option options [Hash] all other options will be forwarded in :params to WebService
      #
      # @example 'Count bobs'
      #   Person.async_count(:when => {:name => 'bob'}}){|i| result = i}
      def async_count(options={})
        options[:page] = 1
        options[:per_page] = 1

        options = self.merge_key(options)

        request = Typhoeus::Request.new(resource_uri, :params => options)
        request.on_complete do |response|
          if response.code >= 200 && response.code < 400
            log_ok(response)

            result_set = self.from_json(response.body)

            yield result_set[:total]
          else
            log_failed(response)
          end
        end
        self.hydra.queue(request)
      end

      # synchronic count
      def count(options={})
        result = nil
        async_count(options){|i| result = i}
        Timeout::timeout(self.timeout/1000) do
          self.hydra.run
        end
        result
      rescue Timeout::Error
        self.logger.warn("timeout")
        return nil
      end

      # Asynchronic Find
      #  This find won't block excecution waiting for result, excecution will be enqueued in Objectr#hydra.
      #
      # Parameters:
      #   - id, id of object to find
      # @param [String/Integer] id
      # @param [Hash] params
      #
      # Usage:
      #   Person.async_find(params[:id])
      def async_find(id, params = {})
        params = self.merge_key(params)
        request = Typhoeus::Request.new( resource_uri(id), :params => params )

        request.on_complete do |response|
          if response.code >= 200 && response.code < 400
            log_ok(response)
            yield self.new.from_json(response.body) # this from_json is defined in ActiveModel::Serializers::JSON
          else
            log_failed(response)
          end
        end

        self.hydra.queue(request)
      end

      # synchronic find
      def find(id, params = {})
        result = nil
        async_find(id, params){|i| result = i}
        Timeout::timeout(self.timeout/1000) do
          self.hydra.run
        end
        result
      rescue Timeout::Error
        self.logger.warn("timeout")
        return nil
      end

      # Deletes Object#id
      #
      # Returns nil if delete failed
      #
      # @param [String] id - id of contact to be deleted
      # @param [Hash] params - other params to be sent to WS on request
      #
      # Usage:
      #   Person.delete(params[:id])
      def delete(id, params={})

        params = self.merge_key(params)

        response = nil
        Timeout::timeout(self.timeout/1000) do
          response = Typhoeus::Request.delete( self.resource_uri(id),
                                               :params => params,
                                               :timeout => self.timeout
          )
        end
        if response.code == 200
          log_ok(response)
          return self
        else
          log_failed(response)
          return nil
        end
      rescue Timeout::Error
        self.logger.warn "timeout"
        return nil
      end

      # Deletes all Objects matching given ids
      #
      # This method will make a DELETE request to resource_uri/destroy_multiple
      #
      # Returns nil if delete failed
      #
      # @param [Array] ids - ids of contacts to be deleted
      # @param [Hash] params - other params to be sent to WS on request
      #
      # Usage:
      #   Person.delete_multiple([1,2,4,5,6])
      def delete_multiple(ids, params={})
        raise "not-enabled" unless self.delete_multiple_enabled?

        params = self.merge_key(params)
        params = params.merge({:ids => ids})

        response = nil
        Timeout::timeout(self.timeout/1000) do
          response = Typhoeus::Request.delete( self.resource_uri+"/destroy_multiple",
                                               :params => params,
                                               :timeout => self.timeout
          )
        end
        if response.code == 200
          log_ok(response)
          return self
        else
          log_failed(response)
          return nil
        end
      rescue Timeout::Error
        self.logger.warn "timeout"
        return nil
      end
    end
  end

  ##
  # Will parse JSON string and initialize classes for all hashes in json_string[collection_key].
  #
  # @param json_string [JSON String] This JSON should have format: {collection: [...], total: X}
  #
  def self.from_json(json_string)
    parsed_response = ActiveSupport::JSON.decode(json_string)
    parsed_collection = collection_key.nil?? parsed_response : parsed_response[collection_key]
    collection = parsed_collection.map{|i| self.new(i)}

    if total_key
      {collection: collection, total: parsed_response[total_key].to_i}
    else
      { collection: collection }
    end
  end

end