require 'date'

class Cohort < ActiveRecord::Base

  EARLIEST_DATE = Time.parse('2014-03-01')
  TOTAL_COHORT_DATE = Time.at(0)

  ALLTIME_LABELS = {
    registered_users:                'Registered Users',
    first_downloaded_client_at:      'DL app', 
    first_certified_gear_at:         'Certified Gear',
    music_sessions_user_history:     'Played Online', 
    jam_tracks_played:               'Played JT', 
    jam_track_rights:                'Purchased JT',
    recorded_tracks:                 'Made Recording', 
    friendships:                     'Friended',
    invited_users:                   'Invite Others',
  }

  MONTHLY_LABELS = {
    registered_users:                'Registered Users',
    first_downloaded_client_at:      'DL app', 
    first_certified_gear_at:         'Certified Gear',
    music_sessions_user_history_1:   'Played Online 1', 
    music_sessions_user_history_2_5: 'Played Online 2-5', 
    music_sessions_user_history_6_:  'Played Online 6+', 
    jam_tracks_played_1:              'Played JT 1', 
    jam_tracks_played_2_5:            'Played JT 2-5', 
    jam_tracks_played_6_:             'Played JT 6+', 
    jam_track_rights_redeemed:       'Redeemed JT',
    jam_track_rights:                'Purchased JT',
    recorded_tracks:                 'Made Recording', 
    friendships:                     'Friended',
    invited_users:                   'Invite Others',
  }

  attr_accessible :all_time, :monthly_start
  #serialize :data_set, JSON

  before_create do
    self.data_set ||= {}
  end

  def self.date_tuples(from,to)
    prec   = from.size
    start  = Date.new(*from)
    finish = Date.new(*to)

    filter_on = [:day,:mon].first(3-prec)
    filter = ->(d) { filter_on.all? {|attr| d.send(attr) == 1 } }

    (start..finish)
      .select(&filter)
      .map { |d| [d.year,d.mon,d.day].first(prec) }
  end

  def self.earliest_cohort
    user = User.where(admin: false).order(:created_at).first
    starting = user.created_at if user
    starting = EARLIEST_DATE if starting.nil? || starting < EARLIEST_DATE
    starting # this is necessary to always return not null
  end

  def self.cohort_group_ranges(starting=nil, ending=nil)
    starting ||= self.earliest_cohort
    ending ||= Time.now
    dates = self.date_tuples([starting.year, starting.month], [ending.year, ending.month])
    ranges = []
    dates.each_with_index do |d1, idx|
      d2 = dates[idx+1] || [Time.now.next_month.year,Time.now.next_month.month]
      rr = Time.parse("#{d1[0]}-#{d1[1]}-1")..(Time.parse("#{d2[0]}-#{d2[1]}-1") - 1.second)
      ranges << rr
    end
    ranges
  end

  def self.generate_monthly_cohorts(monthly_start, monthly_end)
    Cohort.delete_all(['all_time = ?',false])
    self.cohort_group_ranges.collect do |range|
      next if range.first > monthly_end
      cc = Cohort.new
      cc.group_start = range.first
      cc.group_end = range.last
      cc.monthly_start = monthly_start
      cc.monthly_end = monthly_end
      cc.all_time = false
      cc.save!
      cc
    end
  end

  def self.generate_all_time_cohorts
    Cohort.delete_all("all_time = 't'")
    self.cohort_group_ranges.collect do |range|
      unless cc = Cohort.where(group_start: range.first).where(all_time: true).limit(1).first
        cc = Cohort.new
        cc.group_start = range.first
        cc.group_end = range.last
        cc.all_time = true
        cc.save!
      end
      cc
    end
  end

  def _put_data_set(key, count, num_user)
    self.data_set[key.to_s] = count
    xx = (count.to_f / num_user.to_f)
    self.data_set["#{key}%"] = 100.0 * xx.round(2)
  end

  def self.cohort_users(cohort)
    User.where(created_at: cohort.group_start..cohort.group_end)
  end

  def _played_online_subquery(constraint)
    where = if constraint.is_a?(Range)
              "played.cnt >= #{constraint.first} AND played.cnt <= #{constraint.last}"
            else
              "played.cnt #{constraint}"
            end
    start_date = all_time ? self.group_start : self.monthly_start
    end_date = all_time ? self.group_end : self.monthly_end
    sql =<<SQL
