#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Sync the watch status from one Plex or Tautulli user to other users across any owned server.

Author: Blacktwin
Requires: requests, plexapi, argparse

Enabling Scripts in Tautulli:
Taultulli > Settings > Notification Agents > Add a Notification Agent > Script

Configuration:
Taultulli > Settings > Notification Agents > New Script > Configuration:

 Script Name: sync_watch_status.py
 Set Script Timeout: default
 Description: Sync watch status
 Save

Triggers:
Taultulli > Settings > Notification Agents > New Script > Triggers:

 Check: Notify on Watched
 Save

Conditions:
Taultulli > Settings > Notification Agents > New Script > Conditions:

 Set Conditions: [{username} | {is} | {user_to_sync_from} ]
 Save

Script Arguments:
Taultulli > Settings > Notification Agents > New Script > Script Arguments:

 Select: Notify on Watched
 Arguments: --ratingKey {rating_key} --userFrom Tautulli=Tautulli --userTo "Username2=Server1" "Username3=Server1"

 Save
 Close

 Example:
    Set in Tautulli in script notification agent (above) or run manually (below)

    sync_watch_status.py --userFrom USER1=Server1 --userTo USER2=Server1 --libraries Movies
       - Synced watch status from Server1 {title from library} to {USER2}'s account on Server1.

    sync_watch_status.py --userFrom USER1=Server2 --userTo USER2=Server1 USER3=Server1 --libraries Movies "TV Shows"
       - Synced watch status from Server2 {title from library} to {USER2 or USER3}'s account on Server1.

    sync_watch_status.py --userFrom USER1=Tautulli --userTo USER2=Server1 USER3=Server2 --libraries Movies "TV Shows"
       - Synced watch statuses from Tautulli {title from library} to {USER2 or USER3}'s account on selected servers.

    sync_watch_status.py --userFrom USER1=Tautulli --userTo USER2=Server1 USER3=Server2 --ratingKey  1234
       - Synced watch statuse of rating key 1234 from USER1's Tautulli history to {USER2 or USER3}'s account
       on selected servers.
       **Rating key must be a movie or episode. Shows and Seasons not support.... yet.
"""
from __future__ import print_function
from __future__ import unicode_literals
from builtins import object
import argparse
from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer
from plexapi.server import CONFIG
from requests import Session
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException

# Using CONFIG file
PLEX_TOKEN = ''
TAUTULLI_URL = ''
TAUTULLI_APIKEY = ''

if not PLEX_TOKEN:
    PLEX_TOKEN = CONFIG.data['auth'].get('server_token')
if not TAUTULLI_URL:
    TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl')
if not TAUTULLI_APIKEY:
    TAUTULLI_APIKEY = CONFIG.data['auth'].get('tautulli_apikey')

VERIFY_SSL = False


class Connection(object):
    def __init__(self, url=None, apikey=None, verify_ssl=False):
        self.url = url
        self.apikey = apikey
        self.session = Session()
        self.adapters = HTTPAdapter(max_retries=3,
                                    pool_connections=1,
                                    pool_maxsize=1,
                                    pool_block=True)
        self.session.mount('http://', self.adapters)
        self.session.mount('https://', self.adapters)

        # Ignore verifying the SSL certificate
        if verify_ssl is False:
            self.session.verify = False
            # Disable the warning that the request is insecure, we know that...
            import urllib3
            urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class Library(object):
    def __init__(self, data=None):
        d = data or {}
        self.title = d['section_name']
        self.key = d['section_id']


class Metadata(object):
    def __init__(self, data=None):
        d = data or {}
        self.type = d['media_type']
        self.grandparentTitle = d['grandparent_title']
        self.parentIndex = d['parent_media_index']
        self.index = d['media_index']
        if self.type == 'episode':
            ep_name = d['full_title'].partition('-')[-1]
            self.title = ep_name.lstrip()
        else:
            self.title = d['full_title']

        # For History
        try:
            if d['watched_status']:
                self.watched_status = d['watched_status']
        except KeyError:
            pass
        # For Metadata
        try:
            if d["library_name"]:
                self.libraryName = d['library_name']
        except KeyError:
            pass


class Tautulli(object):
    def __init__(self, connection):
        self.connection = connection

    def _call_api(self, cmd, payload, method='GET'):
        payload['cmd'] = cmd
        payload['apikey'] = self.connection.apikey

        try:
            response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload)
        except RequestException as e:
            print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
            return

        try:
            response_json = response.json()
        except ValueError:
            print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd))
            return

        if response_json['response']['result'] == 'success':
            return response_json['response']['data']
        else:
            error_msg = response_json['response']['message']
            print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
            return

    def get_watched_history(self, user=None, section_id=None, rating_key=None, start=None, length=None):
        """Call Tautulli's get_history api endpoint."""
        payload = {"order_column": "full_title",
                   "order_dir": "asc"}
        if user:
            payload["user"] = user
        if section_id:
            payload["section_id"] = section_id
        if rating_key:
            payload["rating_key"] = rating_key
        if start:
            payload["start"] = start
        if length:
            payload["lengh"] = length

        history = self._call_api('get_history', payload)

        return [d for d in history['data'] if d['watched_status'] == 1]

    def get_metadata(self, rating_key):
        """Call Tautulli's get_metadata api endpoint."""
        payload = {"rating_key": rating_key}
        return self._call_api('get_metadata', payload)

    def get_libraries(self):
        """Call Tautulli's get_libraries api endpoint."""
        payload = {}
        return self._call_api('get_libraries', payload)


