module JamRuby
  class LessonSessionAnalyser

    SUCCESS = 'success'
    SESSION_ONGOING = 'session_ongoing'
    THRESHOLD_MET = 'threshold_met'
    WAITED_CORRECTLY = 'waited_correctly'
    MINIMUM_TIME_MET = 'minimum_time_met' # for a teacher primarily; they waited around for the student sufficiently
    MINIMUM_TIME_NOT_MET = 'mininum_time_not_met'
    LATE_CANCELLATION = 'late_cancellation'

    TEACHER_FAULT = 'teacher_fault'
    STUDENT_FAULT = 'student_fault'
    BOTH_FAULT = 'both_fault'

    STUDENT_NOT_THERE_WHEN_JOINED = 'student_not_there_when_joined'
    JOINED_LATE = 'did_not_join_on_time'
    NO_SHOW = 'no_show'
    NEITHER_SHOW = 'neither_show'


    # what are the potential results?

    # bill: true/false

    # teacher: 'no_show'
    # teacher: 'late'
    # teacher: 'early_leave'
    # teacher: 'waited_correctly'
    # teacher: 'late_cancellation'

    # student: 'no_show'
    # student: 'late'
    # student: 'early_leave'
    # student: 'minimum_time_not_met'
    # student: 'threshold_met'


    # reason: 'session_ongoing'
    # reason: 'success'
    # reason: 'student_fault'
    # reason: 'teacher_fault'
    # reason: 'both_fault'


    def self.analyse(lesson_session, force = false)
      reason = nil
      teacher = nil
      student = nil
      bill = false

      music_session = lesson_session.music_session

      student_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.student.id)
      teacher_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.teacher.id)

      # create ranges from music session user history
      all_student_ranges = time_ranges(student_histories)
      all_teacher_ranges = time_ranges(teacher_histories)

      # flatten ranges into non-overlapping ranges to simplifly logic
      student_ranges = merge_overlapping_ranges(all_student_ranges)
      teacher_ranges = merge_overlapping_ranges(all_teacher_ranges)

      intersecting = intersecting_ranges(student_ranges, teacher_ranges)
      student_analysis = analyse_intersection(lesson_session, student_ranges)
      teacher_analysis = analyse_intersection(lesson_session, teacher_ranges)
      together_analysis = analyse_intersection(lesson_session, intersecting)

      # spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest

      if !force && !((music_session.scheduled_start + (lesson_session.duration * 60)) < Time.now)
        reason = SESSION_ONGOING
        bill = false
      else
        #if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late?
        #  # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student.
        #  teacher = LATE_CANCELLATION
        #  bill = false
        #elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late?
        #  # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student.
        #  student = LATE_CANCELLATION
        #  bill = true
        if together_analysis[:session_time] / 60 > APP_CONFIG.lesson_together_threshold_minutes
          bill = true
          reason = SUCCESS
        elsif teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly]
          # if the teacher was present in the session within the first 5 minutes of the scheduled start time and stayed in the session for 10 minutes;
          # and if either:

          if student_analysis[:no_show]
            # the student no-showed entirely, then we bill the student.
            student = NO_SHOW
            bill = true

          elsif student_analysis[:joined_late]
            #  the student joined the lesson more than 10 minutes after the teacher did, regardless of whether the teacher was still in the lesson session at that point; then we bill the student
            student = JOINED_LATE
            bill = true
          end
        else
          if teacher_analysis[:no_show]
            teacher = NO_SHOW
          elsif !teacher_analysis[:joined_on_time]
            teacher = JOINED_LATE
          elsif !teacher_analysis[:waited_correctly]
            teacher = MINIMUM_TIME_NOT_MET
          end
        end


      end

      if reason.nil?
        if student
          reason = STUDENT_FAULT
        elsif teacher
          reason = TEACHER_FAULT
        else
          reason = NEITHER_SHOW
        end
      end


      {
          reason: reason,
          teacher: teacher,
          student: student,
          bill: bill,
          student_ranges: student_ranges,
          teacher_ranges: teacher_ranges,
          intersecting: intersecting,
          student_analysis: student_analysis,
          teacher_analysis: teacher_analysis,
          together_analysis: together_analysis,
      }
    end

    def self.annotate_timeline(lesson_session, analysis, ranges)
      start = lesson_session.scheduled_start
    end

    def self.intersecting_ranges(ranges_a, ranges_b)
      intersections = []
      ranges_a.each do |range_a|
        ranges_b.each do |range_b|
          intersection = intersect(range_a, range_b)
          intersections << intersection if intersection
        end
      end

      merge_overlapping_ranges(intersections)
    end

    # needs to add to joined_on_time
    # joined_on_time      bool
    # waited_correctly    bool
    # no_show             bool
    # joined_late         bool
    # minimum_time_met    bool
    # present_at_end      bool


    def self.analyse_intersection(lesson_session, ranges)
      # be sure to call .to_time on any ActiveRecord time, because we get a ton of deprecation warninsg about Time#succ if you use ActiveSupport:: TimeZone
      start = lesson_session.scheduled_start.to_time
      planned_duration_seconds = lesson_session.duration * 60
      end_time = start + planned_duration_seconds

      join_start_boundary_begin = start
      join_start_boundary_end = start + (APP_CONFIG.lesson_join_time_window_minutes * 60)

      wait_boundary_begin = start
      wait_boundary_end = start + (APP_CONFIG.lesson_wait_time_window_minutes * 60)

      initial_join_window = Range.new(join_start_boundary_begin, join_start_boundary_end)
      initial_wait_window = Range.new(wait_boundary_begin, wait_boundary_end)
      session_window = Range.new(start, end_time)

      # let's see how much time they spent together, irrespective of scheduled time
      # and also, based on scheduled time
      total = 0

      in_scheduled_time = 0
      in_wait_window_time = 0

      # the initial time joined in the initial 'waiting window'
      initial_join_in_scheduled_time = nil

      # the amount of time spent in the initial 'waiting window'
      initial_wait_time_in_scheduled_time = 0

      last_wait_time_out = nil

      joined_on_time = false
      waited_correctly = false
      no_show = true
      joined_late = false
      joined_in_wait_window = false
      ranges.each do |range|
        time = range.end - range.begin

        total += time

        in_session_range = intersect(range, session_window)
        in_join_window_range = intersect(range, initial_join_window)
        in_wait_window_range = intersect(range, initial_wait_window)

        if in_session_range
          in_scheduled_time += in_session_range.end - in_session_range.begin
          no_show = false
        end

        if in_join_window_range
          if initial_join_in_scheduled_time.nil?
            initial_join_in_scheduled_time = in_join_window_range.begin
          end
          joined_on_time = true
        end

        if in_wait_window_range
          in_wait_window_time += in_wait_window_range.end - in_wait_window_range.begin
          last_wait_time_out = range.end
          joined_in_wait_window = true
        end
      end

      if joined_in_wait_window && !joined_on_time
        joined_late = true
      end

      if last_wait_time_out && last_wait_time_out > wait_boundary_end
        last_wait_time_out = wait_boundary_end
      end

      initial_waiting_time_pct = nil
      potential_waiting_time = nil

      # let's see if this person was hanging around for the bulk of this waiting window (to rule out someone coming/going very fast, trying to miss someone)
      if last_wait_time_out && initial_join_in_scheduled_time
        total_in_waiting_time = 0
        potential_waiting_range = Range.new(initial_join_in_scheduled_time, last_wait_time_out)
        ranges.each do |range|
          in_waiting = intersect(potential_waiting_range, range)
          if in_waiting
            total_in_waiting_time += in_waiting.end - in_waiting.begin
          end
        end

        potential_waiting_time = last_wait_time_out - initial_join_in_scheduled_time
        initial_waiting_time_pct = total_in_waiting_time.to_f / potential_waiting_time.to_f

        # finally with all this stuff calculated, we can check:
        # 1) did they wait a solid % of time between the time they joined, and left, during the initial 10 minute waiting window?
        # 2) did they
        if (initial_waiting_time_pct >= APP_CONFIG.wait_time_window_pct) &&
            (last_wait_time_out >= (wait_boundary_end - (APP_CONFIG.end_of_wait_window_forgiveness_minutes * 60)))
          waited_correctly = true
        end
      end


      # percentage computation of time spent during the session time
      in_scheduled_percentage = in_scheduled_time.to_f / planned_duration_seconds.to_f

      # missed is an aggregrate concept shown in the UI often
      # if you were a no show, or joined late, or didn't wait correctly, then you 'missed'
      missed = no_show || joined_late || !waited_correctly

      joined_on_time = joined_on_time
      {
          total_time: total,
          session_time: in_scheduled_time,
          session_pct: in_scheduled_percentage,
          joined_on_time: joined_on_time,
          waited_correctly: waited_correctly,
          no_show: no_show,
          joined_late: joined_late,
          missed: missed,
          initial_join_in_scheduled_time: initial_join_in_scheduled_time,
          last_wait_time_out: last_wait_time_out,
          in_wait_window_time: in_wait_window_time,
          initial_waiting_pct: initial_waiting_time_pct,
          potential_waiting_time: potential_waiting_time
      }
    end

    # not OK for time objects.  Here for docs more than anything
    def self.intersect2(a, b)
      min, max = a.first, a.exclude_end? ? a.max : a.last
      other_min, other_max = b.first, b.exclude_end? ? b.max : b.last

      new_min = a === other_min ? other_min : b === min ? min : nil
      new_max = a === other_max ? other_max : b === max ? max : nil

      new_min && new_max ? Range.new(new_min, new_max) : nil
    end


    def self.contained_by(a, test)
      min, max = a.first, a.exclude_end? ? a.max : a.last

      test >= min && test <= max
    end

    def self.intersect(a, b)
      min, max = a.first, a.exclude_end? ? a.max : a.last
      other_min, other_max = b.first, b.exclude_end? ? b.max : b.last

      new_min = contained_by(a, other_min) ? other_min : contained_by(b, min) ? min : nil
      new_max = contained_by(a, other_max) ? other_max : contained_by(b, max) ? max : nil

      new_min && new_max ? Range.new(new_min, new_max) : nil
    end

    def self.time_ranges(histories)
      ranges = []
      histories.each do |history|
        ranges << history.range
      end
      ranges
    end

    def self.ranges_overlap?(a, b)
      a.cover?(b.begin) || b.cover?(a.begin)
    end

    def self.merge_ranges(a, b)
      [a.begin, b.begin].min..[a.end, b.end].max
    end

    def self.merge_overlapping_ranges(ranges)
      ranges.sort_by(&:begin).inject([]) do |ranges, range|
        if !ranges.empty? && ranges_overlap?(ranges.last, range)
          ranges[0...-1] + [merge_ranges(ranges.last, range)]
        else
          ranges + [range]
        end
      end
    end

  end
end