__filename__ = "reading.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.5.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Core"


import os
from collections import OrderedDict
from utils import get_post_attachments
from utils import get_content_from_post
from utils import has_object_dict
from utils import remove_id_ending
from utils import get_attributed_to
from utils import load_json
from utils import save_json
from utils import remove_html
from utils import get_image_extensions
from utils import date_epoch
from utils import date_from_string_format


def get_book_link_from_content(content: str) -> str:
    """ Returns a book link from the given content
    """
    if '://' not in content or \
       '"' not in content:
        return None
    sections = content.split('://')
    if '"' not in sections[0] or '"' not in sections[1]:
        return None
    previous_str = sections[0].split('"')[-1]
    next_str = sections[1].split('"')[0]
    book_url = previous_str + '://' + next_str
    return book_url


def get_book_from_post(post_json_object: {}, debug: bool) -> {}:
    """ Returns a book details from the given post
    """
    if 'tag' not in post_json_object:
        if debug:
            print('DEBUG: get_book_from_post no tag in post')
        return {}
    if not isinstance(post_json_object['tag'], list):
        if debug:
            print('DEBUG: get_book_from_post tag is not a list')
        return {}
    for tag_dict in post_json_object['tag']:
        if 'type' not in tag_dict:
            continue
        if not isinstance(tag_dict['type'], str):
            continue
        if tag_dict['type'] != 'Edition':
            continue
        if not tag_dict.get('href'):
            continue
        if not isinstance(tag_dict['href'], str):
            continue
        if not tag_dict.get('name'):
            continue
        if not isinstance(tag_dict['name'], str):
            continue
        tag_dict['name'] = tag_dict['name'].replace('@', '')
        return tag_dict.copy()
    return {}


def _get_book_image_from_post(post_json_object: {}) -> str:
    """ Returns a book image from the given post
    """
    post_attachments = get_post_attachments(post_json_object)
    if not post_attachments:
        return ''
    extensions = get_image_extensions()
    for attach_dict in post_attachments:
        if not isinstance(attach_dict, dict):
            continue
        if 'url' not in attach_dict:
            continue
        if not isinstance(attach_dict['url'], str):
            continue
        for ext in extensions:
            if attach_dict['url'].endswith('.' + ext):
                return attach_dict['url']
    return ''


def has_edition_tag(post_json_object: {}) -> bool:
    """Checks whether the given post has an Edition tag
    indicating that it contains a book event
    """
    post_obj = post_json_object
    if has_object_dict(post_json_object):
        post_obj = post_json_object['object']

    if not post_obj.get('tag'):
        return False
    if not isinstance(post_obj['tag'], list):
        return False
    for tag in post_obj['tag']:
        if not isinstance(tag, dict):
            continue
        if not tag.get('type'):
            continue
        if not isinstance(tag['type'], str):
            continue
        if tag['type'] == 'Edition':
            return True
    return False


