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

"""

TODO

1) Deprecate "add" (see above) and make it interactive.
   Slixfeed: Do you still want to add this URL to subscription list?
   See: case _ if command_lowercase.startswith("add"):

2) If subscription is inadequate (see XmppPresence.request), send a message that says so.

    elif not self.client_roster[jid]["to"]:
        breakpoint()
        message.reply("Share online status to activate bot.").send()
        return

3) Set timeout for moderator interaction.
   If moderator interaction has been made, and moderator approves the bot, then
   the bot will add the given groupchat to bookmarks; otherwise, the bot will
   send a message that it was not approved and therefore leaves the groupchat.

"""

import json
from omemo.storage import Just, Maybe, Nothing, Storage
from omemo.types import DeviceInformation, JSONType
import os
from slixfeed.config import Data
from slixfeed.utility.logger import UtilityLogger
from slixmpp.exceptions import IqTimeout, IqError
from slixmpp.jid import JID
#from slixmpp.plugins import register_plugin
from slixmpp.stanza import Message
from slixmpp_omemo import TrustLevel, XEP_0384
from typing import Any, Dict, FrozenSet, Literal, Optional, Union

logger = UtilityLogger(__name__)

    # for task in main_task:
    #     task.cancel()

    # Deprecated in favour of event "presence_available"
    # if not main_task:
    #     await select_file()

