module JamRuby

  # not a active_record model; just a search result container
  class Search
    attr_accessor :results, :search_type, :query
    attr_accessor :user_counters, :page_num, :page_count

    LIMIT = 10

    SEARCH_TEXT_TYPES = [:musicians, :bands, :fans]
    SEARCH_TEXT_TYPE_ID = :search_text_type

    PARAM_SESSION_INVITE = :srch_sessinv
    PARAM_MUSICIAN = :srch_m
    PARAM_BAND = :srch_b
    PARAM_FEED = :srch_f
    PARAM_JAMTRACK = :srch_j

    F_PER_PAGE = B_PER_PAGE = M_PER_PAGE = 20
    M_MILES_DEFAULT = 500
    B_MILES_DEFAULT = 0

    M_ORDER_FOLLOWS = ['Most Followed', :followed]
    M_ORDER_PLAYS = ['Most Plays', :plays]
    M_ORDER_PLAYING = ['Playing Now', :playing]
    M_ORDER_LATENCY = ['Latency To Me', :latency]
    M_ORDER_DISTANCE = ['Distance To Me', :distance]
    M_ORDERINGS = [M_ORDER_LATENCY, M_ORDER_DISTANCE, M_ORDER_FOLLOWS, M_ORDER_PLAYS]
    ORDERINGS = B_ORDERINGS  = [M_ORDER_FOLLOWS, M_ORDER_PLAYS, M_ORDER_PLAYING]
    M_ORDERING_KEYS = M_ORDERINGS.collect { |oo| oo[1] }
    B_ORDERING_KEYS = B_ORDERINGS.collect { |oo| oo[1] }

    DISTANCE_OPTS = B_DISTANCE_OPTS = M_DISTANCE_OPTS = [[25.to_s, 25], [50.to_s, 50], [100.to_s, 100], [250.to_s, 250], [500.to_s, 500], [1000.to_s, 1000] ]

    # the values for score ranges are raw roundtrip scores. david often talks of one way scores (<= 20 is good), but
    # the client reports scores as roundtrip and the server uses those values throughout
    GOOD_SCORE = '.-40'
    MODERATE_SCORE = '40-80'
    POOR_SCORE = '80-120'
    UNACCEPTABLE_SCORE = '120-.'
    SCORED_SCORE = '.-.' # does not appear in menu choices
    TEST_SCORE = '.-60' # does not appear in menu choices
    ANY_SCORE = ''
    M_SCORE_OPTS = [['Any', ANY_SCORE], ['Good', GOOD_SCORE], ['Moderate', MODERATE_SCORE], ['Poor', POOR_SCORE], ['Unacceptable', UNACCEPTABLE_SCORE]]
    M_SCORE_DEFAULT = ANY_SCORE
    M_DISTANCE_DEFAULT = 500

    F_SORT_RECENT = ['Most Recent', :date]
    F_SORT_OLDEST = ['Most Liked', :likes]
    F_SORT_LENGTH = ['Most Played', :plays]
    F_SORT_OPTS = [F_SORT_RECENT, F_SORT_LENGTH, F_SORT_OLDEST]

    SHOW_BOTH = ['Sessions & Recordings', :all]
    SHOW_SESSIONS = ['Sessions', :music_session]
    SHOW_RECORDINGS = ['Recordings', :recording]
    SHOW_OPTS = [SHOW_BOTH, SHOW_SESSIONS, SHOW_RECORDINGS]

    DATE_OPTS = [['Today', 'today'], ['This Week', 'week'], ['This Month', 'month'], ['All Time', 'all']]


    def initialize(search_results=nil)
      @results = []
      self
    end
    
    def is_blank?
      !!@query && @query.empty?
    end

    def text_search(params, user = nil)
      @query = params[:query]
      tsquery = Search.create_tsquery(params[:query])
      return [] if tsquery.blank?

      rel = case params[SEARCH_TEXT_TYPE_ID].to_s
            when 'bands'
              @search_type = :bands
              Band.where(nil)
            when 'fans'
              @search_type = :fans
              User.fans
            else
              @search_type = :musicians
              User.musicians
            end
      @results = rel.where("(name_tsv @@ to_tsquery('jamenglish', ?))", tsquery).limit(10)

      @results = Search.scope_schools_together(@results, user)
    end

    class << self
      def band_search(txt, user = nil)
        self.text_search({ SEARCH_TEXT_TYPE_ID => :bands, :query => txt }, user)
      end

      def fan_search(txt, user = nil)
        self.text_search({ SEARCH_TEXT_TYPE_ID => :fans, :query => txt }, user)
      end

      def musician_search(txt, user = nil)
        self.text_search({ SEARCH_TEXT_TYPE_ID => :musicians, :query => txt }, user)
      end

      def session_invite_search(query, user)
        srch = Search.new
        srch.search_type = :session_invite
        like_str = "%#{query.downcase}%"
        rel = User
          .musicians
          .where(["users.id IN (SELECT friend_id FROM friendships WHERE user_id = '#{user.id}')"])
          .where(["first_name ILIKE ? OR last_name ILIKE ?", like_str, like_str])
          .limit(10)
          .order([:last_name, :first_name])
        srch.results = rel.all
        srch
      end

      def scope_schools_together_feeds(rel, user)
        if user.nil?
          return rel.where("feeds.school_id is null")
        end

        # platform instructors can search anybody (non-school and school). So no nothing special for them.

        if !user.is_platform_instructor
          # for everyone else...
          # make sure you can only see same-school.  Or in the case of 'null school', you'll get other non-schoolers (i.e. normies)
          # also, make sure anyone will find platform_instructors
          if user.school_id.nil?
            rel = rel.where("feeds.school_id is null")
          else
            rel = rel.where("feeds.school_id = #{user.school_id} OR feeds.is_platform_instructor")
          end
        end

        rel
      end

      def scope_schools_together_chats(rel, user)
        # TODO:
        return rel
      end


      def scope_schools_together_sessions(rel, user, table_name = 'active_music_sessions')
        if user.nil?
          return rel.where("#{table_name}.school_id is null")
        end
        # platform instructors can search anybody (non-school and school). So no nothing special for them.

        if !user.is_platform_instructor
          # for everyone else...
          # make sure you can only see same-school.  Or in the case of 'null school', you'll get other non-schoolers (i.e. normies)
          # also, make sure anyone will find platform_instructors
          if user.school_id.nil?
            rel = rel.where("#{table_name}.school_id is null")
          else
            rel = rel.where("#{table_name}.school_id = #{user.school_id} OR #{table_name}.is_platform_instructor")
          end
        end

        rel
      end

      def scope_schools_together(rel, user)
        if user.nil?
          return rel.where("school_id is null")
        end
        # platform instructors can search anybody (non-school and school). So no nothing special for them.

        if !user.is_platform_instructor
          # for everyone else...
          # make sure you can only see same-school.  Or in the case of 'null school', you'll get other non-schoolers (i.e. normies)
          # also, make sure anyone will find platform_instructors
          if user.school_id.nil?
            rel = rel.where("school_id is null")
          else
            rel = rel.where("school_id = #{user.school_id} OR is_platform_instructor")
          end
        end

        rel
      end

      def text_search(params, user = nil)
        srch = Search.new
        unless (params.blank? || params[:query].blank? || 2 > params[:query].length)
          srch.text_search(params, user)
        end
        srch
      end

      def create_tsquery(query)
        return nil if query.blank?

        search_terms = query.split
        return nil if search_terms.length == 0

        args = nil
        search_terms.each do |search_term|
          # remove ( ) ! : from query terms. parser blows up
          search_term.gsub!(/[\(\)!:]/, '')
          if args == nil  
            args = '"' + search_term + '"'
          else
            args = args + " & " + '"' + search_term + '"'
          end
        end
        args = args + ":*"
        args
      end

      def order_param(params, keys=M_ORDERING_KEYS)
        ordering = params[:orderby]
        ordering.blank? ? keys[0] : keys.detect { |oo| oo.to_s == ordering }
      end

      # produce a list of musicians (users where musician is true)
      # params:
      #   instrument - instrument to search for or blank
      #   score_limit - a range specification for score, see M_SCORE_OPTS above.
      #   handled by relation_pagination:
      #     page - page number to fetch (origin 1)
      #     per_page - number of entries per page
      #   handled by order_param:
      #     orderby - what sort of search, also defines order (followed, plays, playing)
      #   previously handled by where_latlng:
      #     distance - defunct!
      #     city - defunct!
      #     remote_ip - defunct!
      def musician_filter(params={}, user=nil)

        rel = User.musicians # not musicians_geocoded on purpose; we allow 'unknowns' to surface in the search page
        rel = rel.select('users.*')
        rel = rel.group('users.id')

        unless (instrument = params[:instrument]).blank?
          rel = rel.joins("inner JOIN musicians_instruments AS minst ON minst.player_id = users.id")
            .where(['minst.instrument_id = ?', instrument])
        end

        rel = scope_schools_together(rel, user)

        # to find appropriate musicians we need to join users with scores to get to those with no scores or bad scores
        # weeded out

        # filter on scores using selections from params
        # see M_SCORE_OPTS
        score_limit = ANY_SCORE
        l = params[:score_limit]
        unless l.nil?
          score_limit = l
        end

        locidispid = user.nil? ? 0 : (user.last_jam_locidispid || 0)

        # user can override their location with these 3 values
        country = params[:country]
        region = params[:region]
        city = params[:city]

        my_locid = nil # this is used for distance searches only

        #if country && region && city
        #  geoiplocation = GeoIpLocations.where(countrycode: country, region: region, city: city).first
        #  my_locid = geoiplocation.locid
        #end

        unless my_locid
          my_locid = locidispid/1000000  # if the user didn't specify a location to search on, user their account locidispid
        end

        if !locidispid.nil? && !user.nil?
          # score_join of left allows for null scores, whereas score_join of inner requires a score however good or bad
          # this is ANY_SCORE:
          score_join = 'left outer' # or 'inner'
          score_min = nil
          score_max = nil
          # these score_min, score_max come from here (doubled): https://jamkazam.atlassian.net/browse/VRFS-1962
          case score_limit
            when GOOD_SCORE
              score_join = 'inner'
              score_min = nil
              score_max = 40
            when MODERATE_SCORE
              score_join = 'inner'
              score_min = 40
              score_max = 70
            when POOR_SCORE
              score_join = 'inner'
              score_min = 80
              score_max = 100
            when UNACCEPTABLE_SCORE
              score_join = 'inner'
              score_min = 100
              score_max = nil
            when SCORED_SCORE
              score_join = 'inner'
              score_min = nil
              score_max = nil
            when TEST_SCORE
              score_join = 'inner'
              score_min = nil
              score_max = 60
            when ANY_SCORE
              # the default of ANY setup above applies
            else
              # the default of ANY setup above applies
          end

          rel = rel.joins("LEFT JOIN current_scores ON current_scores.a_userid = users.id AND current_scores.b_userid = '#{user.id}'")

          rel = rel.joins('LEFT JOIN regions ON regions.countrycode = users.country AND regions.region = users.state')

          rel = rel.where(['current_scores.full_score > ?', score_min]) unless score_min.nil?
          rel = rel.where(['current_scores.full_score <= ?', score_max]) unless score_max.nil?

          rel = rel.select('current_scores.full_score, current_scores.score, regions.regionname')
          rel = rel.group('current_scores.full_score, current_scores.score, regions.regionname')
        end

        ordering = self.order_param(params)
        case ordering
          when :latency
            # nothing to do. the sort added below 'current_scores.score ASC NULLS LAST' handles this
          when :distance
            # convert miles to meters for PostGIS functions
            miles = params[:distance].blank? ? 500 : params[:distance].to_i
            meters = miles * 1609.34
            #rel = rel.joins("INNER JOIN geoiplocations AS my_geo ON #{my_locid} = my_geo.locid")
            #rel = rel.joins("INNER JOIN geoiplocations AS other_geo ON users.last_jam_locidispid/1000000 = other_geo.locid")
            #rel = rel.where("users.last_jam_locidispid/1000000 IN (SELECT locid FROM geoiplocations WHERE geog && st_buffer((SELECT geog FROM geoiplocations WHERE locid = #{my_locid}), #{meters}))")
            #rel = rel.group("my_geo.geog, other_geo.geog")
            #rel = rel.order('st_distance(my_geo.geog, other_geo.geog)')
          when :plays  # FIXME:  double counting?
            # sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}"
            rel = rel.select('COUNT(records.id)+COUNT(sessions.id) AS search_play_count')
            rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id")
            rel = rel.joins("LEFT JOIN recordings AS records ON records.owner_id = users.id")
            rel = rel.order("search_play_count DESC")
          when :followed
            rel = rel.joins('left outer join follows on follows.followable_id = users.id')
            rel = rel.select('count(follows.user_id) as search_follow_count')
            rel = rel.order('search_follow_count DESC')
          when :playing
            rel = rel.joins("inner JOIN connections ON connections.user_id = users.id")
            rel = rel.where(['connections.aasm_state != ?', 'expired'])
        end

        if !locidispid.nil? && !user.nil?
          rel = rel.order('current_scores.full_score ASC NULLS LAST')
        end

        rel = rel.order('users.created_at DESC')

        rel, page = self.relation_pagination(rel, params)
        rel = rel.includes([:instruments, :followings, :friends])

        # XXX: DOES THIS MEAN ALL MATCHING USERS ARE RETURNED?
        objs = rel.all

        srch = Search.new
        srch.search_type = :musicians_filter
        srch.page_num, srch.page_count = page, objs.total_pages
        srch.musician_results_for_user(objs, user)
      end

      def relation_pagination(rel, params)
        perpage = [(params[:per_page] || M_PER_PAGE).to_i, 100].min
        page = [params[:page].to_i, 1].max
        [rel.paginate(:page => page, :per_page => perpage), page]
      end

      def new_musicians(usr, since_date)
        # this attempts to find interesting musicians to tell another musician about where interesting
        # is "has a good score and was created recently"
        # we're sort of depending upon usr being a musicians_geocoded as well...
        # this appears to only be called from EmailBatchNewMusician#deliver_batch_sets! which is
        # an offline process and thus uses the last jam location as "home base"

        locidispid = usr.last_jam_locidispid
        score_limit = 70
        limit = 50

        rel = User.musicians_geocoded
          .where(['users.created_at >= ? AND users.id != ?', since_date, usr.id])
          .joins('inner join current_scores on users.id = current_scores.a_userid')
          .where(['current_scores.b_userid = ?', usr.id])
          .where(['current_scores.full_score <= ?', score_limit])
          .order('current_scores.full_score') # best scores first
          .order('users.created_at DESC') # then most recent
          .limit(limit)

        objs = rel.all.to_a
        
        if block_given?
          yield(objs) if 0 < objs.count
        else
          return objs
        end
      end

      def band_filter(params={}, current_user=nil)
        rel = Band.scoped

        unless (genre = params[:genre]).blank?
          rel = Band.joins("RIGHT JOIN genre_players AS bgenres ON bgenres.player_id = bands.id AND bgenres.player_type = 'JamRuby::Band'")
            .where(['bgenres.genre_id = ? AND bands.id IS NOT NULL', genre])
        end

        rel = GeoIpLocations.where_latlng(rel, params, current_user)

        sel_str = 'bands.*'
        case ordering = self.order_param(params, B_ORDERING_KEYS)
        when :plays  # FIXME:  double counting?
          sel_str = "COUNT(records)+COUNT(msh) AS play_count, #{sel_str}"
          rel = rel.joins("LEFT JOIN music_sessions AS msh ON msh.band_id = bands.id")
            .joins("LEFT JOIN recordings AS records ON records.band_id = bands.id")
            .group("bands.id")
            .order("play_count DESC, bands.created_at DESC")
        when :followed
          sel_str = "COUNT(follows) AS search_follow_count, #{sel_str}"
          rel = rel.joins("LEFT JOIN follows ON follows.followable_id = bands.id")
            .group("bands.id")
            .order("COUNT(follows) DESC, bands.created_at DESC")
        when :playing
          rel = rel.joins("LEFT JOIN music_sessions AS msh ON msh.band_id = bands.id")
            .where('msh.music_session_id IS NOT NULL AND msh.session_removed_at IS NULL')
            .order("bands.created_at DESC")
        end

        rel = rel.select(sel_str)
        rel, page = self.relation_pagination(rel, params)
        rel = rel.includes([{ :users => :instruments }, :genres ])

        objs = rel.all
        srch = Search.new
        srch.search_type = :band_filter
        srch.page_num, srch.page_count = page, objs.total_pages
        if 1 == page && current_user.bands.present?
          current_user.bands.order('created_at DESC').each { |bb| objs.unshift(bb) }
        end if current_user && current_user.is_a?(User)
        srch.band_results_for_user(objs, current_user)
      end

    end

      

    RESULT_FOLLOW = :follows
    RESULT_FRIEND = :friends

    COUNT_FRIEND = :count_friend
    COUNT_FOLLOW = :count_follow
    COUNT_RECORD = :count_record
    COUNT_SESSION = :count_session
    COUNTERS = [COUNT_FRIEND, COUNT_FOLLOW, COUNT_RECORD, COUNT_SESSION]

    def musician_results_for_user(_results, user)
      @results = _results
      if user
        @user_counters = @results.inject({}) { |hh,val| hh[val.id] = []; hh }
        mids = "'#{@results.map(&:id).join("','")}'"

        # this gets counts for each search result on friends/follows/records/sessions
        @results.each do |uu|
          counters = { }
          counters[COUNT_FRIEND] = Friendship.where(:user_id => uu.id).count
          counters[COUNT_FOLLOW] = Follow.where(:followable_id => uu.id).count
          counters[COUNT_RECORD] = ClaimedRecording.where(:user_id => uu.id).count
          counters[COUNT_SESSION] = MusicSession.where(:user_id => uu.id).count
          @user_counters[uu.id] << counters
        end

        # this section determines follow/like/friend status for each search result
        # so that action links can be activated or not
        rel = User.select("users.id AS uid")
        rel = rel.joins("LEFT JOIN follows ON follows.user_id = '#{user.id}'")
        rel = rel.where(["users.id IN (#{mids}) AND follows.followable_id = users.id"])
        rel.all.each { |val| @user_counters[val.uid] << RESULT_FOLLOW }

        rel = User.select("users.id AS uid")
        rel = rel.joins("LEFT JOIN friendships AS friends ON friends.friend_id = '#{user.id}'")
        rel = rel.where(["users.id IN (#{mids}) AND friends.user_id = users.id"])
        rel.all.each { |val| @user_counters[val.uid] << RESULT_FRIEND }

      else
        @user_counters = {}
      end
      self
    end

    private 

    def _count(musician, key)
      if mm = @user_counters[musician.id] 
        return mm.detect { |ii| ii.is_a?(Hash) }[key]
      end if @user_counters
      0
    end

    public

    def session_invite_search?
      :session_invite == @search_type 
    end

    def musicians_text_search?
      :musicians == @search_type 
    end

    def fans_text_search?
      :fans == @search_type 
    end

    def bands_text_search?
      :bands == @search_type 
    end

    def musicians_filter_search?
      :musicians_filter == @search_type
    end

    def bands_filter_search?
      :band_filter == @search_type
    end

    def follow_count(musician)
      _count(musician, COUNT_FOLLOW)
    end

    def friend_count(musician)
      _count(musician, COUNT_FRIEND)
    end

    def record_count(musician)
      _count(musician, COUNT_RECORD)
    end

    def session_count(musician)
      _count(musician, COUNT_SESSION)
    end

    def is_friend?(musician)
      if mm = @user_counters[musician.id] 
        return mm.include?(RESULT_FRIEND)
      end if @user_counters
      false
    end
    def is_follower?(musician)
      if mm = @user_counters[musician.id] 
        return mm.include?(RESULT_FOLLOW)
      end if @user_counters
      false
    end

      

    def band_results_for_user(_results, user)
      @results = _results
      if user
        @user_counters = @results.inject({}) { |hh,val| hh[val.id] = []; hh }
        mids = "'#{@results.map(&:id).join("','")}'"

        # this gets counts for each search result 
        @results.each do |bb|
          counters = { }
          counters[COUNT_FOLLOW] = Follow.where(:followable_id => bb.id).count
          counters[COUNT_RECORD] = Recording.where(:band_id => bb.id).count
          counters[COUNT_SESSION] = MusicSession.where(:band_id => bb.id).count
          @user_counters[bb.id] << counters
        end

        # this section determines follow/like/friend status for each search result
        # so that action links can be activated or not

        rel = Band.select("bands.id AS bid")
        rel = rel.joins("LEFT JOIN follows ON follows.user_id = '#{user.id}'")
        rel = rel.where(["bands.id IN (#{mids}) AND follows.followable_id = bands.id"])
        rel.all.each { |val| @user_counters[val.bid] << RESULT_FOLLOW }

      else
        @user_counters = {}
      end
      self
    end

  end
end