def get_reading_status(post_json_object: {},
                       system_language: str,
                       languages_understood: [],
                       translate: {},
                       debug: bool) -> {}:
    """Returns any reading status from the content of a post
    """
    post_obj = post_json_object
    if has_object_dict(post_json_object):
        post_obj = post_json_object['object']

    content = get_content_from_post(post_json_object, system_language,
                                    languages_understood,
                                    "content")
    if not content:
        if debug:
            print('DEBUG: get_reading_status no content')
        return {}
    book_url = get_book_link_from_content(content)
    if not book_url:
        if debug:
            print('DEBUG: get_reading_status no book url')
        return {}

    if not post_obj.get('id'):
        if debug:
            print('DEBUG: get_reading_status no id')
        return {}
    if not isinstance(post_obj['id'], str):
        if debug:
            print('DEBUG: get_reading_status id is not a string')
        return {}

    # get the published date
    if not post_obj.get('published'):
        if debug:
            print('DEBUG: get_reading_status no published')
        return {}
    if not isinstance(post_obj['published'], str):
        if debug:
            print('DEBUG: get_reading_status published is not a string')
        return {}
    published = post_obj['published']
    if post_obj.get('updated'):
        if isinstance(post_obj['updated'], str):
            published = post_obj['updated']

    if not post_obj.get('attributedTo'):
        if debug:
            print('DEBUG: get_reading_status no attributedTo')
        return {}
    actor = get_attributed_to(post_obj['attributedTo'])
    if not actor:
        if debug:
            print('DEBUG: get_reading_status no actor')
        return {}

    book_image_url = _get_book_image_from_post(post_obj)

    # rating of a book
    if post_obj.get('rating'):
        rating = post_obj['rating']
        if isinstance(rating, (float, int)):
            translated_str = 'rated'
            if translate.get('rated'):
                translated_str = translate['rated']
            if translated_str in content or \
               'rated' in content:
                book_dict = {
                    'id': remove_id_ending(post_obj['id']),
                    'actor': actor,
                    'type': 'rated',
                    'href': book_url,
                    'rating': rating,
                    'published': published
                }
                if book_image_url:
                    book_dict['image_url'] = book_image_url
                return book_dict

    if not has_edition_tag(post_json_object):
        return {}

    # get the book details from a post tag
    book_dict = get_book_from_post(post_obj, debug)
    if not book_dict:
        if debug:
            print('DEBUG: get_reading_status no book_dict ' +
                  str(post_json_object))
        return {}

    # want to read a book
    translated_str = 'wants to read'
    if translate.get('wants to read'):
        translated_str = translate['wants to read']
    if translated_str in content or \
       'wants to read' in content:
        book_dict['id'] = remove_id_ending(post_obj['id'])
        book_dict['actor'] = actor
        book_dict['type'] = 'want'
        book_dict['published'] = published
        if book_image_url:
            book_dict['image_url'] = book_image_url
        return book_dict

    translated_str = 'finished reading'
    if translate.get('finished reading'):
        translated_str = translate['finished reading']
    if translated_str in content or \
       'finished reading' in content:
        book_dict['id'] = remove_id_ending(post_obj['id'])
        book_dict['actor'] = actor
        book_dict['type'] = 'finished'
        book_dict['published'] = published
        if book_image_url:
            book_dict['image_url'] = book_image_url
        return book_dict

    translated_str = 'am reading'
    if translate.get('am reading'):
        translated_str = translate['am reading']
    if translated_str in content or \
       'am reading' in content or \
       'currently reading' in content or \
       'is reading' in content:
        book_dict['id'] = remove_id_ending(post_obj['id'])
        book_dict['actor'] = actor
        book_dict['type'] = 'reading'
        book_dict['published'] = published
        if book_image_url:
            book_dict['image_url'] = book_image_url
        return book_dict

    return {}


def remove_reading_event(base_dir: str,
                         actor: str, post_secs_since_epoch: str,
                         book_event_type: str,
                         books_cache: {},
                         debug: bool) -> bool:
    """Removes a reading status for the given actor
    """
    if not book_event_type:
        print('remove_reading_event no book event')
        return False
    reading_path = base_dir + '/accounts/reading'
    readers_path = reading_path + '/readers'
    reader_books_filename = \
        readers_path + '/' + actor.replace('/', '#') + '.json'

    reader_books_json = {}
    if 'readers' not in books_cache:
        books_cache['readers'] = {}
    if books_cache['readers'].get(actor):
        reader_books_json = books_cache['readers'][actor]
    elif os.path.isfile(reader_books_filename):
        # if not in cache then load from file
        reader_books_json = load_json(reader_books_filename)
    if not reader_books_json:
        if debug:
            print('remove_reading_event reader_books_json does not exist')
        return False
    if not reader_books_json.get('timeline'):
        if debug:
            print('remove_reading_event ' +
                  'reader_books_json timeline does not exist')
        return False
    if not reader_books_json['timeline'].get(post_secs_since_epoch):
        if debug:
            print('remove_reading_event ' +
                  'reader_books_json timeline event does not exist ' +
                  str(post_secs_since_epoch))
        return False
    book_url = reader_books_json['timeline'][post_secs_since_epoch]
    if not book_url:
        if debug:
            print('remove_reading_event no book_url')
        return False
    if not reader_books_json.get(book_url):
        if debug:
            print('remove_reading_event ' +
                  'book_url not found in reader_books_json ' + book_url)
        return False
    if not reader_books_json[book_url].get(book_event_type):
        if debug:
            print('remove_reading_event ' +
                  'book event not found in reader_books_json ' +
                  book_url + ' ' + book_event_type)
        return False
    del reader_books_json[book_url][book_event_type]
    if not save_json(reader_books_json, reader_books_filename):
        if debug:
            print('DEBUG: ' +
                  'remove_reading_event unable to save reader book event')
        return False
    print('reading status removed by ' + actor)
    return True