class XmppOmemo:

    async def decrypt(self, stanza: Message):

        omemo_decrypted = None

        mto = stanza["from"]
        mtype = stanza["type"]

        namespace = self['xep_0384'].is_encrypted(stanza)
        if namespace is None:
            omemo_decrypted = False
            response = f"Unencrypted message or unsupported message encryption: {stanza['body']}"
        else:
            logger.info(f'Message in namespace {namespace} received: {stanza}')
            try:
                response, device_information = await self['xep_0384'].decrypt_message(stanza)
                logger.info(f'Information about sender: {device_information}')
                omemo_decrypted = True
            except Exception as e:  # pylint: disable=broad-exception-caught
                response = f'Error {type(e).__name__}: {e}'

        return response, omemo_decrypted

    async def encrypt(
        self,
        mto: JID,
        mtype: Literal['chat', 'normal'],
        mbody: str
    ) -> None:
        
        if isinstance(mbody, str):
            reply = self.make_message(mto=mto, mtype=mtype)
            reply['body'] = mbody

        reply.set_to(mto)
        reply.set_from(self.boundjid)

        # It might be a good idea to strip everything except for the body from the stanza,
        # since some things might break when echoed.
        message, encryption_errors = await self['xep_0384'].encrypt_message(reply, mto)

        if len(encryption_errors) > 0:
            logger.info(f'There were non-critical errors during encryption: {encryption_errors}')
            #log.info(f'There were non-critical errors during encryption: {encryption_errors}')

        # for namespace, message in messages.items():
        #     message['eme']['namespace'] = namespace
        #     message['eme']['name'] = self['xep_0380'].mechanisms[namespace]

        return message, True

    async def _decrypt(self, message: Message, allow_untrusted: bool = False):
        jid = message['from']
        try:
            logger.info('XmppOmemo.decrypt')
            message_omemo_encrypted = message['omemo_encrypted']
            message_body = await self['xep_0384'].decrypt_message(
                message_omemo_encrypted, jid, allow_untrusted)
            # decrypt_message returns Optional[str]. It is possible to get
            # body-less OMEMO message (see KeyTransportMessages), currently
            # used for example to send heartbeats to other devices.
            if message_body is not None:
                response = message_body.decode('utf8')
                omemo_decrypted = True
            else:
                omemo_decrypted = response = None
            retry = None
        except (MissingOwnKey,) as exn:
            logger.info('XmppOmemo.decrypt. except: MissingOwnKey')
            # The message is missing our own key, it was not encrypted for
            # us, and we can't decrypt it.
            response = ('Error: Your message has not been encrypted for '
                        'Slixfeed (MissingOwnKey).')
            omemo_decrypted = False
            retry = False
            logger.error(exn)
        except (NoAvailableSession,) as exn:
            logger.info('XmppOmemo.decrypt. except: NoAvailableSession')
            # We received a message from that contained a session that we
            # don't know about (deleted session storage, etc.). We can't
            # decrypt the message, and it's going to be lost.
            # Here, as we need to initiate a new encrypted session, it is
            # best if we send an encrypted message directly. XXX: Is it
            # where we talk about self-healing messages?
            response = ('Error: Your message has not been encrypted for '
                        'Slixfeed (NoAvailableSession).')
            omemo_decrypted = False
            retry = False
            logger.error(exn)
        except (UndecidedException, UntrustedException) as exn:
            logger.info('XmppOmemo.decrypt. except: UndecidedException')
            logger.info('XmppOmemo.decrypt. except: UntrustedException')
            # We received a message from an untrusted device. We can
            # choose to decrypt the message nonetheless, with the
            # `allow_untrusted` flag on the `decrypt_message` call, which
            # we will do here. This is only possible for decryption,
            # encryption will require us to decide if we trust the device
            # or not. Clients _should_ indicate that the message was not
            # trusted, or in undecided state, if they decide to decrypt it
            # anyway.
            response = (f'Error: Device "{exn.device}" is not present in the '
                        'trusted devices of Slixfeed.')
            omemo_decrypted = False
            retry = True
            logger.error(exn)
            # We resend, setting the `allow_untrusted` parameter to True.
            # await XmppChat.process_message(self, message, allow_untrusted=True)
        except (EncryptionPrepareException,) as exn:
            logger.info('XmppOmemo.decrypt. except: EncryptionPrepareException')
            # Slixmpp tried its best, but there were errors it couldn't
            # resolve. At this point you should have seen other exceptions
            # and given a chance to resolve them already.
            response = ('Error: Your message has not been encrypted for '
                        'Slixfeed (EncryptionPrepareException).')
            omemo_decrypted = False
            retry = False
            logger.error(exn)
        except (Exception,) as exn:
            logger.info('XmppOmemo.decrypt. except: Exception')
            response = ('Error: Your message has not been encrypted for '
                        'Slixfeed (Unknown).')
            omemo_decrypted = False
            retry = False
            logger.error(exn)
            raise

        return response, omemo_decrypted, retry

    async def _encrypt(self, jid: JID, message_body):
        logger.info(jid)
        logger.info(message_body)
        expect_problems = {}  # type: Optional[Dict[JID, List[int]]]
        while True:
            try:
                logger.info('XmppOmemo.encrypt')
                # `encrypt_message` excepts the plaintext to be sent, a list of
                # bare JIDs to encrypt to, and optionally a dict of problems to
                # expect per bare JID.
                #
                # Note that this function returns an `<encrypted/>` object,
                # and not a full Message stanza. This combined with the
                # `recipients` parameter that requires for a list of JIDs,
                # allows you to encrypt for 1:1 as well as groupchats (MUC).
                #
                # `expect_problems`: See EncryptionPrepareException handling.
                recipients = [jid]
                message_body = await self['xep_0384'].encrypt_message(
                    message_body, recipients, expect_problems)
                omemo_encrypted = True
                break
            except UndecidedException as exn:
                logger.info('XmppOmemo.encrypt. except: UndecidedException')
                # The library prevents us from sending a message to an
                # untrusted/undecided barejid, so we need to make a decision here.
                # This is where you prompt your user to ask what to do. In
                # this bot we will automatically trust undecided recipients.
                await self['xep_0384'].trust(exn.bare_jid, exn.device, exn.ik)
                omemo_encrypted = False
            # TODO: catch NoEligibleDevicesException
            except EncryptionPrepareException as exn:
                logger.info('XmppOmemo.encrypt. except: EncryptionPrepareException')
                # This exception is being raised when the library has tried
                # all it could and doesn't know what to do anymore. It
                # contains a list of exceptions that the user must resolve, or
                # explicitely ignore via `expect_problems`.
                # TODO: We might need to bail out here if errors are the same?
                for error in exn.errors:
                    if isinstance(error, MissingBundleException):
                        # We choose to ignore MissingBundleException. It seems
                        # to be somewhat accepted that it's better not to
                        # encrypt for a device if it has problems and encrypt
                        # for the rest, rather than error out. The "faulty"
                        # device won't be able to decrypt and should display a
                        # generic message. The receiving end-user at this
                        # point can bring up the issue if it happens.
                        message_body = (f'Could not find keys for device '
                                        '"{error.device}"'
                                        f' of recipient "{error.bare_jid}". '
                                        'Skipping.')
                        omemo_encrypted = False
                        jid = JID(error.bare_jid)
                        device_list = expect_problems.setdefault(jid, [])
                        device_list.append(error.device)
            except (IqError, IqTimeout) as exn:
                logger.info('XmppOmemo.encrypt. except: IqError, IqTimeout')
                message_body = ('An error occured while fetching information '
                                'on a recipient.\n%r' % exn)
                omemo_encrypted = False
            except Exception as exn:
                logger.info('XmppOmemo.encrypt. except: Exception')
                message_body = ('An error occured while attempting to encrypt'
                                '.\n%r' % exn)
                omemo_encrypted = False
                raise

        return message_body, omemo_encrypted

