﻿# This file is part of Tautulli.
#
#  Tautulli is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Tautulli is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with Tautulli.  If not, see <http://www.gnu.org/licenses/>.

from collections import defaultdict
import json

import plexpy
from plexpy import database
from plexpy import helpers
from plexpy import libraries
from plexpy import logger
from plexpy import pmsconnect
from plexpy import users


class ActivityProcessor(object):

    def write_session(self, session=None, notify=True):
        if session:
            db = database.MonitorDatabase()

            values = {'session_key': session.get('session_key', ''),
                      'session_id': session.get('session_id', ''),
                      'transcode_key': session.get('transcode_key', ''),
                      'section_id': session.get('section_id', ''),
                      'rating_key': session.get('rating_key', ''),
                      'media_type': session.get('media_type', ''),
                      'state': session.get('state', ''),
                      'user_id': session.get('user_id', ''),
                      'user': session.get('user', ''),
                      'machine_id': session.get('machine_id', ''),
                      'title': session.get('title', ''),
                      'parent_title': session.get('parent_title', ''),
                      'grandparent_title': session.get('grandparent_title', ''),
                      'original_title': session.get('original_title', ''),
                      'full_title': session.get('full_title', ''),
                      'media_index': session.get('media_index', ''),
                      'parent_media_index': session.get('parent_media_index', ''),
                      'thumb': session.get('thumb', ''),
                      'parent_thumb': session.get('parent_thumb', ''),
                      'grandparent_thumb': session.get('grandparent_thumb', ''),
                      'year': session.get('year', ''),
                      'friendly_name': session.get('friendly_name', ''),
                      'ip_address': session.get('ip_address', ''),
                      'bandwidth': session.get('bandwidth', 0),
                      'location': session.get('location', ''),
                      'player': session.get('player', ''),
                      'product': session.get('product', ''),
                      'platform': session.get('platform', ''),
                      'parent_rating_key': session.get('parent_rating_key', ''),
                      'grandparent_rating_key': session.get('grandparent_rating_key', ''),
                      'originally_available_at': session.get('originally_available_at', ''),
                      'added_at': session.get('added_at', ''),
                      'guid': session.get('guid', ''),
                      'view_offset': session.get('view_offset', ''),
                      'duration': session.get('duration', '') or 0,
                      'video_decision': session.get('video_decision', ''),
                      'audio_decision': session.get('audio_decision', ''),
                      'transcode_decision': session.get('transcode_decision', ''),
                      'width': session.get('width', ''),
                      'height': session.get('height', ''),
                      'container': session.get('container', ''),
                      'bitrate': session.get('bitrate', ''),
                      'video_codec': session.get('video_codec', ''),
                      'video_bitrate': session.get('video_bitrate', ''),
                      'video_width': session.get('video_width', ''),
                      'video_height': session.get('video_height', ''),
                      'video_resolution': session.get('video_resolution', ''),
                      'video_framerate': session.get('video_framerate', ''),
                      'video_scan_type': session.get('video_scan_type', ''),
                      'video_full_resolution': session.get('video_full_resolution', ''),
                      'video_dynamic_range': session.get('video_dynamic_range', ''),
                      'aspect_ratio': session.get('aspect_ratio', ''),
                      'audio_codec': session.get('audio_codec', ''),
                      'audio_bitrate': session.get('audio_bitrate', ''),
                      'audio_channels': session.get('audio_channels', ''),
                      'audio_language': session.get('audio_language', ''),
                      'audio_language_code': session.get('audio_language_code', ''),
                      'subtitle_codec': session.get('subtitle_codec', ''),
                      'subtitle_forced': session.get('subtitle_forced', ''),
                      'subtitle_language': session.get('subtitle_language', ''),
                      'transcode_protocol': session.get('transcode_protocol', ''),
                      'transcode_container': session.get('transcode_container', ''),
                      'transcode_video_codec': session.get('transcode_video_codec', ''),
                      'transcode_audio_codec': session.get('transcode_audio_codec', ''),
                      'transcode_audio_channels': session.get('transcode_audio_channels', ''),
                      'transcode_width': session.get('stream_video_width', ''),
                      'transcode_height': session.get('stream_video_height', ''),
                      'transcode_hw_decoding': session.get('transcode_hw_decoding', ''),
                      'transcode_hw_encoding': session.get('transcode_hw_encoding', ''),
                      'synced_version': session.get('synced_version', ''),
                      'synced_version_profile': session.get('synced_version_profile', ''),
                      'optimized_version': session.get('optimized_version', ''),
                      'optimized_version_profile': session.get('optimized_version_profile', ''),
                      'optimized_version_title': session.get('optimized_version_title', ''),
                      'stream_bitrate': session.get('stream_bitrate', ''),
                      'stream_video_resolution': session.get('stream_video_resolution', ''),
                      'quality_profile': session.get('quality_profile', ''),
                      'stream_container_decision': session.get('stream_container_decision', ''),
                      'stream_container': session.get('stream_container', ''),
                      'stream_video_decision': session.get('stream_video_decision', ''),
                      'stream_video_codec': session.get('stream_video_codec', ''),
                      'stream_video_bitrate': session.get('stream_video_bitrate', ''),
                      'stream_video_width': session.get('stream_video_width', ''),
                      'stream_video_height': session.get('stream_video_height', ''),
                      'stream_video_framerate': session.get('stream_video_framerate', ''),
                      'stream_video_scan_type': session.get('stream_video_scan_type', ''),
                      'stream_video_full_resolution': session.get('stream_video_full_resolution', ''),
                      'stream_video_dynamic_range': session.get('stream_video_dynamic_range', ''),
                      'stream_audio_decision': session.get('stream_audio_decision', ''),
                      'stream_audio_codec': session.get('stream_audio_codec', ''),
                      'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
                      'stream_audio_channels': session.get('stream_audio_channels', ''),
                      'stream_audio_language': session.get('stream_audio_language', ''),
                      'stream_audio_language_code': session.get('stream_audio_language_code', ''),
                      'stream_subtitle_decision': session.get('stream_subtitle_decision', ''),
                      'stream_subtitle_codec': session.get('stream_subtitle_codec', ''),
                      'stream_subtitle_forced': session.get('stream_subtitle_forced', ''),
                      'stream_subtitle_language': session.get('stream_subtitle_language', ''),
                      'subtitles': session.get('subtitles', 0),
                      'live': session.get('live', 0),
                      'live_uuid': session.get('live_uuid', ''),
                      'secure': session.get('secure', None),
                      'relayed': session.get('relayed', 0),
                      'rating_key_websocket': session.get('rating_key_websocket', ''),
                      'raw_stream_info': json.dumps(session),
                      'channel_call_sign': session.get('channel_call_sign', ''),
                      'channel_id': session.get('channel_id', ''),
                      'channel_identifier': session.get('channel_identifier', ''),
                      'channel_title': session.get('channel_title', ''),
                      'channel_thumb': session.get('channel_thumb', ''),
                      'channel_vcn': session.get('channel_vcn', ''),
                      'stopped': helpers.timestamp()
                      }

            keys = {'session_key': session.get('session_key', ''),
                    'rating_key': session.get('rating_key', '')}

            result = db.upsert('sessions', values, keys)

            if result == 'insert':
                # If it's our first write then time stamp it.
                started = helpers.timestamp()
                initial_stream = self.is_initial_stream(user_id=values['user_id'],
                                                        machine_id=values['machine_id'],
                                                        media_type=values['media_type'],
                                                        started=started)
                timestamp = {'started': started, 'initial_stream': initial_stream}
                db.upsert('sessions', timestamp, keys)

                # Check if any notification agents have notifications enabled
                if notify:
                    session.update(timestamp)
                    plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})

                # Add Live TV library if it hasn't been added
                if values['live']:
                    libraries.add_live_tv_library()

                return True

    def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
        section_id = session['section_id'] if not is_import else import_metadata['section_id']

        if not is_import:
            user_data = users.Users()
            user_details = user_data.get_details(user_id=session['user_id'])

            library_data = libraries.Libraries()
            library_details = library_data.get_details(section_id=section_id)

            # Return false if failed to retrieve user or library details
            if not user_details or not library_details:
                return False

        if session:
            logging_enabled = False

            # Reload json from raw stream info
            if session.get('raw_stream_info'):
                raw_stream_info = json.loads(session['raw_stream_info'])
                # Don't overwrite id, session_key, stopped, view_offset
                raw_stream_info.pop('id', None)
                raw_stream_info.pop('session_key', None)
                raw_stream_info.pop('stopped', None)
                raw_stream_info.pop('view_offset', None)
                session.update(raw_stream_info)

            session = defaultdict(str, session)

            if is_import:
                if str(session['stopped']).isdigit():
                    stopped = int(session['stopped'])
                else:
                    stopped = helpers.timestamp()
            elif session['stopped']:
                stopped = int(session['stopped'])
            else:
                stopped = helpers.timestamp()
                self.set_session_state(session_key=session['session_key'],
                                       state='stopped',
                                       stopped=stopped)

            if not is_import:
                self.write_continued_session(user_id=session['user_id'],
                                             machine_id=session['machine_id'],
                                             media_type=session['media_type'],
                                             stopped=stopped)

            if str(session['rating_key']).isdigit() and session['media_type'] in ('movie', 'episode', 'track'):
                logging_enabled = True
            else:
                logger.debug("Tautulli ActivityProcessor :: Session %s ratingKey %s not logged. "
                             "Does not meet logging criteria. Media type is '%s'" %
                             (session['session_key'], session['rating_key'], session['media_type']))
                return session['id']

            real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(session['paused_counter'])

            if not is_import and plexpy.CONFIG.LOGGING_IGNORE_INTERVAL:
                if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
                        (real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
                    logging_enabled = False
                    logger.debug("Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs "
                                 "which is less than %s seconds, so we're not logging it." %
                                 (session['session_key'], session['rating_key'], str(real_play_time),
                                  plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
            if not is_import and session['media_type'] == 'track':
                if real_play_time < 15 and helpers.cast_to_int(session['duration']) >= 30:
                    logging_enabled = False
                    logger.debug("Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs, "
                                 "looks like it was skipped so we're not logging it" %
                                 (session['session_key'], session['rating_key'], str(real_play_time)))
            elif is_import and import_ignore_interval:
                if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
                        (real_play_time < int(import_ignore_interval)):
                    logging_enabled = False
                    logger.debug("Tautulli ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
                                 "seconds, so we're not logging it." %
                                 (session['rating_key'], str(real_play_time), import_ignore_interval))

            if not is_import and not user_details['keep_history']:
                logging_enabled = False
                logger.debug("Tautulli ActivityProcessor :: History logging for user '%s' is disabled." % user_details['username'])
            elif not is_import and not library_details['keep_history']:
                logging_enabled = False
                logger.debug("Tautulli ActivityProcessor :: History logging for library '%s' is disabled." % library_details['section_name'])

            if logging_enabled:
                db = database.MonitorDatabase()

                media_info = {}

                # Fetch metadata first so we can return false if it fails
                if not is_import:
                    logger.debug("Tautulli ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
                    pms_connect = pmsconnect.PmsConnect()
                    if session['live']:
                        metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']),
                                                                    cache_key=session['session_key'],
                                                                    return_cache=True)
                    else:
                        metadata = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))

                    if session['live'] and not metadata:
                        metadata = session
                    elif not metadata:
                        return False
                    else:
                        if 'media_info' in metadata and len(metadata['media_info']) > 0:
                            media_info = metadata['media_info'][0]
                else:
                    metadata = import_metadata
                    ## TODO: Fix media info from imports. Temporary media info from import session.
                    media_info = session

                # logger.debug("Tautulli ActivityProcessor :: Attempting to write sessionKey %s to session_history table..."
                #              % session['session_key'])
                keys = {'id': None}
                values = {'started': session['started'],
                          'stopped': stopped,
                          'rating_key': session['rating_key'],
                          'parent_rating_key': session['parent_rating_key'],
                          'grandparent_rating_key': session['grandparent_rating_key'],
                          'media_type': session['media_type'],
                          'user_id': session['user_id'],
                          'user': session['user'],
                          'ip_address': session['ip_address'],
                          'paused_counter': session['paused_counter'],
                          'player': session['player'],
                          'product': session['product'],
                          'product_version': session['product_version'],
                          'platform': session['platform'],
                          'platform_version': session['platform_version'],
                          'profile': session['profile'],
                          'machine_id': session['machine_id'],
                          'bandwidth': session['bandwidth'],
                          'location': session['location'],
                          'quality_profile': session['quality_profile'],
                          'view_offset': session['view_offset'],
                          'section_id': metadata['section_id'],
                          'secure': session['secure'],
                          'relayed': session['relayed']
                          }

                # logger.debug("Tautulli ActivityProcessor :: Writing sessionKey %s session_history transaction..."
                #              % session['session_key'])
                db.upsert(table_name='session_history', key_dict=keys, value_dict=values)

                # Get the last insert row id
                last_id = db.last_insert_id()
                self.group_history(last_id, session, metadata)
                
                # logger.debug("Tautulli ActivityProcessor :: Successfully written history item, last id for session_history is %s"
                #              % last_id)

                # Write the session_history_media_info table

                # logger.debug("Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_media_info table..."
                #              % session['session_key'])
                keys = {'id': last_id}
                values = {'rating_key': session['rating_key'],
                          'video_decision': session['video_decision'],
                          'audio_decision': session['audio_decision'],
                          'transcode_decision': session['transcode_decision'],
                          'duration': session['duration'],
                          'container': session['container'],
                          'bitrate': session['bitrate'],
                          'width': session['width'],
                          'height': session['height'],
                          'video_bit_depth': session['video_bit_depth'],
                          'video_bitrate': session['video_bitrate'],
                          'video_codec': session['video_codec'],
                          'video_codec_level': session['video_codec_level'],
                          'video_width': session['video_width'],
                          'video_height': session['video_height'],
                          'video_resolution': session['video_resolution'],
                          'video_framerate': session['video_framerate'],
                          'video_scan_type': session['video_scan_type'],
                          'video_full_resolution': session['video_full_resolution'],
                          'video_dynamic_range': session['video_dynamic_range'],
                          'aspect_ratio': session['aspect_ratio'],
                          'audio_codec': session['audio_codec'],
                          'audio_bitrate': session['audio_bitrate'],
                          'audio_channels': session['audio_channels'],
                          'audio_language': session['audio_language'],
                          'audio_language_code': session['audio_language_code'],
                          'subtitle_codec': session['subtitle_codec'],
                          'subtitle_forced': session['subtitle_forced'],
                          'subtitle_language': session['subtitle_language'],
                          'transcode_protocol': session['transcode_protocol'],
                          'transcode_container': session['transcode_container'],
                          'transcode_video_codec': session['transcode_video_codec'],
                          'transcode_audio_codec': session['transcode_audio_codec'],
                          'transcode_audio_channels': session['transcode_audio_channels'],
                          'transcode_width': session['transcode_width'],
                          'transcode_height': session['transcode_height'],
                          'transcode_hw_requested': session['transcode_hw_requested'],
                          'transcode_hw_full_pipeline': session['transcode_hw_full_pipeline'],
                          'transcode_hw_decoding': session['transcode_hw_decoding'],
                          'transcode_hw_decode': session['transcode_hw_decode'],
                          'transcode_hw_decode_title': session['transcode_hw_decode_title'],
                          'transcode_hw_encoding': session['transcode_hw_encoding'],
                          'transcode_hw_encode': session['transcode_hw_encode'],
                          'transcode_hw_encode_title': session['transcode_hw_encode_title'],
                          'stream_container': session['stream_container'],
                          'stream_container_decision': session['stream_container_decision'],
                          'stream_bitrate': session['stream_bitrate'],
                          'stream_video_decision': session['stream_video_decision'],
                          'stream_video_bitrate': session['stream_video_bitrate'],
                          'stream_video_codec': session['stream_video_codec'],
                          'stream_video_codec_level': session['stream_video_codec_level'],
                          'stream_video_bit_depth': session['stream_video_bit_depth'],
                          'stream_video_height': session['stream_video_height'],
                          'stream_video_width': session['stream_video_width'],
                          'stream_video_resolution': session['stream_video_resolution'],
                          'stream_video_framerate': session['stream_video_framerate'],
                          'stream_video_scan_type': session['stream_video_scan_type'],
                          'stream_video_full_resolution': session['stream_video_full_resolution'],
                          'stream_video_dynamic_range': session['stream_video_dynamic_range'],
                          'stream_audio_decision': session['stream_audio_decision'],
                          'stream_audio_codec': session['stream_audio_codec'],
                          'stream_audio_bitrate': session['stream_audio_bitrate'],
                          'stream_audio_channels': session['stream_audio_channels'],
                          'stream_audio_language': session['stream_audio_language'],
                          'stream_audio_language_code': session['stream_audio_language_code'],
                          'stream_subtitle_decision': session['stream_subtitle_decision'],
                          'stream_subtitle_codec': session['stream_subtitle_codec'],
                          'stream_subtitle_container': session['stream_subtitle_container'],
                          'stream_subtitle_forced': session['stream_subtitle_forced'],
                          'stream_subtitle_language': session['stream_subtitle_language'],
                          'subtitles': session['subtitles'],
                          'synced_version': session['synced_version'],
                          'synced_version_profile': session['synced_version_profile'],
                          'optimized_version': session['optimized_version'],
                          'optimized_version_profile': session['optimized_version_profile'],
                          'optimized_version_title': session['optimized_version_title']
                          }

                # logger.debug("Tautulli ActivityProcessor :: Writing sessionKey %s session_history_media_info transaction..."
                #              % session['session_key'])
                db.upsert(table_name='session_history_media_info', key_dict=keys, value_dict=values)

                # Write the session_history_metadata table
                directors = ";".join(metadata['directors'])
                writers = ";".join(metadata['writers'])
                actors = ";".join(metadata['actors'])
                genres = ";".join(metadata['genres'])
                labels = ";".join(metadata['labels'])

                marker_credits_first = None
                marker_credits_final = None
                for marker in metadata['markers']:
                    if marker['first']:
                        marker_credits_first = marker['start_time_offset']
                    if marker['final']:
                        marker_credits_final = marker['start_time_offset']

                # logger.debug("Tautulli ActivityProcessor :: Attempting to write to sessionKey %s session_history_metadata table..."
                #              % session['session_key'])
                keys = {'id': last_id}
                values = {'rating_key': session['rating_key'],
                          'parent_rating_key': session['parent_rating_key'],
                          'grandparent_rating_key': session['grandparent_rating_key'],
                          'title': session['title'],
                          'parent_title': session['parent_title'],
                          'grandparent_title': session['grandparent_title'],
                          'original_title': session['original_title'],
                          'full_title': session['full_title'],
                          'media_index': metadata['media_index'],
                          'parent_media_index': metadata['parent_media_index'],
                          'thumb': metadata['thumb'],
                          'parent_thumb': metadata['parent_thumb'],
                          'grandparent_thumb': metadata['grandparent_thumb'],
                          'art': metadata['art'],
                          'media_type': session['media_type'],
                          'year': metadata['year'],
                          'originally_available_at': metadata['originally_available_at'],
                          'added_at': metadata['added_at'],
                          'updated_at': metadata['updated_at'],
                          'last_viewed_at': metadata['last_viewed_at'],
                          'content_rating': metadata['content_rating'],
                          'summary': metadata['summary'],
                          'tagline': metadata['tagline'],
                          'rating': metadata['rating'],
                          'duration': metadata['duration'],
                          'guid': metadata['guid'],
                          'directors': directors,
                          'writers': writers,
                          'actors': actors,
                          'genres': genres,
                          'studio': metadata['studio'],
                          'labels': labels,
                          'live': session['live'],
                          'channel_call_sign': media_info.get('channel_call_sign', session.get('channel_call_sign', '')),
                          'channel_id': media_info.get('channel_id', session.get('channel_id', '')),
                          'channel_identifier': media_info.get('channel_identifier', session.get('channel_identifier', '')),
                          'channel_title': media_info.get('channel_title', session.get('channel_title', '')),
                          'channel_thumb': media_info.get('channel_thumb', session.get('channel_thumb', '')),
                          'channel_vcn': media_info.get('channel_vcn', session.get('channel_vcn', '')),
                          'marker_credits_first': marker_credits_first,
                          'marker_credits_final': marker_credits_final
                          }

                # logger.debug("Tautulli ActivityProcessor :: Writing sessionKey %s session_history_metadata transaction..."
                #              % session['session_key'])
                db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values)

            # Return the session row id when the session is successfully written to the database
            return session['id']

    def group_history(self, last_id, session, metadata=None):
        new_session = prev_session = None
        prev_watched = None

        db = database.MonitorDatabase()

        if session['live']:
            # Check if we should group the session, select the last guid from the user within the last day
            query = "SELECT session_history.id, session_history_metadata.guid, session_history.reference_id " \
                    "FROM session_history " \
                    "JOIN session_history_metadata ON session_history.id == session_history_metadata.id " \
                    "WHERE session_history.id <= ? AND session_history.user_id = ? " \
                    "AND datetime(session_history.started, 'unixepoch', 'localtime') > datetime('now', '-1 day') " \
                    "ORDER BY session_history.id DESC LIMIT 1 "

            args = [last_id, session['user_id']]

            result = db.select(query=query, args=args)

            if len(result) > 0:
                new_session = {'id': last_id,
                               'guid': metadata['guid'] if metadata else session['guid'],
                               'reference_id': last_id}

                prev_session = {'id': result[0]['id'],
                                'guid': result[0]['guid'],
                                'reference_id': result[0]['reference_id']}
                
                prev_watched = False

        else:
            # Check if we should group the session, select the last two rows from the user
            query = "SELECT id, rating_key, view_offset, reference_id FROM session_history " \
                    "WHERE id <= ? AND user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 "

            args = [last_id, session['user_id'], session['rating_key']]

            result = db.select(query=query, args=args)

            if len(result) > 1:
                new_session = {'id': result[0]['id'],
                               'rating_key': result[0]['rating_key'],
                               'view_offset': helpers.cast_to_int(result[0]['view_offset']),
                               'reference_id': result[0]['reference_id']}

                prev_session = {'id': result[1]['id'],
                                'rating_key': result[1]['rating_key'],
                                'view_offset': helpers.cast_to_int(result[1]['view_offset']),
                                'reference_id': result[1]['reference_id']}

                if metadata:
                    marker_first, marker_final = helpers.get_first_final_marker(metadata['markers'])
                else:
                    marker_first = session['marker_credits_first']
                    marker_final = session['marker_credits_final']

                prev_watched = helpers.check_watched(
                    session['media_type'], prev_session['view_offset'], session['duration'],
                    marker_first, marker_final
                )

        query = "UPDATE session_history SET reference_id = ? WHERE id = ? "

        # If previous session view offset less than watched threshold,
        # and new session view offset is greater,
        # then set the reference_id to the previous row,
        # else set the reference_id to the new id
        if prev_watched is False and (
            not session['live'] and prev_session['view_offset'] <= new_session['view_offset'] or 
            session['live'] and prev_session['guid'] == new_session['guid']
        ):
            if metadata:
                logger.debug("Tautulli ActivityProcessor :: Grouping history for sessionKey %s", session['session_key'])
            args = [prev_session['reference_id'], new_session['id']]

        else:
            if metadata:
                logger.debug("Tautulli ActivityProcessor :: Not grouping history for sessionKey %s", session['session_key'])
            args = [last_id, last_id]

        db.action(query=query, args=args)

    def get_sessions(self, user_id=None, ip_address=None):
        db = database.MonitorDatabase()
        query = "SELECT * FROM sessions"
        args = []

        if str(user_id).isdigit():
            ip = " GROUP BY ip_address" if ip_address else ""
            query += " WHERE user_id = ?" + ip
            args.append(user_id)

        sessions = db.select(query, args)
        return sessions

    def get_session_by_key(self, session_key=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            session = db.select_single("SELECT * FROM sessions "
                                            "WHERE session_key = ? ",
                                            args=[session_key])
            if session:
                return session

        return None

    def get_session_by_id(self, session_id=None):
        db = database.MonitorDatabase()
        if session_id:
            session = db.select_single("SELECT * FROM sessions "
                                            "WHERE session_id = ? ",
                                            args=[session_id])
            if session:
                return session

        return None

    def set_session_state(self, session_key=None, state=None, **kwargs):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            values = {}

            if state:
                values['state'] = state

            for k, v in kwargs.items():
                values[k] = v

            keys = {'session_key': session_key}
            result = db.upsert('sessions', values, keys)

            return result

        return None

    def delete_session(self, session_key=None, row_id=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            db.action("DELETE FROM sessions WHERE session_key = ?", [session_key])
        elif str(row_id).isdigit():
            db.action("DELETE FROM sessions WHERE id = ?", [row_id])

    def set_session_last_paused(self, session_key=None, timestamp=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            result = db.select("SELECT last_paused, paused_counter "
                                    "FROM sessions "
                                    "WHERE session_key = ?", args=[session_key])

            paused_counter = None
            for session in result:
                if session['last_paused']:
                    paused_offset = helpers.timestamp() - int(session['last_paused'])
                    if session['paused_counter']:
                        paused_counter = int(session['paused_counter']) + int(paused_offset)
                    else:
                        paused_counter = int(paused_offset)

            values = {'last_paused': timestamp}

            if paused_counter:
                values['paused_counter'] = paused_counter

            keys = {'session_key': session_key}
            db.upsert('sessions', values, keys)

    def increment_session_buffer_count(self, session_key=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            db.action("UPDATE sessions SET buffer_count = buffer_count + 1 "
                           "WHERE session_key = ?",
                           [session_key])

    def get_session_buffer_count(self, session_key=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            buffer_count = db.select_single("SELECT buffer_count "
                                                 "FROM sessions "
                                                 "WHERE session_key = ?",
                                                 [session_key])
            if buffer_count:
                return buffer_count['buffer_count']

            return 0

    def set_session_buffer_trigger_time(self, session_key=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            db.action("UPDATE sessions SET buffer_last_triggered = strftime('%s', 'now') "
                           "WHERE session_key = ?",
                           [session_key])

    def get_session_buffer_trigger_time(self, session_key=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            last_time = db.select_single("SELECT buffer_last_triggered "
                                              "FROM sessions "
                                              "WHERE session_key = ?",
                                              [session_key])
            if last_time:
                return last_time['buffer_last_triggered']

            return None

    def set_temp_stopped(self):
        db = database.MonitorDatabase()
        stopped_time = helpers.timestamp()
        db.action("UPDATE sessions SET stopped = ?", [stopped_time])

    def increment_write_attempts(self, session_key=None):
        db = database.MonitorDatabase()
        if str(session_key).isdigit():
            session = self.get_session_by_key(session_key=session_key)
            db.action("UPDATE sessions SET write_attempts = ? WHERE session_key = ?",
                           [session['write_attempts'] + 1, session_key])

    def set_marker(self, session_key=None, marker_idx=None, marker_type=None):
        db = database.MonitorDatabase()
        marker_args = [
            int(marker_type == 'intro'),
            int(marker_type == 'commercial'),
            int(marker_type == 'credits')
        ]
        db.action("UPDATE sessions SET intro = ?, commercial = ?, credits = ?, marker = ? "
                       "WHERE session_key = ?",
                       marker_args + [marker_idx, session_key])

    def set_watched(self, session_key=None):
        db = database.MonitorDatabase()
        db.action("UPDATE sessions SET watched = ? "
                       "WHERE session_key = ?",
                       [1, session_key])

    def write_continued_session(self, user_id=None, machine_id=None, media_type=None, stopped=None):
        db = database.MonitorDatabase()
        keys = {'user_id': user_id, 'machine_id': machine_id, 'media_type': media_type}
        values = {'stopped': stopped}
        db.upsert(table_name='sessions_continued', key_dict=keys, value_dict=values)

    def is_initial_stream(self, user_id=None, machine_id=None, media_type=None, started=None):
        db = database.MonitorDatabase()
        last_session = db.select_single("SELECT stopped "
                                             "FROM sessions_continued "
                                             "WHERE user_id = ? AND machine_id = ? AND media_type = ? "
                                             "ORDER BY stopped DESC",
                                             [user_id, machine_id, media_type])
        return int(started - last_session.get('stopped', 0) >= plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD)

    def regroup_history(self):
        logger.info("Tautulli ActivityProcessor :: Creating database backup...")
        if not database.make_backup():
            return False

        logger.info("Tautulli ActivityProcessor :: Regrouping session history...")

        db = database.MonitorDatabase()
        query = (
            "SELECT * FROM session_history "
            "JOIN session_history_metadata ON session_history.id = session_history_metadata.id"
        )
        results = db.select(query)
        count = len(results)
        progress = 0

        for i, session in enumerate(results, start=1):
            if int(i / count * 10) > progress:
                progress = int(i / count * 10)
                logger.info("Tautulli ActivityProcessor :: Regrouping session history: %d%%", progress * 10)

            try:
                self.group_history(session['id'], session)
            except Exception as e:
                logger.error("Tautulli ActivityProcessor :: Error regrouping session history: %s", e)
                return False

        logger.info("Tautulli ActivityProcessor :: Regrouping session history complete.")
        return True


def regroup_history():
    ActivityProcessor().regroup_history()