def _add_book_to_reader(reader_books_json: {}, book_dict: {},
                        debug: bool) -> bool:
    """Updates reader books
    """
    if not book_dict.get('published'):
        if debug:
            print('_add_book_to_reader no published field')
        return False
    book_url = book_dict['href']
    book_event_type = book_dict['type']
    if not reader_books_json.get(book_url):
        reader_books_json[book_url] = {}
        if debug:
            print('_add_book_to_reader first book')
    else:
        # has this book event already been stored?
        if reader_books_json[book_url].get(book_event_type):
            prev_book_dict = reader_books_json[book_url][book_event_type]
            if book_dict.get('updated'):
                if prev_book_dict.get('updated'):
                    if prev_book_dict['updated'] == book_dict['updated']:
                        if debug:
                            print('_add_book_to_reader ' +
                                  'updated date already seen')
                        return False
                else:
                    if prev_book_dict['published'] == book_dict['updated']:
                        if debug:
                            print('_add_book_to_reader ' +
                                  'published date already seen')
                        return False
            if prev_book_dict['published'] == book_dict['published']:

                return False
    # store the book event
    reader_books_json[book_url][book_event_type] = book_dict
    if 'timeline' not in reader_books_json:
        reader_books_json['timeline'] = {}
    published = book_dict['published']
    if book_dict.get('updated'):
        published = book_dict['updated']
    if '.' in published:
        published = published.split('.')[0] + 'Z'
    post_time_object = \
        date_from_string_format(published, ["%Y-%m-%dT%H:%M:%S%z",
                                            "%Y-%m-%dT%H:%M:%S%Z"])
    if post_time_object:
        baseline_time = date_epoch()
        days_diff = post_time_object - baseline_time
        post_secs_since_epoch = days_diff.total_seconds()
        reader_books_json['timeline'][post_secs_since_epoch] = book_url
        return True
    elif debug:
        print('_add_book_to_reader published date not recognised ' + published)
    return False


def _add_reader_to_book(book_json: {}, book_dict: {}) -> None:
    """Updates book with a new reader
    """
    book_event_type = book_dict['type']
    actor = book_dict['actor']
    if not book_json.get(actor):
        book_json[actor] = {
            book_event_type: book_dict
        }
        if book_dict.get('name'):
            book_json['title'] = remove_html(book_dict['name'])
        return
    book_json[actor][book_event_type] = book_dict
    if book_dict.get('name'):
        book_json['title'] = remove_html(book_dict['name'])


def _update_recent_books_list(base_dir: str, book_id: str,
                              debug: bool) -> None:
    """prepend a book to the recent books list
    """
    recent_books_filename = base_dir + '/accounts/recent_books.txt'
    if os.path.isfile(recent_books_filename):
        try:
            with open(recent_books_filename, 'r+',
                      encoding='utf-8') as recent_file:
                content = recent_file.read()
                if book_id + '\n' not in content:
                    recent_file.seek(0, 0)
                    recent_file.write(book_id + '\n' + content)
                    if debug:
                        print('DEBUG: recent book added')
        except OSError as ex:
            print('WARN: Failed to write entry to recent books ' +
                  recent_books_filename + ' ' + str(ex))
    else:
        try:
            with open(recent_books_filename, 'w+',
                      encoding='utf-8') as recent_file:
                recent_file.write(book_id + '\n')
        except OSError:
            print('EX: unable to write recent books ' +
                  recent_books_filename)