SELECT played.user_id FROM 
  (SELECT user_id, COUNT(*) cnt FROM music_sessions_user_history msuh1
    WHERE 
      msuh1.created_at >= '#{start_date}' AND 
      msuh1.created_at <= '#{end_date}' AND
      EXTRACT(EPOCH FROM (msuh1.session_removed_at - msuh1.created_at)) >= 900 AND
      (SELECT COUNT(*) FROM music_sessions_user_history msuh2
         WHERE msuh1.music_session_id = msuh2.music_session_id
      ) > 1
    GROUP BY user_id
  ) played
WHERE #{where}
SQL
  end

  def _played_jamtrack_subquery(constraint)
    where = if constraint.is_a?(Range)
              "played.cnt >= #{constraint.first} AND played.cnt <= #{constraint.last}"
            else
              "played.cnt #{constraint}"
            end
    start_date = all_time ? self.group_start : self.monthly_start
    end_date = all_time ? self.group_end : self.monthly_end
    sql =<<SQL
SELECT played.player_id FROM 
  (SELECT player_id, COUNT(*) cnt FROM playable_plays pp
    WHERE 
      pp.created_at >= '#{start_date}' AND 
      pp.created_at <= '#{end_date}' AND
      pp.playable_type = 'JamRuby::JamTrack'  /* VRFS-2916 jam_tracks.id is varchar: ADD */
     GROUP BY player_id
  ) played
WHERE #{where}
SQL
  end

  def _subquery(assoc_key, num_user)
    assoc = User.reflections[assoc_key]
    return 0 unless assoc
    start_date = all_time ? self.group_start : self.monthly_start
    end_date = all_time ? self.group_end : self.monthly_end
    sql =<<SQL
SELECT #{assoc.foreign_key} FROM #{assoc.class_name.constantize.table_name} tt 
WHERE 
    tt.created_at >= '#{start_date}' AND 
    tt.created_at <= '#{end_date}'
