# -*- coding: utf-8 -*-
#
# This file is part of SickGear.
#
# SickGear 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.
#
# SickGear 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 SickGear.  If not, see <http://www.gnu.org/licenses/>.

from time import sleep

import platform
import re

try:
    import urllib.request as urllib2
except ImportError:
    import urllib2

from sickgear import logger
from sickgear.helpers import get_url, try_int, parse_xml

from _23 import unquote, urlencode
from six import iteritems


class Plex(object):
    def __init__(self, settings=None):

        settings = settings or {}
        self._plex_host = settings.get('plex_host') or '127.0.0.1'
        self.plex_port = settings.get('plex_port') or '32400'

        self.username = settings.get('username', '')
        self.password = settings.get('password', '')
        self.token = settings.get('token', '')

        self.device_name = settings.get('device_name', '')
        self.client_id = settings.get('client_id') or '5369636B47656172'
        self.machine_client_identifier = ''

        self.default_home_users = settings.get('default_home_users', '')

        # Progress percentage to consider video as watched
        # if set to anything > 0, videos with watch progress greater than this will be considered watched
        self.default_progress_as_watched = settings.get('default_progress_as_watched', 0)

        # Sections to scan. If empty all sections will be looked at,
        # the section id should be used which is the number found be in the url on PlexWeb after /section/[ID]
        self.section_list = settings.get('section_list', [])

        # Sections to skip scanning, for use when Settings['section_list'] is not specified,
        # the same as section_list, the section id should be used
        self.ignore_sections = settings.get('ignore_sections', [])

        # Filter sections by paths that are in this array
        self.section_filter_path = settings.get('section_filter_path', [])

        # Results
        self.show_states = {}
        self.file_count = 0

        # Conf
        self.config_version = 2.0
        self.use_logger = False
        self.test = None
        self.home_user_tokens = {}

        if self.username and '' == self.token:
            self.token = self.get_token(self.username, self.password)

    @property
    def plex_host(self):

        host = self._plex_host
        if not host.startswith('http'):
            host = 'http://%s' % host
        return host

    @plex_host.setter
    def plex_host(self, value):

        self._plex_host = value

    def log(self, msg, debug=True):

        try:
            if self.use_logger:
                msg = 'Plex:: ' + msg
                if debug:
                    logger.debug(msg)
                else:
                    logger.log(msg)
            # else:
            #     print(msg.encode('ascii', 'replace').decode())
        except (BaseException, Exception):
            pass

    def get_token(self, user, passw):

        auth = ''
        try:
            auth = get_url('https://plex.tv/users/sign_in.json',
                           headers={'X-Plex-Device-Name': 'SickGear',
                                    'X-Plex-Platform': platform.system(), 'X-Plex-Device': platform.system(),
                                    'X-Plex-Platform-Version': platform.release(),
                                    'X-Plex-Provides': 'Python', 'X-Plex-Product': 'Python',
                                    'X-Plex-Client-Identifier': self.client_id,
                                    'X-Plex-Version': str(self.config_version),
                                    'X-Plex-Username': user
                                    },
                           parse_json=True,
                           failure_monitor=False,
                           post_data=urlencode({b'user[login]': user, b'user[password]': passw}).encode('utf-8')
                           )['user']['authentication_token']
        except TypeError:
            self.log('Error in response from plex.tv auth server')
        except IndexError:
            self.log('Error getting Plex Token')

        return auth

    def get_access_token(self, token):

        resources = self.get_url_x('https://plex.tv/api/resources?includeHttps=1', token=token)
        if None is resources:
            return ''

        devices = resources.findall('Device')
        for device in devices:
            if 1 == len(devices) \
                    or self.machine_client_identifier == device.get('clientIdentifier') \
                    or (self.device_name
                        and (self.device_name.lower() in device.get('name').lower()
                             or self.device_name.lower() in device.get('clientIdentifier').lower())):
                access_token = device.get('accessToken')
                if not access_token:
                    return ''
                return access_token

            connections = device.findall('Connection')
            for connection in connections:
                if self.plex_host == connection.get('address'):
                    access_token = device.get('accessToken')
                    if not access_token:
                        return ''
                    uri = connection.get('uri')
                    match = re.compile(r'(http[s]?://.*?):(\d*)').match(uri)
                    if match:
                        self.plex_host = match.group(1)
                        self.plex_port = match.group(2)
                    return access_token
        return ''

    def get_plex_home_user_tokens(self):

        user_tokens = {}

        # check Plex is contactable
        home_users = self.get_url_x('https://plex.tv/api/home/users')
        if None is not home_users:
            for user in home_users.findall('User'):
                user_id = user.get('id')
                switch_page = self.get_url_x('https://plex.tv/api/home/users/%s/switch' % user_id, post_data=True)
                if None is not switch_page:
                    home_token = 'user' == switch_page.tag and switch_page.get('authenticationToken')
                    if home_token:
                        username = switch_page.get('title')
                        user_tokens[username] = self.get_access_token(home_token)
        return user_tokens

    def get_url_x(self, url, token=None, **kwargs):

        if not token:
            token = self.token
        if not url.startswith('http'):
            url = 'http://' + url

        for x in range(0, 3):
            if 0 < x:
                sleep(0.5)
            try:
                headers = {'X-Plex-Device-Name': 'SickGear',
                           'X-Plex-Platform': platform.system(), 'X-Plex-Device': platform.system(),
                           'X-Plex-Platform-Version': platform.release(),
                           'X-Plex-Provides': 'controller', 'X-Plex-Product': 'Python',
                           'X-Plex-Client-Identifier': self.client_id,
                           'X-Plex-Version': str(self.config_version),
                           'X-Plex-Token': token,
                           'Accept': 'application/xml'
                           }
                if self.username:
                    headers.update({'X-Plex-Username': self.username})
                page = get_url(url, headers=headers, failure_monitor=False, **kwargs)
                if page:
                    parsed = parse_xml(page)
                    if None is not parsed and len(parsed):
                        return parsed
                    return None

            except Exception as e:
                self.log('Error requesting page: %s' % e)
                continue
        return None

    # uses the Plex API to delete files instead of system functions, useful for remote installations
    def delete_file(self, media_id=0):

        try:
            endpoint = ('/library/metadata/%s' % str(media_id))
            req = urllib2.Request('%s:%s%s' % (self.plex_host, self.plex_port, endpoint),
                                  None, {'X-Plex-Token': self.token})
            req.get_method = lambda: 'DELETE'
            urllib2.urlopen(req)
        except (BaseException, Exception):
            return False
        return True

    @staticmethod
    def get_media_info(video_node):

        progress = 0
        if None is not video_node.get('viewOffset') and None is not video_node.get('duration'):
            progress = try_int(video_node.get('viewOffset')) * 100 / try_int(video_node.get('duration'))

        for media in video_node.findall('Media'):
            for part in media.findall('Part'):
                file_name = part.get('file')
                # if '3' > sys.version:  # remove HTML quoted characters, only works in python < 3
                #     file_name = urllib2.unquote(file_name.encode('utf-8', errors='replace'))
                # else:
                file_name = unquote(file_name)

                return {'path_file': file_name, 'media_id': video_node.get('ratingKey'),
                        'played': int(video_node.get('viewCount') or 0), 'progress': progress}

    def check_users_watched(self, users, media_id):

        if not self.home_user_tokens:
            self.home_user_tokens = self.get_plex_home_user_tokens()

        result = {}
        if 'all' in users:
            users = self.home_user_tokens.keys()

        for user in users:
            user_media_page = self.get_url_pms('/library/metadata/%s' % media_id, token=self.home_user_tokens[user])
            if None is not user_media_page:
                video_node = user_media_page.find('Video')

                progress = 0
                if None is not video_node.get('viewOffset') and None is not video_node.get('duration'):
                    progress = try_int(video_node.get('viewOffset')) * 100 / try_int(video_node.get('duration'))

                played = int(video_node.get('viewCount') or 0)
                if not progress and not played:
                    continue

                date_watched = 0
                if (0 < try_int(video_node.get('viewCount'))) or (0 < self.default_progress_as_watched < progress):
                    last_viewed_at = video_node.get('lastViewedAt')
                    if last_viewed_at and last_viewed_at not in ('', '0'):
                        date_watched = last_viewed_at

                if date_watched:
                    result[user] = dict(played=played, progress=progress, date_watched=date_watched)
            else:
                self.log('Do not have the token for %s.' % user)

        return result

    def get_url_pms(self, endpoint=None, **kwargs):

        return endpoint and self.get_url_x(
            '%s:%s%s' % (self.plex_host, self.plex_port, endpoint), **kwargs)

    # parse episode information from season pages
    def stat_show(self, node):

        ep_nodes = []
        if 'directory' == node.tag.lower() and 'show' == node.get('type'):
            show = self.get_url_pms(node.get('key'))
            if None is show:  # Check if show page is None or empty
                self.log('Failed to load show page. Skipping...')
                return None

            for season_node in show.findall('Directory'):  # Each directory is a season
                if 'season' != season_node.get('type'):  # skips Specials
                    continue

                season_node_key = season_node.get('key')
                season_node = self.get_url_pms(season_node_key)
                if None is not season_node:
                    ep_nodes += [season_node]

        elif 'mediacontainer' == node.tag.lower() and 'episode' == node.get('viewGroup'):
            ep_nodes = [node]

        check_users = []
        if self.default_home_users:
            check_users = self.default_home_users.strip(' ,').lower().split(',')
            for k in range(0, len(check_users)):  # Remove extra spaces and commas
                check_users[k] = check_users[k].strip(', ')

        for episode_node in ep_nodes:
            for video_node in episode_node.findall('Video'):

                media_info = self.get_media_info(video_node)

                if check_users:
                    user_info = self.check_users_watched(check_users, media_info['media_id'])
                    for user_name, user_media_info in user_info.items():
                        self.show_states.update({len(self.show_states): dict(
                            path_file=media_info['path_file'],
                            media_id=media_info['media_id'],
                            played=(100 * user_media_info['played']) or user_media_info['progress'] or 0,
                            label=user_name,
                            date_watched=user_media_info['date_watched'])})
                else:
                    self.show_states.update({len(self.show_states): dict(
                        path_file=media_info['path_file'],
                        media_id=media_info['media_id'],
                        played=(100 * media_info['played']) or media_info['progress'] or 0,
                        label=self.username,
                        date_watched=video_node.get('lastViewedAt'))})

                self.file_count += 1

        return True

    def fetch_show_states(self, fetch_all=False):

        error_log = []
        self.show_states = {}

        server_check = self.get_url_pms('/')
        if None is server_check or 'MediaContainer' != server_check.tag:
            error_log.append('Cannot reach server!')

        else:
            if not self.device_name:
                self.device_name = server_check.get('friendlyName')

            if not self.machine_client_identifier:
                self.machine_client_identifier = server_check.get('machineIdentifier')

            access_token = None
            if self.token:
                access_token = self.get_access_token(self.token)
                if access_token:
                    self.token = access_token
                    if not self.home_user_tokens:
                        self.home_user_tokens = self.get_plex_home_user_tokens()
                else:
                    error_log.append('Access Token not found')

            resp_sections = None
            if None is access_token or len(access_token):
                resp_sections = self.get_url_pms('/library/sections/')

            if None is not resp_sections:

                unpather = []
                for loc in self.section_filter_path:
                    loc = re.sub(r'[/\\]+', '/', loc.lower())
                    loc = re.sub(r'^(.{,2})[/\\]', '', loc)
                    unpather.append(loc)
                self.section_filter_path = unpather

                for section in resp_sections.findall('Directory'):
                    if 'show' != section.get('type') or not section.findall('Location'):
                        continue

                    section_path = re.sub(r'[/\\]+', '/', section.find('Location').get('path').lower())
                    section_path = re.sub(r'^(.{,2})[/\\]', '', section_path)
                    if not any(section_path in path for path in self.section_filter_path):
                        continue

                    if section.get('key') not in self.ignore_sections \
                            and section.get('title') not in self.ignore_sections:
                        section_key = section.get('key')

                        for (user, token) in iteritems(self.home_user_tokens or {'': None}):
                            self.username = user

                            resp_section = self.get_url_pms('/library/sections/%s/%s' % (
                                section_key, ('recentlyViewed', 'all')[fetch_all]), token=token)
                            if None is not resp_section:
                                view_group = 'MediaContainer' == resp_section.tag and \
                                             resp_section.get('viewGroup') or ''
                                if 'show' == view_group and fetch_all:
                                    for DirectoryNode in resp_section.findall('Directory'):
                                        self.stat_show(DirectoryNode)
                                elif 'episode' == view_group and not fetch_all:
                                    self.stat_show(resp_section)

        if 0 < len(error_log):
            self.log('Library errors...')
            for item in error_log:
                self.log(item)

        return 0 < len(error_log)