def _deduplicate_recent_books_list(base_dir: str,
                                   max_recent_books: int) -> None:
    """ Deduplicate and limit the length of the recent books list
    """
    recent_books_filename = base_dir + '/accounts/recent_books.txt'
    if not os.path.isfile(recent_books_filename):
        return

    # load recent books as a list
    recent_lines = []
    try:
        with open(recent_books_filename, 'r',
                  encoding='utf-8') as recent_file:
            recent_lines = recent_file.read().split('\n')
    except OSError as ex:
        print('WARN: Failed to read recent books trim ' +
              recent_books_filename + ' ' + str(ex))

    # deduplicate the list
    new_recent_lines = []
    for line in recent_lines:
        if line not in new_recent_lines:
            new_recent_lines.append(line)
    if len(new_recent_lines) < len(recent_lines):
        recent_lines = new_recent_lines
        result = ''
        for line in recent_lines:
            result += line + '\n'
        try:
            with open(recent_books_filename, 'w+',
                      encoding='utf-8') as recent_file:
                recent_file.write(result)
        except OSError:
            print('EX: unable to deduplicate recent books ' +
                  recent_books_filename)

    # remove excess lines from the list
    if len(recent_lines) > max_recent_books:
        result = ''
        for ctr in range(max_recent_books):
            result += recent_lines[ctr] + '\n'
        try:
            with open(recent_books_filename, 'w+',
                      encoding='utf-8') as recent_file:
                recent_file.write(result)
        except OSError:
            print('EX: unable to trim recent books ' +
                  recent_books_filename)


def store_book_events(base_dir: str,
                      post_json_object: {},
                      system_language: str,
                      languages_understood: [],
                      translate: {},
                      debug: bool,
                      max_recent_books: int,
                      books_cache: {},
                      max_cached_readers: int) -> bool:
    """Saves book events to file under accounts/reading/books
    and accounts/reading/readers
    """
    book_dict = get_reading_status(post_json_object,
                                   system_language,
                                   languages_understood,
                                   translate, debug)
    if not book_dict:
        if debug:
            print('DEBUG: no book event')
        return False
    reading_path = base_dir + '/accounts/reading'
    if not os.path.isdir(base_dir + '/accounts'):
        os.mkdir(base_dir + '/accounts')
    if not os.path.isdir(reading_path):
        os.mkdir(reading_path)
    books_path = reading_path + '/books'
    if not os.path.isdir(books_path):
        os.mkdir(books_path)
    readers_path = reading_path + '/readers'
    if not os.path.isdir(readers_path):
        os.mkdir(readers_path)

    actor = book_dict['actor']
    book_url = remove_id_ending(book_dict['href'])

    reader_books_filename = \
        readers_path + '/' + actor.replace('/', '#') + '.json'
    if debug:
        print('reader_books_filename: ' + reader_books_filename)
    reader_books_json = {}

    # get the reader from cache if possible
    if 'readers' not in books_cache:
        books_cache['readers'] = {}
    if books_cache['readers'].get(actor):
        reader_books_json = books_cache['readers'][actor]
    elif os.path.isfile(reader_books_filename):
        # if not in cache then load from file
        reader_books_json = load_json(reader_books_filename)
    if _add_book_to_reader(reader_books_json, book_dict, debug):
        if not save_json(reader_books_json, reader_books_filename):
            if debug:
                print('DEBUG: unable to save reader book event')
            return False

        # update the cache for this reader
        books_cache['readers'][actor] = reader_books_json
        if 'reader_list' not in books_cache:
            books_cache['reader_list'] = []
        if actor in books_cache['reader_list']:
            books_cache['reader_list'].remove(actor)
        books_cache['reader_list'].append(actor)
        # avoid too much caching
        if len(books_cache['reader_list']) > max_cached_readers:
            first_actor = books_cache['reader_list'][0]
            books_cache['reader_list'].remove(first_actor)
            del books_cache['readers'][actor]
    elif debug:
        print('_add_book_to_reader failed ' + str(book_dict))

    book_id = book_url.replace('/', '#')
    book_filename = books_path + '/' + book_id + '.json'
    book_json = {}
    if os.path.isfile(book_filename):
        book_json = load_json(book_filename)
    _add_reader_to_book(book_json, book_dict)
    if not save_json(book_json, book_filename):
        if debug:
            print('DEBUG: unable to save book reader')
        return False

    _update_recent_books_list(base_dir, book_id, debug)
    _deduplicate_recent_books_list(base_dir, max_recent_books)

    return True


