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

"""
Media list provider using Kitsu <https://kitsu.app>.

Example API response: https://kitsu.app/api/edge/anime?filter[text]=build+divide
"""

import datetime
from enum import Enum
import gzip
import json
import socket
import time
import urllib.error
import urllib.parse
import urllib.request

from trackma import utils
from trackma.lib.lib import lib


class PosterImageKey(str, Enum):
    TINY = 'tiny'
    LARGE = 'large'
    SMALL = 'small'
    MEDIUM = 'medium'
    ORIGINAL = 'original'


class libkitsu(lib):
    """
    API class to communicate with Kitsu
    Should inherit a base library interface.

    Website: https://kitsu.app/
    API documentation:
    Designed by:

    """
    name = 'libkitsu'
    user_agent = 'Trackma/{}'.format(utils.VERSION)

    auth = ''
    logged_in = False

    api_info = {
        'name': 'Kitsu',
        'shortname': 'kitsu',
        'version': 'v0.3',
        'merge': True
    }

    default_mediatype = 'anime'
    default_statuses = ['current', 'completed',
                        'on_hold', 'dropped', 'planned']
    default_statuses_dict = {
        'current': 'Watching',
        'completed': 'Completed',
        'on_hold': 'On Hold',
        'dropped': 'Dropped',
        'planned': 'Plan to Watch'
    }

    mediatypes = dict()
    mediatypes['anime'] = {
        'has_progress': True,
        'can_add': True,
        'can_delete': True,
        'can_score': True,
        'can_status': True,
        'can_update': True,
        'can_play': True,
        'statuses_start': ['current'],
        'statuses_finish': ['completed'],
        'statuses_library': ['current', 'on_hold', 'planned'],
        'statuses': default_statuses,
        'statuses_dict': default_statuses_dict,
        'score_max': 5,
        'score_step': 0.25,
    }
    mediatypes['manga'] = {
        'has_progress': True,
        'can_add': True,
        'can_delete': True,
        'can_score': True,
        'can_status': True,
        'can_update': True,
        'can_play': False,
        'statuses_start': ['current'],
        'statuses_finish': ['completed'],
        'statuses': default_statuses,
        'statuses_dict': {
            'current': 'Reading',
            'completed': 'Completed',
            'on_hold': 'On Hold',
            'dropped': 'Dropped',
            'planned': 'Plan to Read'
        },
        'score_max': 5,
        'score_step': 0.25,
    }
    mediatypes['drama'] = {
        'has_progress': True,
        'can_add': True,
        'can_delete': True,
        'can_score': True,
        'can_status': True,
        'can_update': True,
        'can_play': True,
        'statuses_start': ['current'],
        'statuses_finish': ['completed'],
        'statuses': default_statuses,
        'statuses_dict': default_statuses_dict,
        'score_max': 5,
        'score_step': 0.25,
    }

    url = 'https://kitsu.app/api'
    prefix = 'https://kitsu.app/api/edge'

    # TODO : These values are provisional.
    _client_id = 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd'
    _client_secret = '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151'

    type_translate = {'ONA': utils.Type.ONA,
                      'OVA': utils.Type.OVA,
                      'TV': utils.Type.TV,
                      'movie': utils.Type.MOVIE,
                      'music': utils.Type.MUSIC,
                      'special': utils.Type.SP}

    status_translate = {'Currently Airing': utils.Status.AIRING,
                        'Finished Airing': utils.Status.FINISHED,
                        'Not Yet Aired': utils.Status.NOTYET}

    def __init__(self, messenger, account, userconfig):
        """Initializes the useragent through credentials."""
        super(libkitsu, self).__init__(messenger, account, userconfig)

        self.username = account['username']
        self.password = account['password']

        # Build opener with the mashape API key
        self.opener = urllib.request.build_opener()
        self.opener.addheaders = [
            ('User-Agent',      self.user_agent),
            ('Accept',          'application/vnd.api+json'),
            ('Accept-Encoding', 'gzip'),
            ('Accept-Charset',  'utf-8'),
        ]

    def _request(self, method, url, get=None, post=None, body=None, auth=False):
        content_type = None

        if get:
            url += "?%s" % urllib.parse.urlencode(get)
        if post:
            post = urllib.parse.urlencode(post).encode('utf-8')
            content_type = 'application/x-www-form-urlencoded'
        if body:
            post = body.encode('utf-8')
            content_type = 'application/vnd.api+json'

        request = urllib.request.Request(url, post)
        request.get_method = lambda: method

        if content_type:
            request.add_header('Content-Type', content_type)

        if auth:
            request.add_header('Authorization', '{0} {1}'.format(
                self._get_userconfig('token_type').capitalize(),
                self._get_userconfig('access_token'),
            ))

        try:
            response = self.opener.open(request)

            # The response most probably will be gzipped so we
            # have to take care of that first
            if response.info().get('content-encoding') == 'gzip':
                return gzip.GzipFile(fileobj=response).read().decode('utf-8')
            else:
                return response.read().decode('utf-8')
        except urllib.error.HTTPError as e:
            if e.code == 401:
                raise utils.APIError("Incorrect credentials.")
            else:
                api_error = self._parse_errors(e)
                if api_error:
                    raise utils.APIError("API error: %s" % api_error)
                else:
                    raise utils.APIError("Connection error: %s" % e)
        except urllib.error.URLError as e:
            raise utils.APIError("URL error: %s" % e)
        except socket.timeout:
            raise utils.APIError("Operation timed out.")

    def _parse_errors(self, e):
        try:
            data = json.loads(e.read().decode('utf-8'))
            errors = ""
            for error in data['errors']:
                errors += "{}: {}".format(error['code'], error['detail'])

            return errors
        except Exception:
            return None

    def _request_access_token(self, refresh=False):
        params = {
            'client_id':     self._client_id,
            'client_secret': self._client_secret,
        }

        if refresh:
            self.msg.info('Refreshing access token...')

            params['grant_type'] = 'refresh_token'
            params['refresh_token'] = self._get_userconfig('refresh_token')
        else:
            self.msg.info('Requesting access token...')

            params['grant_type'] = 'password'
            params['username'] = self.username
            params['password'] = self.password

        response = self._request(
            'POST', self.url + '/oauth/token', post=params)
        data = json.loads(response)

        timestamp = int(time.time())

        self._set_userconfig('access_token',  data['access_token'])
        self._set_userconfig('token_type',    data['token_type'])
        self._set_userconfig('expires',       timestamp + data['expires_in'])
        self._set_userconfig('refresh_token', data['refresh_token'])

        self.logged_in = True
        self._refresh_user_info()
        self._emit_signal('userconfig_changed')

    def _refresh_user_info(self):
        self.msg.info('Refreshing user details...')
        params = {
            "filter[self]": 'true',
        }
        data = self._request('GET', self.prefix +
                             "/users", get=params, auth=True)
        json_data = json.loads(data)
        user = json_data['data'][0]

        # Parse user information
        self._set_userconfig('userid', user['id'])
        self._set_userconfig('username', user['attributes']['name'])

    def check_credentials(self):
        """
        Log into Kitsu. If there isn't an access token, request it, or
        refresh it if necessary.
        """
        timestamp = int(time.time())

        if not self._get_userconfig('access_token'):
            self._request_access_token(False)
        elif (timestamp+60) > self._get_userconfig('expires'):
            self._request_access_token(True)
        else:
            self.logged_in = True

        return True

    def fetch_list(self):
        """Queries the full list from the remote server.
        Returns the list if successful, False otherwise."""
        self.check_credentials()
        self.msg.info('Downloading list...')

        try:
            showlist = dict()
            infolist = list()

            # Get first page and continue from there
            params = {
                "filter[user_id]": self._get_userconfig('userid'),
                "filter[kind]": self.mediatype,
                # "include": self.mediatype, # TODO : This returns a 500 for some reason.
                "include": "media",
                # TODO : List for manga should be different
                f"fields[{self.mediatype}]": ','.join([
                    'id',
                    'slug',
                    'canonicalTitle',
                    'titles',
                    'episodeCount' if self.mediatype in [
                        'anime', 'drama'] else 'chapterCount',
                    'description',
                    'status',
                    'tba',
                    'subtype',
                    'posterImage',
                    'startDate',
                    'endDate',
                    'abbreviatedTitles',
                    'averageRating',
                    'popularityRank',
                    'ratingRank',
                    'ageRating',
                    'ageRatingGuide',
                    'userCount',
                    'favoritesCount'
                ]),
                "page[limit]": "250",
            }

            if self.mediatype == 'anime':
                params['fields[anime]'] += ',nsfw'

            if self.mediatype == 'manga':
                params['fields[manga]'] += ',serialization'

            url = "{}/library-entries?{}".format(
                self.prefix, urllib.parse.urlencode(params))
            i = 1

            while url:
                self.msg.info('Getting page {}...'.format(i))

                data = self._request('GET', url)
                data_json = json.loads(data)

                # print(json.dumps(data_json, sort_keys=True, indent=2))
                # return []

                entries = data_json['data']
                links = data_json['links']

                for entry in entries:
                    # TODO : Including the mediatype returns a 500 for some reason.
                    # showid = int(entry['relationships'][self.mediatype]['data']['id'])
                    showid = int(entry['relationships']['media']['data']['id'])
                    status = entry['attributes']['status']
                    rating = entry['attributes']['ratingTwenty']

                    showlist[showid] = utils.show()
                    showlist[showid].update({
                        'id': showid,
                        'my_id': entry['id'],
                        'my_progress': entry['attributes']['progress'],
                        'my_score': float(rating)/4.00 if rating is not None else 0.0,
                        'my_status': entry['attributes']['status'],
                        'my_start_date': self._iso2date(entry['attributes']['startedAt']),
                        'my_finish_date': self._iso2date(entry['attributes']['finishedAt']),
                        'my_last_update': self._iso2datetime(entry['attributes']['updatedAt']),
                    })

                if 'included' in data_json:
                    medias = data_json['included']
                    for media in medias:
                        info = self._parse_info(media)
                        infolist.append(info)

                    self._emit_signal('show_info_changed', infolist)

                url = links.get('next')
                i += 1

            return showlist
        except urllib.error.HTTPError as e:
            raise utils.APIError(
                "Error getting list (HTTPError): %s" % e.read())
        except urllib.error.URLError as e:
            raise utils.APIError(
                "Error getting list (URLError): %s" % e.reason)

    def merge(self, show, info):
        show['title'] = info['title']
        show['aliases'] = info['aliases']
        show['url'] = info['url']
        show['total'] = info['total']
        show['image'] = info['image']

        show['image_thumb'] = info['image_thumb']

        show['start_date'] = info['start_date']
        show['end_date'] = info['end_date']
        show['status'] = info['status']

    def request_info(self, item_list):
        print("These are missing: " + repr(item_list))
        # TODO implement
        raise NotImplementedError

    def add_show(self, item):
        """Adds a new show in the server"""
        self.check_credentials()
        self.msg.info("Adding show %s..." % item['title'])

        data = self._build_data(item)

        try:
            data = self._request('POST', self.prefix +
                                 "/library-entries", body=data, auth=True)

            data_json = json.loads(data)
            return int(data_json['data']['id'])
        except urllib.error.HTTPError as e:
            raise utils.APIError('Error adding: ' + str(e.code))
        except urllib.error.URLError as e:
            raise utils.APIError('Error adding: ' + str(e.reason))

    def update_show(self, item):
        """Sends a show update to the server"""
        self.check_credentials()
        self.msg.info("Updating show %s..." % item['title'])

        data = self._build_data(item)

        try:
            data = self._request('PATCH', self.prefix + "/library-entries/%s" %
                          item['my_id'], body=data, auth=True)
            data_json = json.loads(data)
            return self._iso2datetime(data_json['data']['attributes']['updatedAt'])
        except urllib.error.HTTPError as e:
            raise utils.APIError('Error updating: ' + str(e.code))
        except urllib.error.URLError as e:
            raise utils.APIError('Error updating: ' + str(e.reason))

    def delete_show(self, item):
        """Sends a show delete to the server"""
        self.check_credentials()
        self.msg.info("Deleting show %s..." % item['title'])

        try:
            self._request('DELETE', self.prefix +
                          "/library-entries/%s" % item['my_id'], auth=True)
        except urllib.error.HTTPError as e:
            raise utils.APIError('Error deleting: ' + str(e.code))
        except urllib.error.URLError as e:
            raise utils.APIError('Error deleting: ' + str(e.reason))

    def search(self, query, method):
        self.msg.info("Searching for %s..." % query)

        values = {
            "filter[text]": query,
            "page[limit]": 20,
        }

        try:
            data = self._request('GET', self.prefix +
                                 "/" + self.mediatype, get=values)
            shows = json.loads(data)

            infolist = []
            for media in shows['data']:
                info = self._parse_info(media)
                infolist.append(info)

            self._emit_signal('show_info_changed', infolist)

            if not infolist:
                raise utils.APIError('No results.')

            return infolist
        except urllib.error.HTTPError as e:
            raise utils.APIError('Error searching: ' + str(e.code))
        except urllib.error.URLError as e:
            raise utils.APIError('Error searching: ' + str(e.reason))

    def _build_data(self, item):
        values = {'data': {
            'type': 'libraryEntries',
            'attributes': {},
            'relationships': {
                'media': {
                    'data': {
                        'type': self.mediatype,
                        'id': item['id'],
                    }
                },
                'user': {
                    'data': {
                        'type': 'users',
                        'id': self._get_userconfig('userid'),
                    }
                },
            }
        }
        }

        # Update necessary keys
        if item['my_id']:
            values['data']['id'] = str(item['my_id'])
        if 'my_progress' in item:
            values['data']['attributes']['progress'] = item['my_progress']
        if 'my_status' in item:
            values['data']['attributes']['status'] = item['my_status']
        if 'my_score' in item:
            values['data']['attributes']['ratingTwenty'] = int(
                item['my_score']*4) or None

        return json.dumps(values)

    def _str2date(self, string):
        if string is None:
            return None

        try:
            return datetime.datetime.strptime(string, "%Y-%m-%d")
        except Exception:
            self.msg.debug('Invalid date {}'.format(string))
            return None  # Ignore date if it's invalid

    def _iso2date(self, string):
        if string is None:
            return None

        try:
            return datetime.datetime.strptime(string, "%Y-%m-%dT%H:%M:%S.%fZ").date()
        except Exception:
            self.msg.debug('Invalid date {}'.format(string))
            return None  # Ignore date if it's invalid

    def _iso2datetime(self, string):
        if string is None:
            return None

        try:
            return datetime.datetime.strptime(string, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=datetime.timezone.utc)
        except Exception:
            self.msg.debug('Invalid date {}'.format(string))
            return None  # Ignore date if it's invalid

    def _guess_status(self, start_date, end_date):
        # Try to guess show status by checking start and end dates
        now = datetime.datetime.now()

        if end_date and end_date < now:
            return utils.Status.FINISHED

        if start_date:
            if start_date > now:
                return utils.Status.NOTYET
            else:
                return utils.Status.AIRING

        # Safe to assume dates haven't even been announced yet
        return utils.Status.NOTYET

    def _parse_info(self, media):
        info = utils.show()
        attr = media['attributes']

        if media['type'] == 'anime':
            total = attr['episodeCount']
        elif media['type'] == 'manga':
            total = attr['chapterCount']
        elif media['type'] == 'drama':
            total = attr['episodeCount']

        poster_image = attr.get('posterImage', {})
        image = utils.get_any(poster_image, PosterImageKey.SMALL, PosterImageKey.MEDIUM, PosterImageKey.LARGE, PosterImageKey.ORIGINAL)
        image_thumb = utils.get_any(poster_image, PosterImageKey.TINY)

        info.update({
            'id': int(media['id']),
            # TODO : Some shows actually don't have a canonicalTitle; this should be fixed in the future.
            # For now I'm just picking the romaji title in these cases.
            'title':       attr['titles'].get('en_jp') or attr.get('canonicalTitle') or attr['titles'].get('en'),
            'total':       total or 0,
            'image':       image,
            'image_thumb': image_thumb,
            'start_date':  self._str2date(attr['startDate']),
            'end_date':    self._str2date(attr['endDate']),
            'type':        self.type_translate.get(attr['subtype'], utils.Type.UNKNOWN),
            'status':      self.status_translate.get(attr['status'], utils.Status.UNKNOWN),
            'url': "https://kitsu.app/{}/{}".format(self.mediatype, attr['slug']),
            'aliases':     list(filter(None, attr['titles'].values())),
            'extra': [
                ('Synopsis',            attr['description']),
                ('Type',                attr['subtype']),
                ('Titles',              list(
                    filter(None, attr['titles'].values()))),
                ('Synonyms',            attr.get('abbreviatedTitles', '?')),
                ('Average Rating',      attr.get('averageRating', '?')),
                ('Rank',                "Popularity #{}\nRating #{}".format(
                    attr.get('popularityRank', '?'), attr.get('ratingRank', '?'))),
                ('Age Rating',          "{} ({})".format(
                    attr.get('ageRating', 'Unknown'), attr.get('ageRatingGuide', 'Unknown'))),
            ]
        })

        if attr.get('nsfw', False):
            info['extra'].insert(
                2, ('NSFW', 'Yes'))
        elif attr.get('serialization', False):
            info['extra'].insert(
                4, ('Serialization', attr['serialization']))

        # WORKAROUND: Shows with 1 episode (TVs, SPs, OVAs) end the same day they start
        if total == 1:
            info['end_date'] = info['start_date']

        if attr['status'] in ['upcoming', 'unreleased']:
            info['extra'].append(('Expected Release', attr.get('tba', '?')))

        return info
