__filename__ = "outbox.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.3.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Timeline"

import os
from shutil import copyfile
from session import create_session
from auth import create_password
from posts import is_image_media
from posts import outbox_message_create_wrap
from posts import save_post_to_box
from posts import send_to_followers_thread
from posts import send_to_named_addresses_thread
from utils import get_account_timezone
from utils import has_object_stringType
from utils import get_base_content_from_post
from utils import has_object_dict
from utils import get_local_network_addresses
from utils import get_full_domain
from utils import remove_id_ending
from utils import get_domain_from_actor
from utils import dangerous_markup
from utils import is_featured_writer
from utils import load_json
from utils import save_json
from utils import acct_dir
from utils import local_actor_url
from utils import has_actor
from blocking import is_blocked_domain
from blocking import outbox_block
from blocking import outbox_undo_block
from blocking import outbox_mute
from blocking import outbox_undo_mute
from media import replace_you_tube
from media import replace_twitter
from media import get_media_path
from media import create_media_dirs
from inbox import inbox_update_index
from announce import outbox_announce
from announce import outbox_undo_announce
from follow import outbox_undo_follow
from follow import follower_approval_active
from skills import outbox_skills
from availability import outbox_availability
from like import outbox_like
from like import outbox_undo_like
from reaction import outbox_reaction
from reaction import outbox_undo_reaction
from bookmarks import outbox_bookmark
from bookmarks import outbox_undo_bookmark
from delete import outbox_delete
from shares import outbox_share_upload
from shares import outbox_undo_share_upload
from webapp_post import individual_post_as_html


def _person_receive_update_outbox(recent_posts_cache: {},
                                  base_dir: str, http_prefix: str,
                                  nickname: str, domain: str, port: int,
                                  message_json: {}, debug: bool) -> None:
    """ Receive an actor update from c2s
    For example, setting the PGP key from the desktop client
    """
    # these attachments are updatable via c2s
    updatable_attachments = ('PGP', 'OpenPGP', 'Email')

    if not message_json.get('type'):
        return
    if not isinstance(message_json['type'], str):
        if debug:
            print('DEBUG: c2s actor update type is not a string')
        return
    if message_json['type'] != 'Update':
        return
    if not has_object_stringType(message_json, debug):
        return
    if not isinstance(message_json['object']['type'], str):
        if debug:
            print('DEBUG: c2s actor update object type is not a string')
        return
    if message_json['object']['type'] != 'Person':
        if debug:
            print('DEBUG: not a c2s actor update')
        return
    if not message_json.get('to'):
        if debug:
            print('DEBUG: c2s actor update has no "to" field')
        return
    if not has_actor(message_json, debug):
        return
    if not message_json.get('id'):
        if debug:
            print('DEBUG: c2s actor update has no id field')
        return
    if not isinstance(message_json['id'], str):
        if debug:
            print('DEBUG: c2s actor update id is not a string')
        return
    domain_full = get_full_domain(domain, port)
    actor = local_actor_url(http_prefix, nickname, domain_full)
    if len(message_json['to']) != 1:
        if debug:
            print('DEBUG: c2s actor update - to does not contain one actor ' +
                  str(message_json['to']))
        return
    if message_json['to'][0] != actor:
        if debug:
            print('DEBUG: c2s actor update - to does not contain actor ' +
                  str(message_json['to']) + ' ' + actor)
        return
    if not message_json['id'].startswith(actor + '#updates/'):
        if debug:
            print('DEBUG: c2s actor update - unexpected id ' +
                  message_json['id'])
        return
    updated_actor_json = message_json['object']
    # load actor from file
    actor_filename = acct_dir(base_dir, nickname, domain) + '.json'
    if not os.path.isfile(actor_filename):
        print('actor_filename not found: ' + actor_filename)
        return
    actor_json = load_json(actor_filename)
    if not actor_json:
        return
    actor_changed = False
    # update fields within actor
    if 'attachment' in updated_actor_json:
        for new_property_value in updated_actor_json['attachment']:
            if not new_property_value.get('name'):
                continue
            if new_property_value['name'] not in updatable_attachments:
                continue
            if not new_property_value.get('type'):
                continue
            if not new_property_value.get('value'):
                continue
            if new_property_value['type'] != 'PropertyValue':
                continue
            if 'attachment' not in actor_json:
                continue
            found = False
            for attach_idx, _ in enumerate(actor_json['attachment']):
                if actor_json['attachment'][attach_idx]['type'] != \
                   'PropertyValue':
                    continue
                if actor_json['attachment'][attach_idx]['name'] != \
                   new_property_value['name']:
                    continue
                if actor_json['attachment'][attach_idx]['value'] != \
                   new_property_value['value']:
                    actor_json['attachment'][attach_idx]['value'] = \
                        new_property_value['value']
                    actor_changed = True
                found = True
                break
            if not found:
                actor_json['attachment'].append({
                    "name": new_property_value['name'],
                    "type": "PropertyValue",
                    "value": new_property_value['value']
                })
                actor_changed = True
    # save actor to file
    if actor_changed:
        save_json(actor_json, actor_filename)
        if debug:
            print('actor saved: ' + actor_filename)
    if debug:
        print('New attachment: ' + str(actor_json['attachment']))
    message_json['object'] = actor_json
    if debug:
        print('DEBUG: actor update via c2s - ' + nickname + '@' + domain)