class Plex(object):
    def __init__(self, token, url=None):
        if token and not url:
            self.account = MyPlexAccount(token)
        if token and url:
            session = Connection().session
            self.server = PlexServer(baseurl=url, token=token, session=session)

    def admin_servers(self):
        """Get all owned servers.

        Returns
        -------
        data: dict

        """
        resources = {}
        for resource in self.account.resources():
            if 'server' in [resource.provides] and resource.owned is True:
                resources[resource.name] = resource

        return resources

    def all_users(self):
        """Get all users.

        Returns
        -------
        data: dict

        """
        users = {self.account.title: self.account}
        for user in self.account.users():
            users[user.title] = user

        return users

    def all_sections(self):
        """Get all sections from all owned servers.

        Returns
        -------
        data: dict

        """
        data = {}
        servers = self.admin_servers()
        print("Connecting to admin server(s) for access info...")
        for name, server in servers.items():
            connect = server.connect()
            sections = {section.title: section for section in connect.library.sections()}
            data[name] = sections

        return data

    def users_access(self):
        """Get users access across all owned servers.

        Returns
        -------
        data: dict

        """
        all_users = self.all_users().values()
        admin_servers = self.admin_servers()
        all_sections = self.all_sections()

        data = {self.account.title: {"account": self.account}}

        for user in all_users:
            if not data.get(user.title):
                servers = []
                for server in user.servers:
                    if admin_servers.get(server.name):
                        access = {}
                        sections = {section.title: section for section in server.sections()
                                    if section.shared is True}
                        access['server'] = {server.name: admin_servers.get(server.name)}
                        access['sections'] = sections
                        servers += [access]
                        data[user.title] = {'account': user,
                                            'access': servers}
            else:
                # Admin account
                servers = []
                for name, server in admin_servers.items():
                    access = {}
                    sections = all_sections.get(name)
                    access['server'] = {name: server}
                    access['sections'] = sections
                    servers += [access]
                    data[user.title] = {'account': user,
                                        'access': servers}
        return data


def connect_to_server(server_obj, user_account):
    """Find server url and connect using user token.

    Parameters
    ----------
    server_obj: class
    user_account: class

    Returns
    -------
    user_connection.server: class

    """
    server_name = server_obj.name
    user = user_account.title

    print('Connecting {} to {}...'.format(user, server_name))
    server_connection = server_obj.connect()
    baseurl = server_connection._baseurl.split('.')
    url = "".join([baseurl[0].replace('-', '.'),
                   baseurl[-1].replace('direct', '')])
    if user_account.title == Plex(PLEX_TOKEN).account.title:
        token = PLEX_TOKEN
    else:
        token = user_account.get_token(server_connection.machineIdentifier)

    user_connection = Plex(url=url, token=token)

    return user_connection.server


