module Recommendable
  module Rater
    module Recommender
      # Get a list of raters that have been found to be the most similar to
      # self. They are sorted by the calculated similarity value.
      #
      # @param [Fixnum] limit the number of users to return (defaults to 10)
      # @return [Array] An array of instances of your user class
      def similar_raters(limit = 10, offset = 0)
        ids = Recommendable.redis.zrevrange(Recommendable::Helpers::RedisKeyMapper.similarity_set_for(id), 0, -1)
        ids = sanitize_ids(ids, self.class)

        order = ids.map { |id| "#{Recommendable.config.user_class.quoted_table_name}.#{Recommendable.config.user_class.quoted_primary_key} = ? DESC" }.join(', ')
        order = self.class.send(:sanitize_sql_for_assignment, [order, *ids])

        Recommendable.query(self.class, ids).order(order).limit(limit).offset(offset)
      end

      # Fetch a list of recommendations for a passed class.
      #
      # @param [String, Symbol, Class] klass the class from which to get recommendations
      # @param [Fixnum] limit the number of recommendations to fetch (defaults to 10)
      # @return [Array] a list of things this person's gonna love
      def recommended_for(klass, limit = 10, offset = 0)
        recommended_set = Recommendable::Helpers::RedisKeyMapper.recommended_set_for(klass, self.id)
        return Recommendable.query(klass, []) unless rated_anything? && Recommendable.redis.zcard(recommended_set) > 0

        ids = Recommendable.redis.zrevrange(recommended_set, 0, -1, :with_scores => true)
        ids = ids.select { |id, score| score > 0 }.map { |pair| pair.first }
        ids = sanitize_ids(ids, klass)

        order = ids.map { |id| "#{klass.quoted_table_name}.#{klass.quoted_primary_key} = ? DESC" }.join(', ')
        order = klass.send(:sanitize_sql_for_assignment, [order, *ids])
        Recommendable.query(klass, ids).order(order).limit(limit).offset(offset)
      end

      private

      # Sanitizes ids using klass type mapping
      # @private
      def sanitize_ids(ids, klass)
        ids.map{ |id| klass.new(klass.primary_key => id).send(klass.primary_key) }.compact
      end

      # Removes an item from a user's set of recommendations
      # @private
      def unrecommend(obj)
        Recommendable.redis.zrem(Recommendable::Helpers::RedisKeyMapper.recommended_set_for(obj.class, id), obj.id)
        true
      end

      # Removes a user from Recommendable. Called internally on a before_destroy hook.
      # @private
      def remove_from_recommendable!
        sets  = [] # SREM needed
        zsets = [] # ZREM needed
        keys  = [] # DEL  needed

        # Remove from other users' similarity ZSETs
        zsets += Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.similarity_set_for('*'))

        # Remove this user's similarity ZSET
        keys << Recommendable::Helpers::RedisKeyMapper.similarity_set_for(id)

        # For each ratable class...
        Recommendable.config.ratable_classes.each do |klass|
          # Remove this user from any class member's liked_by/disliked_by sets
          sets += Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(klass, '*'))
          sets += Recommendable.redis.keys(Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(klass, '*'))

          # Remove this user's liked/disliked/hidden/bookmarked/recommended sets for the class
          keys << Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, id)
          keys << Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, id)
          keys << Recommendable::Helpers::RedisKeyMapper.hidden_set_for(klass, id)
          keys << Recommendable::Helpers::RedisKeyMapper.bookmarked_set_for(klass, id)
          keys << Recommendable::Helpers::RedisKeyMapper.recommended_set_for(klass, id)
        end

        Recommendable.redis.pipelined do |redis|
          sets.each { |set| redis.srem(set, id) }
          zsets.each { |zset| redis.zrem(zset, id) }
          redis.del(*keys)
        end
      end
    end
  end
end