def post_message_to_outbox(session, translate: {},
                           message_json: {}, post_to_nickname: str,
                           server, base_dir: str, http_prefix: str,
                           domain: str, domain_full: str,
                           onion_domain: str, i2p_domain: str, port: int,
                           recent_posts_cache: {}, followers_threads: [],
                           federation_list: [], send_threads: [],
                           post_log: [], cached_webfingers: {},
                           person_cache: {}, allow_deletion: bool,
                           proxy_type: str, version: str, debug: bool,
                           yt_replace_domain: str,
                           twitter_replacement_domain: str,
                           show_published_date_only: bool,
                           allow_local_network_access: bool,
                           city: str, system_language: str,
                           shared_items_federated_domains: [],
                           shared_item_federation_tokens: {},
                           low_bandwidth: bool,
                           signing_priv_key_pem: str,
                           peertube_instances: str, theme: str,
                           max_like_count: int,
                           max_recent_posts: int, cw_lists: {},
                           lists_enabled: str,
                           content_license_url: str) -> bool:
    """post is received by the outbox
    Client to server message post
    https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
    """
    if not message_json.get('type'):
        if debug:
            print('DEBUG: POST to outbox has no "type" parameter')
        return False
    if not message_json.get('object') and message_json.get('content'):
        if message_json['type'] != 'Create':
            # https://www.w3.org/TR/activitypub/#object-without-create
            if debug:
                print('DEBUG: POST to outbox - adding Create wrapper')
            message_json = \
                outbox_message_create_wrap(http_prefix,
                                           post_to_nickname,
                                           domain, port,
                                           message_json)

    # check that the outgoing post doesn't contain any markup
    # which can be used to implement exploits
    if has_object_dict(message_json):
        content_str = get_base_content_from_post(message_json, system_language)
        if content_str:
            if dangerous_markup(content_str, allow_local_network_access):
                print('POST to outbox contains dangerous markup: ' +
                      str(message_json))
                return False

    if message_json['type'] == 'Create':
        if not (message_json.get('id') and
                message_json.get('type') and
                message_json.get('actor') and
                message_json.get('object') and
                message_json.get('to')):
            if not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the id parameter ' +
                          str(message_json))
            elif not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the type parameter ' +
                          str(message_json))
            elif not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the actor parameter ' +
                          str(message_json))
            elif not message_json.get('id'):
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the object parameter ' +
                          str(message_json))
            else:
                if debug:
                    print('DEBUG: POST to outbox - ' +
                          'Create does not have the "to" parameter ' +
                          str(message_json))
            return False

        # actor should be a string
        if not isinstance(message_json['actor'], str):
            return False

        # actor should look like a url
        if '://' not in message_json['actor'] or \
           '.' not in message_json['actor']:
            return False

        # sent by an actor on a local network address?
        if not allow_local_network_access:
            local_network_pattern_list = get_local_network_addresses()
            for local_network_pattern in local_network_pattern_list:
                if local_network_pattern in message_json['actor']:
                    return False

        test_domain, test_port = get_domain_from_actor(message_json['actor'])
        test_domain = get_full_domain(test_domain, test_port)
        if is_blocked_domain(base_dir, test_domain):
            if debug:
                print('DEBUG: domain is blocked: ' + message_json['actor'])
            return False
        # replace youtube, so that google gets less tracking data
        replace_you_tube(message_json, yt_replace_domain, system_language)
        # replace twitter, so that twitter posts can be shown without
        # having a twitter account
        replace_twitter(message_json, twitter_replacement_domain,
                        system_language)
        # https://www.w3.org/TR/activitypub/#create-activity-outbox
        message_json['object']['attributedTo'] = message_json['actor']
        if message_json['object'].get('attachment'):
            attachment_index = 0
            attach = message_json['object']['attachment'][attachment_index]
            if attach.get('mediaType'):
                file_extension = 'png'
                media_type_str = \
                    attach['mediaType']

                extensions = {
                    "jpeg": "jpg",
                    "jxl": "jxl",
                    "gif": "gif",
                    "svg": "svg",
                    "webp": "webp",
                    "avif": "avif",
                    "audio/mpeg": "mp3",
                    "ogg": "ogg",
                    "mp4": "mp4",
                    "webm": "webm",
                    "ogv": "ogv"
                }
                for match_ext, ext in extensions.items():
                    if media_type_str.endswith(match_ext):
                        file_extension = ext
                        break

                media_dir = \
                    base_dir + '/accounts/' + \
                    post_to_nickname + '@' + domain
                upload_media_filename = media_dir + '/upload.' + file_extension
                if not os.path.isfile(upload_media_filename):
                    del message_json['object']['attachment']
                else:
                    # generate a path for the uploaded image
                    mpath = get_media_path()
                    media_path = mpath + '/' + \
                        create_password(16).lower() + '.' + file_extension
                    create_media_dirs(base_dir, mpath)
                    media_filename = base_dir + '/' + media_path
                    # move the uploaded image to its new path
                    os.rename(upload_media_filename, media_filename)
                    # change the url of the attachment
                    attach['url'] = \
                        http_prefix + '://' + domain_full + '/' + media_path
                    attach['url'] = \
                        attach['url'].replace('/media/',
                                              '/system/' +
                                              'media_attachments/files/')

    permitted_outbox_types = (
        'Create', 'Announce', 'Like', 'EmojiReact', 'Follow', 'Undo',
        'Update', 'Add', 'Remove', 'Block', 'Delete', 'Skill', 'Ignore'
    )
    if message_json['type'] not in permitted_outbox_types:
        if debug:
            print('DEBUG: POST to outbox - ' + message_json['type'] +
                  ' is not a permitted activity type')
        return False
    if message_json.get('id'):
        post_id = remove_id_ending(message_json['id'])
        if debug:
            print('DEBUG: id attribute exists within POST to outbox')
    else:
        if debug:
            print('DEBUG: No id attribute within POST to outbox')
        post_id = None
    if debug:
        print('DEBUG: save_post_to_box')
    if message_json['type'] != 'Upgrade':
        outbox_name = 'outbox'

        # if this is a blog post or an event then save to its own box
        if message_json['type'] == 'Create':
            if has_object_dict(message_json):
                if message_json['object'].get('type'):
                    if message_json['object']['type'] == 'Article':
                        outbox_name = 'tlblogs'

        saved_filename = \
            save_post_to_box(base_dir,
                             http_prefix,
                             post_id,
                             post_to_nickname, domain_full,
                             message_json, outbox_name)
        if not saved_filename:
            print('WARN: post not saved to outbox ' + outbox_name)
            return False

        # save all instance blogs to the news actor
        if post_to_nickname != 'news' and outbox_name == 'tlblogs':
            if '/' in saved_filename:
                if is_featured_writer(base_dir, post_to_nickname, domain):
                    saved_post_id = saved_filename.split('/')[-1]
                    blogs_dir = \
                        base_dir + '/accounts/news@' + domain + '/tlblogs'
                    if not os.path.isdir(blogs_dir):
                        os.mkdir(blogs_dir)
                    copyfile(saved_filename, blogs_dir + '/' + saved_post_id)
                    inbox_update_index('tlblogs', base_dir,
                                       'news@' + domain,
                                       saved_filename, debug)

                # clear the citations file if it exists
                citations_filename = \
                    base_dir + '/accounts/' + \
                    post_to_nickname + '@' + domain + '/.citations.txt'
                if os.path.isfile(citations_filename):
                    try:
                        os.remove(citations_filename)
                    except OSError:
                        print('EX: post_message_to_outbox unable to delete ' +
                              citations_filename)

        # The following activity types get added to the index files
        indexed_activities = (
            'Create', 'Question', 'Note', 'EncryptedMessage', 'Article',
            'Patch', 'Announce', 'ChatMessage'
        )
        if message_json['type'] in indexed_activities:
            indexes = [outbox_name, "inbox"]
            self_actor = \
                local_actor_url(http_prefix, post_to_nickname, domain_full)
            for box_name_index in indexes:
                if not box_name_index:
                    continue

                # should this also go to the media timeline?
                if box_name_index == 'inbox':
                    if is_image_media(session, base_dir, http_prefix,
                                      post_to_nickname, domain,
                                      message_json,
                                      translate,
                                      yt_replace_domain,
                                      twitter_replacement_domain,
                                      allow_local_network_access,
                                      recent_posts_cache, debug,
                                      system_language,
                                      domain_full, person_cache,
                                      signing_priv_key_pem):
                        inbox_update_index('tlmedia', base_dir,
                                           post_to_nickname + '@' + domain,
                                           saved_filename, debug)

                if box_name_index == 'inbox' and outbox_name == 'tlblogs':
                    continue

                # avoid duplicates of the message if already going
                # back to the inbox of the same account
                if self_actor not in message_json['to']:
                    # show sent post within the inbox,
                    # as is the typical convention
                    inbox_update_index(box_name_index, base_dir,
                                       post_to_nickname + '@' + domain,
                                       saved_filename, debug)

                    # regenerate the html
                    use_cache_only = False
                    page_number = 1
                    show_individual_post_icons = True
                    manually_approve_followers = \
                        follower_approval_active(base_dir,
                                                 post_to_nickname, domain)
                    timezone = \
                        get_account_timezone(base_dir,
                                             post_to_nickname, domain)
                    individual_post_as_html(signing_priv_key_pem,
                                            False, recent_posts_cache,
                                            max_recent_posts,
                                            translate, page_number,
                                            base_dir, session,
                                            cached_webfingers,
                                            person_cache,
                                            post_to_nickname, domain, port,
                                            message_json, None, True,
                                            allow_deletion,
                                            http_prefix, __version__,
                                            box_name_index,
                                            yt_replace_domain,
                                            twitter_replacement_domain,
                                            show_published_date_only,
                                            peertube_instances,
                                            allow_local_network_access,
                                            theme, system_language,
                                            max_like_count,
                                            box_name_index != 'dm',
                                            show_individual_post_icons,
                                            manually_approve_followers,
                                            False, True, use_cache_only,
                                            cw_lists, lists_enabled,
                                            timezone)

    if outbox_announce(recent_posts_cache,
                       base_dir, message_json, debug):
        if debug:
            print('DEBUG: Updated announcements (shares) collection ' +
                  'for the post associated with the Announce activity')
    if not server.session:
        print('DEBUG: creating new session for c2s')
        server.session = create_session(proxy_type)
        if not server.session:
            print('ERROR: Failed to create session for post_message_to_outbox')
            return False
    if debug:
        print('DEBUG: sending c2s post to followers')
    # remove inactive threads
    inactive_follower_threads = []
    for thr in followers_threads:
        if not thr.is_alive():
            inactive_follower_threads.append(thr)
    for thr in inactive_follower_threads:
        followers_threads.remove(thr)
    if debug:
        print('DEBUG: ' + str(len(followers_threads)) +
              ' followers threads active')
    # retain up to 200 threads
    if len(followers_threads) > 200:
        # kill the thread if it is still alive
        if followers_threads[0].is_alive():
            followers_threads[0].kill()
        # remove it from the list
        followers_threads.pop(0)
    # create a thread to send the post to followers
    followers_thread = \
        send_to_followers_thread(server.session,
                                 base_dir,
                                 post_to_nickname,
                                 domain, onion_domain, i2p_domain,
                                 port, http_prefix,
                                 federation_list,
                                 send_threads,
                                 post_log,
                                 cached_webfingers,
                                 person_cache,
                                 message_json, debug,
                                 version,
                                 shared_items_federated_domains,
                                 shared_item_federation_tokens,
                                 signing_priv_key_pem)
    followers_threads.append(followers_thread)

    if debug:
        print('DEBUG: handle any unfollow requests')
    outbox_undo_follow(base_dir, message_json, debug)

    if debug:
        print('DEBUG: handle skills changes requests')
    outbox_skills(base_dir, post_to_nickname, message_json, debug)

    if debug:
        print('DEBUG: handle availability changes requests')
    outbox_availability(base_dir, post_to_nickname, message_json, debug)

    if debug:
        print('DEBUG: handle any like requests')
    outbox_like(recent_posts_cache,
                base_dir, http_prefix,
                post_to_nickname, domain, port,
                message_json, debug)
    if debug:
        print('DEBUG: handle any undo like requests')
    outbox_undo_like(recent_posts_cache,
                     base_dir, http_prefix,
                     post_to_nickname, domain, port,
                     message_json, debug)

    if debug:
        print('DEBUG: handle any emoji reaction requests')
    outbox_reaction(recent_posts_cache,
                    base_dir, http_prefix,
                    post_to_nickname, domain, port,
                    message_json, debug)
    if debug:
        print('DEBUG: handle any undo emoji reaction requests')
    outbox_undo_reaction(recent_posts_cache,
                         base_dir, http_prefix,
                         post_to_nickname, domain, port,
                         message_json, debug)

    if debug:
        print('DEBUG: handle any undo announce requests')
    outbox_undo_announce(recent_posts_cache,
                         base_dir, http_prefix,
                         post_to_nickname, domain, port,
                         message_json, debug)

    if debug:
        print('DEBUG: handle any bookmark requests')
    outbox_bookmark(recent_posts_cache,
                    base_dir, http_prefix,
                    post_to_nickname, domain, port,
                    message_json, debug)
    if debug:
        print('DEBUG: handle any undo bookmark requests')
    outbox_undo_bookmark(recent_posts_cache,
                         base_dir, http_prefix,
                         post_to_nickname, domain, port,
                         message_json, debug)

    if debug:
        print('DEBUG: handle delete requests')
    outbox_delete(base_dir, http_prefix,
                  post_to_nickname, domain,
                  message_json, debug,
                  allow_deletion,
                  recent_posts_cache)

    if debug:
        print('DEBUG: handle block requests')
    outbox_block(base_dir, http_prefix,
                 post_to_nickname, domain,
                 port,
                 message_json, debug)

    if debug:
        print('DEBUG: handle undo block requests')
    outbox_undo_block(base_dir, http_prefix,
                      post_to_nickname, domain,
                      port, message_json, debug)

    if debug:
        print('DEBUG: handle mute requests')
    outbox_mute(base_dir, http_prefix,
                post_to_nickname, domain,
                port,
                message_json, debug,
                recent_posts_cache)

    if debug:
        print('DEBUG: handle undo mute requests')
    outbox_undo_mute(base_dir, http_prefix,
                     post_to_nickname, domain,
                     port,
                     message_json, debug,
                     recent_posts_cache)

    if debug:
        print('DEBUG: handle share uploads')
    outbox_share_upload(base_dir, http_prefix, post_to_nickname, domain,
                        port, message_json, debug, city,
                        system_language, translate, low_bandwidth,
                        content_license_url)

    if debug:
        print('DEBUG: handle undo share uploads')
    outbox_undo_share_upload(base_dir, http_prefix,
                             post_to_nickname, domain,
                             port, message_json, debug)

    if debug:
        print('DEBUG: handle actor updates from c2s')
    _person_receive_update_outbox(recent_posts_cache,
                                  base_dir, http_prefix,
                                  post_to_nickname, domain, port,
                                  message_json, debug)

    if debug:
        print('DEBUG: sending c2s post to named addresses')
        if message_json.get('to'):
            print('c2s sender: ' +
                  post_to_nickname + '@' + domain + ':' + str(port) +
                  ' recipient: ' + str(message_json['to']))
        else:
            print('c2s sender: ' +
                  post_to_nickname + '@' + domain + ':' + str(port))
    named_addresses_thread = \
        send_to_named_addresses_thread(server.session, base_dir,
                                       post_to_nickname,
                                       domain, onion_domain, i2p_domain, port,
                                       http_prefix,
                                       federation_list,
                                       send_threads,
                                       post_log,
                                       cached_webfingers,
                                       person_cache,
                                       message_json, debug,
                                       version,
                                       shared_items_federated_domains,
                                       shared_item_federation_tokens,
                                       signing_priv_key_pem)
    followers_threads.append(named_addresses_thread)
    return True