def html_profile_book_list(base_dir: str, actor: str, no_of_books: int,
                           translate: {},
                           nickname: str, domain: str,
                           authorized: bool) -> str:
    """Returns html for displaying a list of books on a profile screen
    """
    reading_path = base_dir + '/accounts/reading'
    readers_path = reading_path + '/readers'
    reader_books_filename = \
        readers_path + '/' + actor.replace('/', '#') + '.json'
    reader_books_json = {}
    if not os.path.isfile(reader_books_filename):
        return ''
    reader_books_json = load_json(reader_books_filename)
    if not reader_books_json.get('timeline'):
        return ''
    # sort the timeline in descending order
    recent_books_json = \
        OrderedDict(sorted(reader_books_json['timeline'].items(),
                           reverse=True))
    html_str = '<div class="book_list_section">\n'
    html_str += '  <ul class="book_list">\n'
    ctr = 0
    for published_time_sec, book_url in recent_books_json.items():
        if not reader_books_json.get(book_url):
            continue
        book_rating = None
        book_wanted = False
        book_reading = False
        book_finished = False
        book_event_type = ''
        for event_type in ('want', 'finished', 'rated'):
            if not reader_books_json[book_url].get(event_type):
                continue
            book_dict = reader_books_json[book_url][event_type]
            if book_dict.get('name'):
                book_title = book_dict['name']
            if book_dict.get('image_url'):
                book_image_url = book_dict['image_url']
            if event_type == 'rated':
                book_rating = book_dict['rating']
                book_event_type = event_type
            elif event_type == 'want':
                book_wanted = True
                book_event_type = event_type
            elif event_type == 'reading':
                book_reading = True
                book_event_type = event_type
            elif event_type == 'finished':
                book_finished = True
                book_event_type = event_type
        if book_title:
            book_title = remove_html(book_title)
            html_str += '    <li class="book_event">\n'
            html_str += '      <span class="book_span">\n'
            html_str += '        <div class="book_span_div">\n'

            # book image
            if book_image_url:
                html_str += '          <a href="' + book_url + \
                    '" target="_blank" rel="nofollow noopener noreferrer">\n'
                html_str += '            <div class="book_image_div">\n'
                html_str += '              <img src="' + \
                    book_image_url + '" ' + \
                    'alt="' + book_title + '">\n'
                html_str += '            </div>\n'
                html_str += '          </a>\n'

            # book details
            html_str += '          <div class="book_details_div">\n'
            html_str += '            <a href="' + book_url + \
                '" target="_blank" rel="nofollow noopener noreferrer">\n'
            html_str += '              <b>' + book_title.title() + '</b></a>\n'
            if book_finished:
                html_str += '            <br>' + \
                    translate['finished reading'].title() + '\n'
            if book_wanted:
                html_str += '            <br>' + \
                    translate['Wanted'] + '\n'
            if book_reading:
                html_str += '            <br>' + \
                    translate['reading'].title() + '\n'
            # book star rating
            if book_rating is not None:
                html_str += '            <br>'
                for _ in range(int(book_rating)):
                    html_str += '⭐'
                html_str += ' (' + str(book_rating) + ')\n'
            # remove button
            if authorized:
                if actor.endswith('/users/' + nickname) and \
                   '://' + domain in actor:
                    html_str += \
                        '            <br>\n' + \
                        '            <form method="POST" action="' + \
                        '/users/' + nickname + '/removereadingstatus">\n' + \
                        '              ' + \
                        '<input type="hidden" name="actor" value="' + \
                        actor + '">\n' + \
                        '              ' + \
                        '<input type="hidden" ' + \
                        'name="publishedtimesec" value="' + \
                        str(published_time_sec) + '">\n' + \
                        '              ' + \
                        '<input type="hidden" ' + \
                        'name="bookeventtype" value="' + \
                        book_event_type + '">\n' + \
                        '              ' + \
                        '<button type="submit" class="button" ' + \
                        'name="submitRemoveReadingStatus">' + \
                        translate['Remove'] + '</button>\n' + \
                        '            </form>\n'
            html_str += '          </div>\n'

            html_str += '        </div>\n'
            html_str += '      </span>\n'
            html_str += '    </li>\n'
        ctr += 1
        if ctr >= no_of_books:
            break
    html_str += '  </ul>\n'
    html_str += '</div>\n'
    return html_str