require 'iso-639'

module JamRuby
  class MusicSession < ActiveRecord::Base
    include HtmlSanitize
    html_sanitize strict: [:name, :description]

    @@log = Logging.logger[MusicSession]

    NO_RECURRING = 'once'
    RECURRING_WEEKLY = 'weekly'

    RECURRING_MODES = [NO_RECURRING, RECURRING_WEEKLY]

    UNSTARTED_INTERVAL_DAYS_SKIP = '14'  # days past scheduled start to skip in query
    UNSTARTED_INTERVAL_DAYS_PURGE = '28'  # days past scheduled start to purge session
    UNSTARTED_INTERVAL_DAYS_PURGE_RECUR = '28'  # days past scheduled start to purge recurddingsession

    CREATE_TYPE_START_SCHEDULED = 'start-scheduled'
    CREATE_TYPE_SCHEDULE_FUTURE = 'schedule-future'
    CREATE_TYPE_RSVP = 'rsvp'
    CREATE_TYPE_IMMEDIATE = 'immediately'
    CREATE_TYPE_QUICK_START = 'quick-start'
    CREATE_TYPE_LESSON = 'lesson'
    CREATE_TYPE_QUICK_PUBLIC = 'quick-public'

    attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed

    attr_accessor :approved_rsvps, :open_slots, :pending_invitations

    self.table_name = "music_sessions"

    self.primary_key = 'id'

    belongs_to :creator,:class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :music_session_histories
    belongs_to :band, :class_name => 'JamRuby::Band',  :foreign_key => :band_id, :inverse_of => :music_sessions
    belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id
    belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions
    belongs_to :lesson_session, :class_name => "JamRuby::LessonSession"

    has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all
    has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id"
    has_many :session_info_comments, :class_name => "JamRuby::SessionInfoComment", :foreign_key => "music_session_id"
    has_many :likes, :class_name => "JamRuby::MusicSessionLiker", :foreign_key => "session_id"
    has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
    has_one  :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id'
    has_one  :feed, :class_name => "JamRuby::Feed", :inverse_of => :music_session, :foreign_key => 'music_session_id', :dependent => :destroy
    belongs_to :genre, :class_name => "JamRuby::Genre", :inverse_of => :music_sessions, :foreign_key => 'genre_id'
    has_many :join_requests, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::JoinRequest"
    has_many :invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::Invitation"
    has_many :invited_musicians, :through => :invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
    has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation"
    has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
    has_many :rsvp_slots, :class_name => "JamRuby::RsvpSlot", :foreign_key => "music_session_id", :dependent => :destroy
    has_many :rsvp_requests, :class_name => "JamRuby::RsvpRequest", :foreign_key => "music_session_id", :dependent => :destroy
    has_many :music_notations, :class_name => "JamRuby::MusicNotation", :foreign_key => "music_session_id"
    has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession"
    has_many :broadcasts, :class_name => "JamRuby::Broadcast"

    validates :genre, :presence => true
    validates :description, :presence => true, :no_profanity => true
    validates :fan_chat, :inclusion => {:in => [true, false]}
    validates :fan_access, :inclusion => {:in => [true, false]}
    validates :approval_required, :inclusion => {:in => [true, false]}
    validates :musician_access, :inclusion => {:in => [true, false]}
    validates :friends_can_join, :inclusion => {:in => [true, false]}
    validates :is_unstructured_rsvp, :inclusion => {:in => [true, false]}
    validates :legal_terms, :inclusion => {:in => [true]}, :on => :create
    validates :creator, :presence => true
    validates :timezone, presence: true, if: Proc.new { |session| session.scheduled_start }
    validates :scheduled_duration, presence: true, if: Proc.new { |session| session.scheduled_start }

    validate :creator_is_musician
    validate :validate_timezone

    before_create :generate_share_token
    #before_save   :update_scheduled_start
    before_save :check_scheduling_info_changed

    SHARE_TOKEN_LENGTH = 8

    SEPARATOR = '|'

    def current_broadcast
      Broadcast.current_broadcast(self)
    end

    def unlink_broadcast
      Broadcast.unlink_broadcast(self)
    end

    def create_broadcast(user, broadcast_options, google_client = GoogleClient.new)

      broadcast = current_broadcast

      if broadcast.nil?
        broadcast = create_youtube_broadcast(user, broadcast_options, google_client)
      else
        result = refresh_youtube_broadcast(user, broadcast, broadcast_options, google_client)
        # check against Youtube the real state of broadcast, to see if we need a new one?

        if result.nil?
          unlink_broadcast # user probably deleted it, or marked it complete.
          broadcast = create_youtube_broadcast(user, broadcast_options, google_client)
        end
      end

      broadcast
    end

    def create_stream(user, broadcast_options, google_client = JamRuby::GoogleClient.new)

      broadcast = create_broadcast(user, broadcast_options, google_client)

      stream = current_stream(broadcast)

      if stream.nil?
        create_youtube_stream(user, broadcast, broadcast_options, google_client)
        bind_broadcast(user, broadcast, google_client)
      else
        bind_broadcast(user, broadcast, google_client)
      end
    end

    def current_stream(broadcast)
      broadcast.stream_id
    end

    def refresh_stream(user, broadcast)
      stream_data = get_livestream(user)
      broadcast.stream_id = stream_data["id"]
      broadcast.stream_status = stream_data["status"]["streamStatus"]
      broadcast.stream_name = stream_data["cdn"]["ingestionInfo"]["streamName"]
      broadcast.stream_address = stream_data["cdn"]["ingestionInfo"]["ingestionAddress"]
      broadcast.stream_data = stream_data.to_json
      broadcast.save
    end

    def refresh_youtube_broadcast(user, broadcast, broadcast_data = nil, google_client = GoogleClient.new)
      if broadcast_data.nil?
        broadcast_data = google_client.get_broadcast(user, broadcast.broadcast_id)
      end

      if broadcast_data
        broadcast.update_broadcast_data(broadcast_data)
        broadcast.save
        true
      else
        # this path makes sense if the user deleted the video on the server, but we do not yet know it
        nil
      end

    end

    def get_livestream(user, google_client = GoogleClient.new)
      broadcast = current_broadcast

      if broadcast.nil?
        nil
      else
        stream_id = current_stream(broadcast)

        if stream_id.nil?
          nil
        else
          return google_client.get_livestream(user, stream_id)
        end
      end
    end

    def get_broadcast(user, google_client = UserManager.new.get_google_client)
      broadcast = current_broadcast

      if broadcast.nil?
        nil
      else
        broadcast_data = google_client.get_broadcast(user, broadcast.broadcast_id)
        broadcast.update_broadcast_data(broadcast_data)
        broadcast.save
        broadcast
      end
    end

    def set_livestream_live(user, google_client = GoogleClient.new)
      livestream = get_livestream(user, google_client)

      if livestream
       # if livestream["status"]["streamStatus"] == "active"
          transition_broadcast(user, broadcast, 'live', google_client)
       # end
      else
      end

    end


    # https://developers.google.com/youtube/v3/live/docs/liveStreams#resource
    def create_youtube_stream(user, broadcast, broadcast_options, google_client = GoogleClient.new)

      # https://developers.google.com/youtube/v3/live/docs/liveStreams/insert
      # required
      # snippet.title
      # cdn.format
      # cdn.ingestionType (deprecated - use resolution/framerate)

      stream_options = {}
      stream_options[:snippet] ||= {}
      stream_options[:snippet][:title] ||= name
      stream_options[:snippet][:isDefaultStream] ||= false
      #broadcast_options[:snippet][:scheduledEndTime] = end_time.utc.iso8601

      stream_options[:cdn] ||= {}
      stream_options[:cdn][:frameRate] ||= '30fps'
      stream_options[:cdn][:resolution] ||= '360p'
      stream_options[:cdn][:ingestionType] ||= 'rtmp'

      stream_options[:contentDetails] ||= {}
      stream_options[:contentDetails][:isReusable] ||= false
      stream_options[:contentDetails][:monitorStream] ||= {}
      stream_options[:contentDetails][:monitorStream][:enableMonitorStream] ||= false

      stream_options = google_client.create_stream(user, stream_options)

      broadcast.stream_id = stream_options["id"]
      broadcast.stream_status = stream_options["status"]["streamStatus"]
      broadcast.stream_name = stream_options["cdn"]["ingestionInfo"]["streamName"]
      broadcast.stream_address = stream_options["cdn"]["ingestionInfo"]["ingestionAddress"]
      broadcast.stream_data = stream_options.to_json
      broadcast.save!
      broadcast
    end

    def create_youtube_broadcast(user, broadcast_options, google_client = GoogleClient.new)

      start_time, end_time = youtube_times
      broadcast_options ||= {}
      broadcast_options[:snippet] ||= {}
      broadcast_options[:snippet][:title] ||= name
      broadcast_options[:snippet][:description] ||= description
      broadcast_options[:snippet][:scheduledStartTime] = start_time.utc.iso8601
      #broadcast_options[:snippet][:scheduledEndTime] = end_time.utc.iso8601

      broadcast_options[:status] ||= {}
      broadcast_options[:status][:privacyStatus] ||= (fan_access ? 'public' : 'private')

      broadcast_options[:contentDetails] ||= {}

      # if false, this causes a 'request not authorized error'
      # From: https://developers.google.com/youtube/v3/live/docs/liveBroadcasts
      # If your channel does not have permission to disable recordings, and you attempt to insert a broadcast with the recordFromStart property set to false, the API will return a Forbidden error.
      #broadcast_options[:contentDetails][:recordFromStart] ||= false

      broadcast_data = google_client.create_broadcast(user, broadcast_options)

      broadcast = Broadcast.new
      broadcast.music_session_id = self.id
      broadcast.user_id = user.id
      broadcast.broadcast_id = broadcast_data["id"]
      broadcast.update_broadcast_data(broadcast_data)
      broadcast.save!
      broadcast
    end

    def bind_broadcast(user, broadcast, google_client = GoogleClient.new)

      bind_data = google_client.bind_broadcast(user, broadcast.broadcast_id, broadcast.stream_id)
      broadcast.update_broadcast_data(bind_data)
      broadcast.save!
      broadcast
    end

    # broadcastStatus one of complete, live, testing
    def transition_broadcast(user, broadcast, broadcastStatus, google_client = GoogleClient.new)

      if broadcastStatus == 'testing' && (broadcast.broadcast_status == 'testing' || broadcast.broadcast_status == 'live' || broadcast.broadcast_status == 'testStarting' || broadcast.broadcast_status == 'liveStarting')
        # short cut out; this in unnecessary
        puts "SHORT CUT OUT OF TRANSITION TESTING TESTING"
        return broadcast
      end

      if broadcastStatus == 'live' && broadcast.broadcast_status == 'live'
        # short cut out; this in unnecessary
        puts "SHORT CUT OUT OF TRANSITION TESTING LIVE"
        return broadcast
      end

      bind_data = google_client.transition_broadcast(user, broadcast.broadcast_id, broadcastStatus)
      broadcast.update_broadcast_data(bind_data)
      broadcast.save!
      broadcast
    end

    def youtube_times
      start = scheduled_start_time

      if start < Time.now
        start = Time.now
        end_time = start + safe_scheduled_duration
        return [start, end_time]
      else
        return [start, scheduled_end_time]
      end

    end

    def check_scheduling_info_changed
      @scheduling_info_changed = scheduled_start_changed?
      true
    end

    def update_scheduled_start

      # it's very important that this only run if timezone changes, or scheduled_start changes
      if self.scheduled_start && (self.scheduled_start_changed? || self.timezone_changed?)
        self.scheduled_start = MusicSession.parse_scheduled_start(self.scheduled_start, self.timezone)
      end
    end

    def comment_count
      self.comments.size
    end

    # copies all relevant info for the recurring session
    def copy

      MusicSession.transaction do
        # copy base music_session data
        new_session = MusicSession.new
        new_session.description = self.description
        new_session.user_id = self.user_id
        new_session.band_id = self.band_id
        new_session.fan_access = self.fan_access
        new_session.scheduled_start = self.scheduled_start + 1.week
        new_session.scheduled_duration = self.scheduled_duration
        new_session.musician_access = self.musician_access
        new_session.approval_required = self.approval_required
        new_session.friends_can_join = self.friends_can_join
        new_session.fan_chat = self.fan_chat
        new_session.genre_id = self.genre_id
        new_session.legal_policy = self.legal_policy
        new_session.language = self.language
        new_session.name = self.name
        new_session.recurring_mode = self.recurring_mode
        new_session.timezone = self.timezone
        new_session.open_rsvps = self.open_rsvps
        new_session.is_unstructured_rsvp = self.is_unstructured_rsvp
        new_session.legal_terms = true
        new_session.session_controller = self.session_controller
        new_session.school_id = self.school_id
        new_session.is_platform_instructor = self.is_platform_instructor

        # copy rsvp_slots, rsvp_requests, and rsvp_requests_rsvp_slots
        RsvpSlot.where("music_session_id = '#{self.id}'").find_each do |slot|
          new_slot = RsvpSlot.new
          new_slot.instrument_id = slot.instrument_id
          new_slot.proficiency_level = slot.proficiency_level
          new_slot.is_unstructured_rsvp = slot.is_unstructured_rsvp
          new_session.rsvp_slots << new_slot

          # get the request for this slot that was approved (should only be ONE)
          rsvp_request_slot = RsvpRequestRsvpSlot.where("chosen = true AND rsvp_slot_id = ?", slot.id).first

          unless rsvp_request_slot.nil?
            rsvp = RsvpRequest.find_by_id(rsvp_request_slot.rsvp_request_id)
            new_rsvp = RsvpRequest.new
            new_rsvp.user_id = rsvp.user_id
            new_rsvp.music_session_id = rsvp.music_session_id

            new_rsvp_req_slot = RsvpRequestRsvpSlot.new
            new_rsvp_req_slot.rsvp_request = new_rsvp
            new_rsvp_req_slot.rsvp_slot = new_slot
            new_rsvp_req_slot.chosen = true

            # .last => new_slot above
            new_session.rsvp_slots.last.rsvp_requests_rsvp_slots << new_rsvp_req_slot

          # if this slot was not chosen, try to get any RSVPs that were 1-time cancellations and copy those
          else
            rejected_req_slots = RsvpRequestRsvpSlot.where("(chosen is null OR chosen = FALSE) AND rsvp_slot_id = ?", slot.id).order("created_at ASC")
            rejected_req_slots.each do |req_slot|
              # get RsvpRequest corresponding to this RsvpRequestRsvpSlot
              rsvp = RsvpRequest.find_by_id(req_slot.rsvp_request_id)

              # if the RSVP was canceled (but all future sessions were NOT canceled), then copy this one and break
              if rsvp.canceled && !rsvp.cancel_all
                new_rsvp = RsvpRequest.new
                new_rsvp.user_id = rsvp.user_id
                new_rsvp.music_session_id = rsvp.music_session_id

                new_rsvp_req_slot = RsvpRequestRsvpSlot.new
                new_rsvp_req_slot.rsvp_request = new_rsvp
                new_rsvp_req_slot.rsvp_slot = new_slot
                new_rsvp_req_slot.chosen = nil

                # .last => new_slot above
                new_session.rsvp_slots.last.rsvp_requests_rsvp_slots << new_rsvp_req_slot
              end
            end
          end
        end

        # copy music_notations
        MusicNotation.where("music_session_id = '#{self.id}'").find_each do |notation|
          new_notation = MusicNotation.new
          new_notation.user_id = notation.user_id
          new_notation.music_session = new_session
          new_notation.file_url = notation.file_url
          new_notation.file_name = notation.file_name
          new_notation.size = notation.size
          new_session.music_notations << new_notation
        end

        new_session.save!

        # mark the next session as scheduled
        self.next_session_scheduled = true
        self.save

        return new_session
      end
    end

    def is_lesson?
      !!lesson_session
    end

    # checks if this is a lesson and if the person indicated is a teacher or student
    def is_lesson_member?(user)
      is_lesson? && (lesson_session.teacher.id == user.id || lesson_session.student.id == user.id)
    end

    def grouped_tracks
      tracks = []
      self.music_session_user_histories.each do |msuh|
        user = User.find(msuh.user_id)

        # see if user already exists in array
        t = tracks.select { |track| track.musician.id == user.id }.first

        if t.blank?
          t = Track.new
          t.musician = user
          t.instrument_ids = []

          # this treats each track as a "user", which has 1 or more instruments in the session
          unless msuh.instruments.blank?
            instruments = msuh.instruments.split(SEPARATOR)
            instruments.each do |instrument|
              if !t.instrument_ids.include? instrument
                t.instrument_ids << instrument
              end
            end
          end
          tracks << t
        # this handles case where user is duplicated in MSUH for the same session
        else
          unless msuh.instruments.blank?
            instruments = msuh.instruments.split(SEPARATOR)
            instruments.each do |instrument|
              if !t.instrument_ids.include? instrument
                t.instrument_ids << instrument
              end
            end
          end
        end
      end
      tracks
    end

    # is the viewer 'friensd with the session?'
    # practically, to be a friend with the session, you have to be a friend with the creator
    # we could loosen to be friend of friends or friends with anyone in the session
    def friends_with_session(user)
      self.creator.friends?(user)
    end

    def can_join? user, as_musician
      if as_musician
        unless user.musician
          return false # "a fan can not join a music session as a musician"
        end

        if self.musician_access
          if self.approval_required
            return self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user)
          else
            return true
          end
        else
          # the creator can always join, and the invited users can join
          return self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) || (self.friends_can_join && self.friends_with_session(user))
        end
      else
        # it's a fan, and the only way a fan can join is if fan_access is true
        self.fan_access
      end
    end

    def can_see? user
      if self.musician_access || self.fan_access
        true
      else
        self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) || self.creator.friends?(user) || self.has_lesson_access?(user)
      end
    end

    def has_lesson_access?(user)
      if is_lesson?
        puts "HAS LESSON ACCESS"
        result = lesson_session.has_access?(user)
        puts "HAS LESSON ACCESS #{ result}"
        result
      else
        false
      end

    end

    def set_session_controller(current_user, user)

      # only allow update of session controller by the creator or the currently marked user

      should_tick = false

      if current_user != creator && current_user != self.session_controller
        return should_tick
      end

      if active_music_session
        if user
          if active_music_session.users.exists?(user)
            self.session_controller = user
            should_tick = save
          end
        else
          self.session_controller = nil
          should_tick = save
        end
      end
      should_tick
    end

    def self.index(current_user, user_id, band_id = nil, genre = nil)
      hide_private = false
      if current_user.id != user_id
        hide_private = false # TODO: change to true once public flag exists
      end

      query = MusicSession
        .joins(
          %Q{
            LEFT OUTER JOIN
              music_sessions_user_history
            ON
              music_sessions.id = music_sessions_user_history.music_session_id
          }
        )
        .where(
          %Q{
              music_sessions.user_id = '#{user_id}'
          }
        )

      #query = query.where("public = false") unless !hide_private
      query = query.where("music_sessions.band_id = '#{band_id}") unless band_id.nil?
      query = query.where("music_sessions.genres like '%#{genre}%'") unless genre.nil?
      return query
    end

    def self.scheduled user, only_public = false
      # keep unstarted sessions around for 12 hours after scheduled_start
      session_not_started = "(music_sessions.scheduled_start > NOW() - '12 hour'::INTERVAL AND music_sessions.started_at IS NULL)"

      # keep started sessions that are not finished yet
      session_started_not_finished = "(music_sessions.started_at IS NOT NULL AND music_sessions.session_removed_at IS NULL)"

      # let session be restarted for up to 2 hours after finishing
      session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)"

      query = MusicSession.select('distinct music_sessions.*')
      query = query.joins(
          %Q{
            LEFT OUTER JOIN
              rsvp_requests
            ON rsvp_requests.music_session_id = music_sessions.id AND rsvp_requests.user_id = '#{user.id}' AND rsvp_requests.chosen = TRUE

            LEFT OUTER JOIN
              invitations
            ON
              music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}'
          }
      )
      query = query.where("music_sessions.old = FALSE")
      query = query.where("music_sessions.canceled = FALSE")
      query = query.where('music_sessions.fan_access = TRUE or music_sessions.musician_access = TRUE') if only_public
      #query = query.where("music_sessions.user_id = '#{user.id}' OR invitations.id IS NOT NULL")
      query = query.where("(rsvp_requests.id IS NOT NULL) OR (invitations.id IS NOT NULL) OR (music_sessions.user_id = '#{user.id}') ")

      query = Search.scope_schools_together_sessions(query, user, 'music_sessions')

      query = query.where("music_sessions.scheduled_start IS NULL OR #{session_not_started} OR #{session_finished} OR #{session_started_not_finished}")
      query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')")
      query = query.order("music_sessions.scheduled_start ASC")
      
      query
    end

    # if only_approved is set, then only return sessions where the current user has been chosen
    def self.scheduled_rsvp(user, only_approved = false)

      filter_approved = only_approved ? 'AND rsvp_requests_rsvp_slots.chosen = true' : ''

      query = MusicSession.joins(
          %Q{
            INNER JOIN
              rsvp_slots
            ON
              music_sessions.id = rsvp_slots.music_session_id
            INNER JOIN
              rsvp_requests_rsvp_slots
            ON
              rsvp_requests_rsvp_slots.rsvp_slot_id = rsvp_slots.id
            INNER JOIN
              rsvp_requests
            ON rsvp_requests.id = rsvp_requests_rsvp_slots.rsvp_request_id
          }
      )

      query = Search.scope_schools_together_sessions(query, user, 'music_sessions')

      query = query.where(%Q{music_sessions.old = FALSE AND music_sessions.canceled = FALSE AND
                            (music_sessions.create_type is NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')) AND
                            (music_sessions.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND rsvp_requests.user_id = '#{user.id}' #{filter_approved}}
                        ).order(:scheduled_start)
      query
    end

    def self.create user, options
      band = Band.where(id: options[:band]).first if options[:band].present?

      ms                    = MusicSession.new
      ms.name               = options[:name]
      ms.description        = options[:description]
      ms.genre_id           = (options[:genres].length > 0 ? options[:genres][0] : nil) if options[:genres]
      ms.musician_access    = options[:musician_access]
      ms.friends_can_join   = options[:friends_can_join] || false
      ms.approval_required  = options[:approval_required]
      ms.fan_access         = options[:fan_access]
      ms.fan_chat           = options[:fan_chat]
      ms.band               = band
      ms.legal_policy       = options[:legal_policy]
      ms.language           = options[:language]
      ms.scheduled_duration = options[:duration].to_i * 1.minutes if options[:duration]
      ms.recurring_mode     = options[:recurring_mode] if options[:recurring_mode]
      ms.timezone           = options[:timezone] if options[:timezone]
      ms.legal_terms        = true
      ms.open_rsvps         = options[:open_rsvps] if options[:open_rsvps]
      ms.creator            = user
      ms.session_controller = user
      ms.create_type        = options[:create_type]
      ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp]
      ms.scheduled_start    = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone]
      ms.scheduled_start    = options[:scheduled_start] if options[:scheduled_start]
      ms.school_id = user.school_id
      ms.is_platform_instructor = user.is_platform_instructor
      if options[:lesson_session]
        ms.lesson_session = options[:lesson_session]
      end


      ms.save

      unless ms.errors.any?
        ms.reload

        rsvp_slot_ids = []
        self_rsvp_slot_ids = []
        options[:rsvp_slots].each do |rs|
          rsvp = RsvpSlot.new
          rsvp.instrument = Instrument.find(rs[:instrument_id])
          rsvp.proficiency_level = rs[:proficiency_level]
          rsvp.music_session = ms
          rsvp.save

          ms.rsvp_slots << rsvp

          if rs[:approve] == true
            self_rsvp_slot_ids.push rsvp.id
          else
            rsvp_slot_ids.push rsvp.id
          end
        end if options[:rsvp_slots]
        RsvpRequest.create({session_id: ms.id, rsvp_slots: self_rsvp_slot_ids, :autoapprove => true}, user)

        options[:invitations].each do |invite_id|
          invitation = Invitation.new
          receiver = User.find(invite_id)
          invitation.sender = user
          invitation.receiver = receiver
          invitation.music_session = ms
          invitation.save

          ms.invitations << invitation

          # send scheduled session notification or join session notification
          case ms.create_type
          when CREATE_TYPE_RSVP, CREATE_TYPE_SCHEDULE_FUTURE
            Notification.send_scheduled_session_invitation(ms, receiver)
          when CREATE_TYPE_LESSON
            if ms.lesson_session.is_active?
              Notification.send_jamclass_invitation_teacher(ms, receiver)
            end
          else
            Notification.send_session_invitation(receiver, user, ms.id)
          end
          
        end if options[:invitations]

        options[:music_notations].each do |notation|
          notation = MusicNotation.find(notation[:id])
          notation.music_session = ms
          notation.save

          ms.music_notations << notation
        end if options[:music_notations]

        ms.save
      end

      ms
    end

    def self.update user, options
      music_session = MusicSession.find(options[:id])
      if music_session.creator == current_user
        Notification.send_scheduled_session_cancelled music_session
        music_session.destroy

        respond_with responder: ApiResponder, :status => 204
      else
        render :json => { :message => ValidationMessages::PERMISSION_VALIDATION_ERROR }, :status => 404
      end
    end

    def unique_users
      User
      .joins(:music_session_user_histories)
      .group("users.id")
      .order("users.id")
      .where(%Q{ music_sessions_user_history.music_session_id = '#{id}'})
    end

    # returns one user history per user, with instruments all crammed together, and with total duration
    def unique_user_histories

      # only get the active users if the session is in progress
      user_filter = "music_sessions_user_history.session_removed_at is null" if self.session_removed_at.nil?

      MusicSessionUserHistory
        .joins(:user)
        .select("STRING_AGG(instruments, '|') AS total_instruments,
                 SUM(date_part('epoch', COALESCE(music_sessions_user_history.session_removed_at, music_sessions_user_history.created_at) - music_sessions_user_history.created_at)) AS total_duration,
                 music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
        .group("music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
        .order("music_sessions_user_history.user_id")
        .where(%Q{ music_sessions_user_history.music_session_id = '#{id}'})
        .where(user_filter)
    end

    def duration_minutes
      end_time = self.session_removed_at || Time.now.utc
      (end_time - self.created_at) / 60.0
    end

    def music_session_user_histories
      @msuh ||= JamRuby::MusicSessionUserHistory
        .where(:music_session_id => self.id)
        .order('created_at DESC')
    end

    def comments
      @comments ||= JamRuby::MusicSessionComment
        .where(:music_session_id => self.id)
        .order('created_at DESC')
    end

    def likes
      @likes ||= JamRuby::MusicSessionLiker
        .where(:music_session_id => self.music_session_id)
    end

    # these are 'users that are a part of this session'
    # which means are currently in the music_session, or, rsvp'ed, or creator
    def part_of_session? user
      # XXX check RSVP'ed
      user == self.creator || (active_music_session ? active_music_session.users.exists?(user.id) : false)
    end

    def is_over?
      active_music_session.nil?
    end

    def has_mount?
      !active_music_session.nil? && !active_music_session.mount.nil?
    end

    def can_cancel?  user
      self.creator == user
    end

    def legal_policy_url
      # TODO: move to DB or config file or helper
      case legal_policy.downcase
        when "standard"
          return "session-legal-policies/standard"
        when "creative"
          return "session-legal-policies/creativecommons"
        when "offline"
          return "session-legal-policies/offline"
        when "jamtracks"
          return "session-legal-policies/jamtracks"
        else
          return ""
      end
    end

    def language_description
      if self.language.blank?
        self.language = "en"
      end

      iso639Details = ISO_639.find_by_code(self.language)

      unless iso639Details.blank?
        return iso639Details.english_name
      else
        return "English"
      end
    end

    def scheduled_end_time
      start = scheduled_start_time
      duration = safe_scheduled_duration
      start + duration
    end

    def timezone_id
      MusicSession.split_timezone(timezone)[0]
    end

    def timezone_description
      MusicSession.split_timezone(timezone)[1]
    end

    def musician_access_description
      if self.musician_access && self.approval_required
        "Musicians may join by approval"
      elsif self.musician_access && !self.approval_required
        "Musicians may join at will"
      elsif !self.musician_access && !self.approval_required
        "Only RSVP musicians may join"
      end
    end

    def fan_access_description
      if self.fan_access && self.fan_chat
        "Fans may listen, chat with the band"
      elsif self.fan_access && !self.fan_chat
        "Fans may listen, chat with each other"
      elsif !self.fan_access && !self.fan_chat
        "Fans may not listen to session"
      end
    end

    def access_description
      "#{musician_access_description}. #{fan_access_description}."
    end

    # retrieve users that have approved RSVPs
    def approved_rsvps
      User.find_by_sql(%Q{select distinct ON(u.id) u.id, u.photo_url, u.first_name, u.last_name, u.last_jam_audio_latency, json_agg(ii.id) as instrument_ids, json_agg(ii.description) as instrument_descriptions, json_agg(rs.proficiency_level) as instrument_proficiencies, json_agg(rr.id) as rsvp_request_ids
                        from rsvp_slots rs
                        inner join rsvp_requests_rsvp_slots rrrs on rrrs.rsvp_slot_id = rs.id
                        inner join rsvp_requests rr on rrrs.rsvp_request_id = rr.id
                        left join instruments ii on ii.id = rs.instrument_id
                        inner join users u on u.id = rr.user_id
                        where rrrs.chosen = true AND rs.music_session_id = '#{self.id}' AND rr.canceled != TRUE
                        group by u.id order by u.id}
                  )
    end

    # get all slots for this session and perform a set difference with all chosen slots;
    # this will return those that are not filled yet
    # this method excludes rsvp_slots marked as 'is_unstructured_rsvp = true'
    def open_slots
      RsvpSlot.find_by_sql(%Q{select rs.*, ii.description
                              from rsvp_slots rs
                              inner join instruments ii on ii.id = rs.instrument_id
                              where rs.music_session_id = '#{self.id}'
                              and rs.is_unstructured_rsvp = false
                              except
                              select distinct rs.*, iii.description
                              from rsvp_slots rs
                              inner join instruments iii on iii.id = rs.instrument_id
                              inner join rsvp_requests_rsvp_slots rrrs on rrrs.rsvp_slot_id = rs.id
                              where rs.music_session_id = '#{self.id}'
                              and rrrs.chosen = true
                            }
                          )
    end

    # retrieve users that have invitations but have not submitted an RSVP request for this session
    def pending_invitations
      User.find_by_sql(%Q{select distinct u.id, u.email, u.photo_url, u.first_name, u.last_name, u.online
                              from users u
                              inner join invitations i on u.id = i.receiver_id
                              left join rsvp_requests rr on rr.user_id = i.receiver_id
                              where i.music_session_id = '#{self.id}'
                              and rr.user_id is null}
                          )
    end

    # retrieve pending RsvpRequests
    def pending_rsvp_requests
      RsvpRequest.index(self, nil, {status: 'pending'})
    end

    def running_recordings
      recordings.where(duration: nil)
    end
    def recordings
      Recording.where(music_session_id: self.id)
    end

    def self.cleanup_old_sessions
      old_scheduled_start = "(create_type is NOT NULL AND music_sessions.scheduled_start < NOW() - '24 hour'::INTERVAL)"

      old_adhoc_sessions = "(create_type IS NULL and music_sessions.created_at < NOW() - '24 hour'::INTERVAL)"

      MusicSession.where("#{old_scheduled_start} OR #{old_adhoc_sessions}").where(old:false).update_all(old: true)
    end

    def end_history
      self.update_attribute(:session_removed_at, Time.now)


      # ensure all user histories are closed
      music_session_user_histories.each do |music_session_user_history|
        music_session_user_history.end_history

        # then update any users that need their user progress updated
        if music_session_user_history.duration_minutes > 15 && music_session_user_history.max_concurrent_connections.to_i >= 3
          music_session_user_history.user.update_progression_field(:first_real_music_session_at)
        end
      end

    end

    def self.removed_music_session(session_id)
      hist = self
        .where(:id => session_id)
        .limit(1)
        .first

      hist.end_history if hist

      Notification.send_session_ended(session_id)
    end

    def remove_non_alpha_num(token)
      token.gsub(/[^0-9A-Za-z]/, '')
    end

    def tag
      nil unless has_attribute?(:tag)
      a = read_attribute(:tag)
      a.nil? ? nil : a.to_i
    end

    def latency
      nil unless has_attribute?(:latency)
      a = read_attribute(:latency)
      a.nil? ? nil : a.to_i
    end


    # initialize the two temporary tables we use to drive sms_index and sms_users
    def self.sms_init(current_user, options = {}, include_pending=false)
      session_id = options[:session_id] || 'any'

      my_locidispid = current_user.last_jam_locidispid
      # 13 is an average audio gear value we use if they have not qualified any gear
      my_audio_latency = current_user.last_jam_audio_latency || 13
      locidispid_expr = my_locidispid ? "#{my_locidispid}::bigint" : '0::bigint'  # Have to pass in zero; NULL fails silently in the stored proc

      self.connection.execute("SELECT sms_index('#{current_user.id}'::varchar, #{locidispid_expr}, #{my_audio_latency}::integer, #{ActiveRecord::Base.connection.quote(session_id)}::varchar, #{include_pending}::boolean)").check
    end

    # Generate a list of music sessions (that are active) filtered by genre, language, keyword, and sorted
    # (and tagged) by rsvp'd (1st), invited (2nd), and musician can join (3rd). within a group tagged the
    # same, sorted by score. date seems irrelevant as these are active sessions. sms_init must be called
    # first.
    def self.sms_query(current_user, options = {})
      client_id = options[:client_id]
      genre = options[:genre]
      lang = options[:lang]
      keyword = options[:keyword]
      offset = options[:offset]
      limit = options[:limit]
      day = options[:day]
      timezone_offset = options[:timezone_offset]

      query = MusicSession
      .select('music_sessions.*')

      # this is not really needed when sms_music_session_tmp is joined
      # unless there is something specific we need out of active_music_sessions
      # query = query.joins(
      #   %Q{
      #     INNER JOIN
      #       active_music_sessions
      #     ON
      #       active_music_sessions.id = music_sessions.id
      #   }
      # )
      # .select('1::integer as tag, 15::integer as latency')

      # integrate sms_music_session_tmp into the processing
      # then we can join sms_music_session_tmp and not join active_music_sessions
      query = query.joins(
          %Q{
          INNER JOIN
            sms_music_session_tmp
          ON
            sms_music_session_tmp.music_session_id = music_sessions.id
        }
      )
      .select('sms_music_session_tmp.tag, sms_music_session_tmp.latency')

      query = query.order(
          %Q{
          tag, latency, music_sessions.id
        }
      )
      .group(
          %Q{
          tag, latency, music_sessions.id
        }
      )

      # if not specified, default offset to 0
      offset ||= 0
      offset = offset.to_i
      # if not specified, default limit to 20
      limit ||= 20
      limit = limit.to_i

      query = query.offset(offset)
      query = query.limit(limit)
      query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE, MusicSession::CREATE_TYPE_QUICK_PUBLIC)
      query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank?
      query = query.where('music_sessions.language = ?', lang) unless lang.blank?
      query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank?

      if !day.blank? && !timezone_offset.blank?
        begin
          day = Date.parse(day)
          next_day = day + 1
          timezone_offset = timezone_offset.to_i
          if timezone_offset == 0
            timezone_offset = ''  # no offset to specify in this case
          elsif timezone_offset > 0
            timezone_offset = "+#{timezone_offset}"
          end
          query = query.where("scheduled_start BETWEEN TIMESTAMP WITH TIME ZONE '#{day} 00:00:00#{timezone_offset}'
                    AND TIMESTAMP WITH TIME ZONE '#{next_day} 00:00:00#{timezone_offset}'")
        rescue Exception => e
          # do nothing. bad date probably
          @@log.warn("unable to parse  day=#{day}, timezone_offset=#{timezone_offset}, e=#{e}")
        end
      else
        sql =<<SQL
music_sessions.started_at IS NULL AND 
(music_sessions.created_at > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days' OR
music_sessions.scheduled_start > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days')
SQL
        query = query.where(sql)
      end
      return query
    end

    # returns the set of users in a music_sessions and the music_session they are in and their latency.
    # sms_init must be called first.
    # user.audio_latency / 2 , + other_user.audio_latency of them / 2, + network latency /2
    def self.sms_users
      return User.select('users.*, sms_users_tmp.music_session_id, sms_users_tmp.full_score, sms_users_tmp.audio_latency, sms_users_tmp.internet_score')
      .joins(
          %Q{
          INNER JOIN
            sms_users_tmp
          ON
            sms_users_tmp.user_id = users.id
        }
      )
      .order('sms_users_tmp.music_session_id, sms_users_tmp.user_id')
    end

    # NOTE: Unused anymore!!
    #
    # wrap me in a transaction!
    # note that these queries must be actualized before the end of the transaction
    # else the temporary tables created by sms_init will be gone.
    def self.sms_index(current_user, params)
      MusicSession.sms_init(current_user, params)

      music_sessions = MusicSession.sms_query(current_user, params).all

      music_session_users = MusicSession.sms_users.all

      user_scores = {}
      music_session_users.each do |user|
        user_scores[user.id] = {full_score: user.full_score,  audio_latency: user.audio_latency, internet_score: user.internet_score}
      end


      [music_sessions, user_scores]
    end


    def self.scheduled_index(user, options)
      session_id = options[:session_id]
      genre = options[:genre]
      lang = options[:lang]
      keyword = options[:keyword]
      offset = options[:offset]
      limit = options[:limit]
      day = options[:day]
      timezone_offset = options[:timezone_offset]

      query = MusicSession.select('music_sessions.*')
      query = query.where("old = FALSE")
      query = query.where("scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute'))")
      query = query.where("music_sessions.canceled = FALSE")
      query = query.where("description != 'Jam Track Session'")
      query = query.where("music_sessions.id NOT IN (SELECT id FROM active_music_sessions)")
      # one flaw in the join below is that we only consider if the creator of the session has asked you to be your friend, but you have not accepted.  While this means you may always see a session by someone you haven't friended, the goal is to match up new users more quickly
      query = query.joins(
          %Q{

            LEFT OUTER JOIN
              rsvp_requests
            ON rsvp_requests.music_session_id = music_sessions.id and rsvp_requests.user_id = '#{user.id}' AND rsvp_requests.chosen = true

            LEFT OUTER JOIN
              invitations
            ON
              music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}'
            LEFT OUTER JOIN
              friendships
            ON
              music_sessions.user_id = friendships.user_id AND friendships.friend_id = '#{user.id}'
            LEFT OUTER JOIN
              friendships as friendships_2
            ON
              music_sessions.user_id = friendships_2.friend_id AND friendships_2.user_id = '#{user.id}'
          }
      )

      # keep only rsvp/invitation/friend results.  Nice tailored active list now!
      query = query.where("open_rsvps = TRUE OR rsvp_requests.id IS NOT NULL OR invitations.id IS NOT NULL or music_sessions.user_id = '#{user.id}' OR (friendships.id IS NOT NULL AND friendships_2.id IS NOT NULL)")

      query = Search.scope_schools_together_sessions(query, user, 'music_sessions')

      # if not specified, default offset to 0
      offset ||= 0
      offset = offset.to_i
      # if not specified, default limit to 20
      limit ||= 20
      limit = limit.to_i

      query = query.offset(offset)
      query = query.limit(limit)
      query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank?
      query = query.where('music_sessions.language = ?', lang) unless lang.blank?
      query = query.where('music_sessions.id = ?', session_id) unless session_id.blank?
      query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))",  ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank?
      query = query.group("music_sessions.id, music_sessions.scheduled_start")
      query = query.order("music_sessions.scheduled_start DESC")

      if !day.blank? && !timezone_offset.blank?
        begin
          day = Date.parse(day)
          next_day = day + 1
          timezone_offset = timezone_offset.to_i
          if timezone_offset == 0
            timezone_offset = ''  # no offset to specify in this case
          elsif timezone_offset > 0
            timezone_offset = "+#{timezone_offset}"
          end
          query = query.where("scheduled_start BETWEEN TIMESTAMP WITH TIME ZONE '#{day} 00:00:00#{timezone_offset}'
                    AND TIMESTAMP WITH TIME ZONE '#{next_day} 00:00:00#{timezone_offset}'")
        rescue Exception => e
          # do nothing. bad date probably
          @@log.warn("unable to parse  day=#{day}, timezone_offset=#{timezone_offset}, e=#{e}")
        end
      else
        sql =<<SQL
music_sessions.started_at IS NULL AND
(music_sessions.created_at > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days' OR
music_sessions.scheduled_start > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days')
SQL
        query = query.where(sql)
      end

      #FROM music_sessions
      # WHERE  old = FALSE AND (scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute')))
      # AND canceled = FALSE AND description != 'Jam Track Session'
      # AND id NOT IN (SELECT id FROM active_music_sessions);

      query
    end

    # returns a single session, but populates any other user info with latency scores, so that show_history.rabl can do it's business
    def self.session_with_scores(current_user, music_session_id, include_pending=false)
      MusicSession.sms_init(current_user, {session_id: music_session_id}, include_pending)

      music_session = MusicSession.find(music_session_id)

      music_session_users = MusicSession.sms_users.all

      user_scores = {}
      music_session_users.each do |user|
        user_scores[user.id] = {full_score: user.full_score, audio_latency: user.audio_latency, internet_score: user.internet_score}
      end

      [music_session, user_scores]
    end

    def self.upcoming_sessions

    end
    # converts the passed scheduled_start into the database timezone using the specified timezone offset.
    # timezone comes in as TIMEZONE DISPLAY, TIMEZONE ID
    def self.parse_scheduled_start(scheduled_start, timezone_param)

      result = scheduled_start

      tz_identifier = split_timezone(timezone_param)[0]
      begin
        timezone = ActiveSupport::TimeZone.new(tz_identifier)
      rescue Exception => e
        @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
        puts "unable to find timezone=#{tz_identifier}, e=#{e}"
      end

      if timezone
        begin
          # first convert the time provided, and convert to the specified timezone (local_to_utc)
          # then, convert that to the system timezone, under the ASSUMPTION that the database is configured to use the system timezone
          # you can get into trouble if your dev database is not using the system timezone of the web machine

          result = timezone.parse(scheduled_start)
        rescue Exception => e
          @@log.error("unable to convert #{scheduled_start} to #{timezone}, e=#{e}")
          puts "unable to convert #{scheduled_start} to #{timezone}, e=#{e}"
        end
      end

      result
    end

      def scheduled_start_date
      if self.scheduled_start_time.blank?
        ""
      else
        self.scheduled_start.strftime "%a %e %B %Y"
      end
    end

    def scheduled_start_time
      if scheduled_start && scheduled_duration
        start_time = scheduled_start
        tz_identifier, tz_display = MusicSession.split_timezone(timezone)
        begin
          tz = TZInfo::Timezone.get(tz_identifier)
        rescue Exception => e
          @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
        end

        if tz
          begin
            start_time = tz.utc_to_local(scheduled_start.utc)
          rescue Exception => e
            @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}")
            puts "unable to convert #{e}"
          end
        end

        start_time
      else
        ""
      end
    end

    # takes our stored timezone which is DISPLAY,ID and returns an array of the two values (id first, then description)
    def self.split_timezone(tz)
      result = [nil, nil]
      if tz
        index = tz.rindex(',')
        if index
          tz_display = tz[0, index]
          tz_identifier = tz[(index + 1)..-1]
          result = [tz_identifier, tz_display]
        end
      end
      result
    end

    def safe_scheduled_duration
      duration =  scheduled_duration
      # you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string
      if scheduled_duration.class == String
        duration = scheduled_duration.to_i.seconds
      end
      if duration == 0
        duration = 30 * 60
      end
      duration
    end

    def pretty_scheduled_start(with_timezone = true, shorter = false, user_tz = nil)

      if scheduled_start && scheduled_duration
        start_time = scheduled_start
        timezone_display = 'UTC'
        utc_offset_display = '00:00'
        tz_identifier, tz_display = MusicSession.split_timezone(timezone)
        short_tz = 'GMT'

        if user_tz
          tz_identifier = user_tz
        end
        begin
          tz = TZInfo::Timezone.get(tz_identifier)
        rescue Exception => e
          @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
        end

        if tz
          begin
            start_time = tz.utc_to_local(scheduled_start.utc)
            timezone_display = tz.pretty_name
            utc_offset_hours = tz.current_period.utc_total_offset /  (60*60)
            hour =  sprintf '%02d', utc_offset_hours.abs
            minutes = sprintf '%02d', ((tz.current_period.utc_total_offset.abs % 3600) / 3600) * 60
            utc_offset_display = "#{utc_offset_hours < 0 ? '-' : ' '}#{hour}:#{minutes}"
            short_tz = start_time.strftime("%Z")
            if short_tz == 'UTC'
              short_tz = 'GMT'
            end

          rescue Exception => e
            @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}")
            puts "unable to convert #{e}"
          end
        end


        duration = safe_scheduled_duration
        end_time = start_time + duration
        if with_timezone
          if shorter
            #"#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{short_tz}#{utc_offset_display})"
            "#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{timezone_display})"
          else
            "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}"
          end
        else
          "#{start_time.strftime("%A, %B %e")} - #{end_time.strftime("%l:%M%P").strip}"
        end

      else
        "Date and time TBD"
      end

    end

    def self.purgeable_sessions
      sessions = []
      sql =<<SQL
  (started_at IS NULL AND
    (created_at <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE} days' OR
     scheduled_start <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE} days')) 
OR 
  (recurring_mode = '#{NO_RECURRING}' AND 
    scheduled_start <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE} days')
OR 
  (recurring_mode = '#{RECURRING_WEEKLY}' AND
    scheduled_start <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE_RECUR} days') 
SQL
      self.where("lesson_session_id is NULL").where("started_at IS NULL").where(sql).find_each do |ms|
        block_given? ? yield(ms) : sessions << ms
      end
      sessions
    end

    def admin_url
      APP_CONFIG.admin_root_url + "/admin/music_sessions/" + id
    end

    private
    def generate_share_token

      token = loop do
        token = SecureRandom.urlsafe_base64(SHARE_TOKEN_LENGTH, false)
        token = remove_non_alpha_num(token)
        token.upcase!
        break token unless ShareToken.exists?(token: token)
      end

      self.share_token = ShareToken.new
      self.share_token.token = token
      self.share_token.shareable_type = "session"
    end

    def creator_is_musician
      unless creator && creator.musician?
        errors.add(:creator, ValidationMessages::MUST_BE_A_MUSICIAN)
      end
    end

    def validate_timezone
      if timezone
        index = timezone.rindex(',')
        if index
          tz_identifier = timezone[(index + 1)..-1]
          begin
            TZInfo::Timezone.get(tz_identifier)
          rescue Exception => e
            @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
            errors.add(:timezone, ValidationMessages::MUST_BE_KNOWN_TIMEZONE)
          end
        end
      end
    end
  end
end