def check_users_access(access, user, server_name, libraries=None):
    """Check user's access to server. If allowed connect.

    Parameters
    ----------
    access: dict
    user: dict
    server_name: str
    libraries: list

    Returns
    -------
    server_connection: class

    """
    try:
        _user = access.get(user)
        for access in _user['access']:
            server = access.get("server")
            # Check user access to server
            if server.get(server_name):
                server_obj = server.get(server_name)
                # If syncing by libraries, check library access
                if libraries:
                    library_check = any(lib.title in access.get("sections").keys() for lib in libraries)
                    # Check user access to library
                    if library_check:
                        server_connection = connect_to_server(server_obj, _user['account'])
                        return server_connection

                    elif not library_check:
                        print("User does not have access to this library.")
                # Not syncing by libraries
                else:
                    server_connection = connect_to_server(server_obj, _user['account'])
                    return server_connection
            # else:
            #     print("User does not have access to this server: {}.".format(server_name))
    except KeyError:
        print('User name is incorrect.')
        print(", ".join(plex_admin.all_users().keys()))
        exit()


def sync_watch_status(watched, section, accountTo, userTo, same_server=False):
    """Sync watched status between two users.

    Parameters
    ----------
    watched: list
        List of watched items either from Tautulli or Plex
    section: str
        Section title of sync from server
    accountTo: class
        User's account that will be synced to
    userTo: str
        User's server class of sync to user
    same_server: bool
        Are serverFrom and serverTo the same

    """
    print('Marking watched...')
    sectionTo = accountTo.library.section(section)
    for item in watched:
        try:
            if same_server:
                fetch_check = sectionTo.fetchItem(item.ratingKey)
            else:
                if item.type == 'episode':
                    show_name = item.grandparentTitle
                    show = sectionTo.get(show_name)
                    watch_check = show.episode(season=int(item.parentIndex), episode=int(item.index))
                else:
                    title = item.title
                    watch_check = sectionTo.get(title)
                # .get retrieves a partial object
                # .fetchItem retrieves a full object
                fetch_check = sectionTo.fetchItem(watch_check.key)
            # If item is already watched ignore
            if not fetch_check.isWatched:
                # todo-me should watched count be synced?
                fetch_check.markWatched()
                title = fetch_check._prettyfilename()
                print("Synced watched status of {} to account {}...".format(title, userTo))

        except Exception as e:
            print(e)
            pass


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Sync watch status from one user to others.",
                                     formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('--libraries', nargs='*', metavar='library',
                        help='Libraries to scan for watched content.')
    parser.add_argument('--ratingKey', nargs="?", type=str,
                        help='Rating key of item whose watch status is to be synced.')
    requiredNamed = parser.add_argument_group('required named arguments')
    requiredNamed.add_argument('--userFrom', metavar='user=server', required=True,
                               type=lambda kv: kv.split("="), default=["", ""],
                               help='Select user and server to sync from')
    requiredNamed.add_argument('--userTo', nargs='*', metavar='user=server', required=True,
                               type=lambda kv: kv.split("="),
                               help='Select user and server to sync to.')

    opts = parser.parse_args()
    # print(opts)
    tautulli_server = ''

    libraries = []
    all_sections = {}
    watchedFrom = ''
    same_server = False
    count = 25
    start = 0
    plex_admin = Plex(PLEX_TOKEN)
    plex_access = plex_admin.users_access()

    userFrom, serverFrom = opts.userFrom

    if serverFrom == "Tautulli":
        # Create a Tautulli instance
        tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip('/'),
                                              apikey=TAUTULLI_APIKEY,
                                              verify_ssl=VERIFY_SSL))

    if serverFrom == "Tautulli" and opts.libraries:
        # Pull all libraries from Tautulli
        _sections = {}
        tautulli_sections = tautulli_server.get_libraries()
        for section in tautulli_sections:
            section_obj = Library(section)
            _sections[section_obj.title] = section_obj
        all_sections[serverFrom] = _sections
    elif serverFrom != "Tautulli" and opts.libraries:
        # Pull all libraries from admin access dict
        admin_access = plex_access.get(plex_admin.account.title).get("access")
        for server in admin_access:
            if server.get("server").get(serverFrom):
                all_sections[serverFrom] = server.get("sections")

    # Defining libraries
    if opts.libraries:
        for library in opts.libraries:
            if all_sections.get(serverFrom).get(library):
                libraries.append(all_sections.get(serverFrom).get(library))
            else:
                print("No matching library name '{}'".format(library))
                exit()

    # If server is Plex and synciing libraries, check access
    if serverFrom != "Tautulli" and libraries:
        print("Checking {}'s access to {}".format(userFrom, serverFrom))
        watchedFrom = check_users_access(plex_access, userFrom, serverFrom, libraries)

    if libraries:
        print("Finding watched items in libraries...")
        plexTo = []

        for user, server_name in opts.userTo:
            plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])

        for _library in libraries:
            watched_lst = []
            print("Checking {}'s library: '{}' watch statuses...".format(userFrom, _library.title))
            if tautulli_server:
                while True:
                    # Getting all watched history for userFrom
                    tt_watched = tautulli_server.get_watched_history(user=userFrom, section_id=_library.key,
                                                                     start=start, length=count)
                    if all([tt_watched]):
                        start += count
                        for item in tt_watched:
                            watched_lst.append(Metadata(item))
                        continue
                    elif not all([tt_watched]):
                        break
                    start += count
            else:
                # Check library for watched items
                sectionFrom = watchedFrom.library.section(_library.title)
                if _library.type == 'show':
                    watched_lst = sectionFrom.search(libtype='episode', unwatched=False)
                else:
                    watched_lst = sectionFrom.search(unwatched=False)

            for user in plexTo:
                username, server = user
                if server == serverFrom:
                    same_server = True
                sync_watch_status(watched_lst, _library.title, server, username, same_server)

    elif opts.ratingKey and serverFrom == "Tautulli":
        plexTo = []
        watched_item = []

        if userFrom != "Tautulli":
            print("Request manually triggered to update watch status")
            tt_watched = tautulli_server.get_watched_history(user=userFrom, rating_key=opts.ratingKey)
            if tt_watched:
                watched_item = Metadata(tautulli_server.get_metadata(opts.ratingKey))
            else:
                print("Rating Key {} was not reported as watched in Tautulli for user {}".format(opts.ratingKey, userFrom))
                exit()

        elif userFrom == "Tautulli":
            print("Request from Tautulli notification agent to update watch status")
            watched_item = Metadata(tautulli_server.get_metadata(opts.ratingKey))

        for user, server_name in opts.userTo:
            # Check access and connect
            plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])

        for user in plexTo:
            username, server = user
            sync_watch_status([watched_item], watched_item.libraryName, server, username)

    elif opts.ratingKey and serverFrom != "Tautulli":
        plexTo = []
        watched_item = []
    
        if userFrom != "Tautulli":
            print("Request manually triggered to update watch status")
            watchedFrom = check_users_access(plex_access, userFrom, serverFrom)
            watched_item = watchedFrom.fetchItem(int(opts.ratingKey))
            if not watched_item.isWatched:
                print("Rating Key {} was not reported as watched in Plex for user {}".format(opts.ratingKey,
                                                                                             userFrom))
                exit()
        else:
            print("Use an actual user.")
            exit()
    
        for user, server_name in opts.userTo:
            # Check access and connect
            plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])
    
        for user in plexTo:
            username, server = user
            library = server.library.sectionByID(watched_item.librarySectionID)
            sync_watch_status([watched_item], library.title, server, username)

    else:
        print("You aren't using this script correctly... bye!")