SQL
    if block_given?
      yield_sql = yield(sql) 
      sql = yield_sql unless yield_sql.blank?
    end
    self.class.cohort_users(self).where("users.id IN (#{sql})").count
  end
  
  def _monthly!
    unless 0 < num_user = self.class.cohort_users(self).count
      self.update_attribute(:data_set, {})
      return 
    end

    self.data_set['registered_users'] = num_user
    num_user = num_user.to_f

    qq = self.class.cohort_users(self)
      .where(first_downloaded_client_at: self.monthly_start..self.monthly_end)
    _put_data_set(:first_downloaded_client_at, qq.count, num_user)

    qq = self.class.cohort_users(self)
      .where(first_certified_gear_at: self.monthly_start..self.monthly_end)
    _put_data_set(:first_certified_gear_at, qq.count, num_user)

    count = _subquery(assoc_key = :invited_users, num_user)
    _put_data_set(assoc_key, count, num_user)

    count = _subquery(assoc_key = :recorded_tracks, num_user)
    _put_data_set(assoc_key, count, num_user)

    count = _subquery(assoc_key = :jam_track_rights, num_user) do |subsql|
      subsql += " AND tt.is_test_purchase = 'f' AND tt.redeemed = 'f' "
    end      
    _put_data_set(assoc_key, count, num_user)

    count = _subquery(assoc_key = :jam_track_rights, num_user) do |subsql|
      subsql += " AND tt.is_test_purchase = 'f' AND tt.redeemed = 't' "
    end      
    _put_data_set(:jam_track_rights_redeemed, count, num_user)

    count = _subquery(assoc_key = :friendships, num_user)
    _put_data_set(assoc_key, count, num_user)

    sql = _played_online_subquery(' = 1 ')
    count = self.class.cohort_users(self).where("users.id IN (#{sql})").count
    _put_data_set(:music_sessions_user_history_1, count, num_user)

    sql = _played_online_subquery(2..5)
    count = self.class.cohort_users(self).where("users.id IN (#{sql})").count
    _put_data_set(:music_sessions_user_history_2_5, count, num_user)

    sql = _played_online_subquery(' >= 6')
    count = self.class.cohort_users(self).where("users.id IN (#{sql})").count
    _put_data_set(:music_sessions_user_history_6_, count, num_user)

    sql = _played_jamtrack_subquery(' = 1 ')
    count = self.class.cohort_users(self).where("users.id IN (#{sql})").count
    _put_data_set(:jam_tracks_played_1, count, num_user)

    sql = _played_jamtrack_subquery(2..5)
    count = self.class.cohort_users(self).where("users.id IN (#{sql})").count
    _put_data_set(:jam_tracks_played_2_5, count, num_user)

    sql = _played_jamtrack_subquery(' >= 6')
    count = self.class.cohort_users(self).where("users.id IN (#{sql})").count
    _put_data_set(:jam_tracks_played_6_, count, num_user)

    self.save!
  end

  def _all_time!
    unless 0 < num_user = self.class.cohort_users(self).count
      self.update_attribute(:data_set, {})
      return 
    end

    self.data_set['registered_users'] = num_user
    num_user = num_user.to_f

    count = self.class.cohort_users(self)
      .where(['first_downloaded_client_at IS NOT NULL'])
      .count
    _put_data_set('first_downloaded_client_at', count, num_user)

    count = self.class.cohort_users(self)
      .where(['first_certified_gear_at IS NOT NULL'])
      .count
    _put_data_set('first_certified_gear_at', count, num_user)

    count = _subquery(assoc_key = :invited_users, num_user)
    _put_data_set(assoc_key, count, num_user)

    count = _subquery(assoc_key = :recorded_tracks, num_user)
    _put_data_set(assoc_key, count, num_user)

    count = _subquery(assoc_key = :friendships, num_user)
    _put_data_set(assoc_key, count, num_user)

    count = _subquery(assoc_key = :jam_track_rights, num_user) do |subsql|
      subsql += " AND tt.is_test_purchase = 'f'"
    end
    _put_data_set(assoc_key, count, num_user)
    
    count = _subquery(assoc_key = :jam_tracks_played, num_user) do |subsql|
      # VRFS-2916 jam_tracks.id is varchar: REMOVE
      # subsql += " AND tt.jam_track_id IS NOT NULL "
      # VRFS-2916 jam_tracks.id is varchar: ADD
      subsql += " AND tt.playable_type = 'JamRuby::JamTrack' "
    end
    _put_data_set(assoc_key, count, num_user)

    sql = _played_online_subquery(' >= 1')
    count = self.class.cohort_users(self).where("users.id IN (#{sql})").count
    _put_data_set(:music_sessions_user_history, count, num_user)

    self.save!
  end

  def populate!
    self.all_time ? _all_time! : _monthly!
  end

  def calculate_totals(cohorts)
    self.group_start = self.group_end = TOTAL_COHORT_DATE
    labels = all_time ? Cohort::ALLTIME_LABELS : Cohort::MONTHLY_LABELS

    self.data_set = labels.inject({}) do |hh, (kk,vv)| 
      hh[kk.to_s] = hh["#{kk}%"] = 0
      hh
    end

    labels = labels.keys.map(&:to_s)
    cohorts.each do |cc| 
      labels.each do |key| 
        self.data_set[key] += cc.data_set[key].to_i
      end
    end

    user_total = self.data_set['registered_users'].to_f
    labels.delete('registered_users')
    labels.each do |key|
      xx = (self.data_set[key].to_f / user_total)
      self.data_set["#{key}%"] = 100.0 * xx.round(4)
    end

    self.save!
    cohorts << self
  end

  def self.alltime_cohorts!
    cohorts = Cohort.generate_all_time_cohorts.each do |cc|
      cc._all_time!
    end
    Cohort.new(all_time: true).calculate_totals(cohorts)
  end

  def self.monthly_cohorts!(monthly_start, monthly_end=nil)
    monthly_end ||= monthly_start + 1.month - 1.second
    cohorts = self.generate_monthly_cohorts(monthly_start, monthly_end).compact.each do |cc|
      cc._monthly!
    end
    Cohort.new(all_time: false, monthly_start: monthly_start).calculate_totals(cohorts)
  end

  def group_start_str
    is_total_cohort? ? 'Total' : self.group_start.strftime('%Y-%m')
  end

  def group_end_str
    self.group_end.strftime('%Y-%m-%d')
  end

  def data_val(col, percent=false)
    if percent
      val = self.data_set["#{col}%"]
      val ? "#{'%0.f' % val}%" : ''
    else
      self.data_set[col.to_s]
    end
  end

  def self.monthly_starts
    self.cohort_group_ranges.collect do |rr|
      rr.first.to_s
    end
  end

  def is_total_cohort?
    self.group_start == TOTAL_COHORT_DATE
  end

end