class StorageImpl(Storage):
    """
    Example storage implementation that stores all data in a single JSON file.
    """

    dir_data = Data.get_directory()
    omemo_dir = os.path.join(dir_data, 'omemo')
    JSON_FILE = os.path.join(omemo_dir, 'omemo.json')

    # TODO Pass JID
    #JSON_FILE = os.path.join(omemo_dir, f'{jid_bare}.json')

    def __init__(self) -> None:
        super().__init__()

        self.__data: Dict[str, JSONType] = {}
        try:
            with open(self.JSON_FILE, encoding="utf8") as f:
                self.__data = json.load(f)
        except Exception:  # pylint: disable=broad-exception-caught
            pass

    async def _load(self, key: str) -> Maybe[JSONType]:
        if key in self.__data:
            return Just(self.__data[key])

        return Nothing()

    async def _store(self, key: str, value: JSONType) -> None:
        self.__data[key] = value
        with open(self.JSON_FILE, "w", encoding="utf8") as f:
            json.dump(self.__data, f)

    async def _delete(self, key: str) -> None:
        self.__data.pop(key, None)
        with open(self.JSON_FILE, "w", encoding="utf8") as f:
            json.dump(self.__data, f)

class XEP_0384Impl(XEP_0384):  # pylint: disable=invalid-name
    """
    Example implementation of the OMEMO plugin for Slixmpp.
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:  # pylint: disable=redefined-outer-name
        super().__init__(*args, **kwargs)

        # Just the type definition here
        self.__storage: Storage

    def plugin_init(self) -> None:
        self.__storage = StorageImpl()

        super().plugin_init()

    @property
    def storage(self) -> Storage:
        return self.__storage

    @property
    def _btbv_enabled(self) -> bool:
        return True

    async def _devices_blindly_trusted(
        self,
        blindly_trusted: FrozenSet[DeviceInformation],
        identifier: Optional[str]
    ) -> None:
        logger.info(f"[{identifier}] Devices trusted blindly: {blindly_trusted}")
        #log.info(f"[{identifier}] Devices trusted blindly: {blindly_trusted}")

    async def _prompt_manual_trust(
        self,
        manually_trusted: FrozenSet[DeviceInformation],
        identifier: Optional[str]
    ) -> None:
        # Since BTBV is enabled and we don't do any manual trust adjustments in the example, this method
        # should never be called. All devices should be automatically trusted blindly by BTBV.

        # To show how a full implementation could look like, the following code will prompt for a trust
        # decision using `input`:
        session_mananger = await self.get_session_manager()

        for device in manually_trusted:
            while True:
                answer = input(f"[{identifier}] Trust the following device? (yes/no) {device}")
                if answer in { "yes", "no" }:
                    await session_mananger.set_trust(
                        device.bare_jid,
                        device.identity_key,
                        TrustLevel.TRUSTED.value if answer == "yes" else TrustLevel.DISTRUSTED.value
                    )
                    break
                print("Please answer yes or no.")

#register_plugin(XEP_0384Impl)
