# Copyright 2019 The Matrix.org Foundation CIC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import defaultdict
from queue import Empty

import attr
import dbus
import notify2
from gi.repository import GLib
from pydbus import SessionBus
from pydbus.generic import signal

from pantalaimon.log import logger
from pantalaimon.store import PanStore
from pantalaimon.thread_messages import (AcceptSasMessage, CancelSasMessage,
                                         CancelSendingMessage,
                                         ConfirmSasMessage, DaemonResponse,
                                         DeviceBlacklistMessage,
                                         DeviceUnblacklistMessage,
                                         DeviceUnverifyMessage,
                                         DeviceVerifyMessage,
                                         ExportKeysMessage, ImportKeysMessage,
                                         InviteSasSignal, SasDoneSignal,
                                         SendAnywaysMessage, ShowSasSignal,
                                         StartSasMessage,
                                         UnverifiedDevicesSignal,
                                         UpdateDevicesMessage,
                                         UpdateUsersMessage)


class IdCounter:
    def __init__(self):
        self._message_id = 0

    @property
    def message_id(self):
        ret = self._message_id
        self._message_id += 1

        return ret


class Control:
    """
    <node>
        <interface name='org.pantalaimon1.control'>
            <method name='ListServers'>
                <arg type='a{sa(ss)}' name='servers' direction='out'/>
            </method>

            <method name='ExportKeys'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='file_path' direction='in'/>
                <arg type='s' name='passphrase' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='ImportKeys'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='file_path' direction='in'/>
                <arg type='s' name='passphrase' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='SendAnyways'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='room_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='CancelSending'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='room_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <signal name="Response">
                <arg direction="out" type="i" name="id"/>
                <arg direction="out" type="s" name="pan_user"/>
                <arg direction="out" type="a{ss}" name="message"/>
            </signal>

            <signal name="UnverifiedDevices">
                <arg direction="out" type="s" name="pan_user"/>
                <arg direction="out" type="s" name="room_id"/>
                <arg direction="out" type="s" name="room_display_name"/>
            </signal>

        </interface>
    </node>
    """

    Response = signal()
    UnverifiedDevices = signal()

    def __init__(self, queue, store, server_list, id_counter):
        self.server_list = server_list
        self.store = store
        self.queue = queue
        self.id_counter = id_counter
        self.users = defaultdict(list)
        self.update_users()

    def update_users(self):
        for server in self.server_list:
            self.users[server.name] = self.store.load_users(server.name)

    @property
    def message_id(self):
        return self.id_counter.message_id

    def ListServers(self):
        """Return the list of pan users."""
        return self.users

    def ExportKeys(self, pan_user, filepath, passphrase):
        message = ExportKeysMessage(
            self.message_id,
            pan_user,
            filepath,
            passphrase
        )
        self.queue.put(message)
        return message.message_id

    def ImportKeys(self, pan_user, filepath, passphrase):
        message = ImportKeysMessage(
            self.message_id,
            pan_user,
            filepath,
            passphrase
        )
        self.queue.put(message)
        return message.message_id

    def SendAnyways(self, pan_user, room_id):
        message = SendAnywaysMessage(
            self.message_id,
            pan_user,
            room_id
        )
        self.queue.put(message)
        return message.message_id

    def CancelSending(self, pan_user, room_id):
        message = CancelSendingMessage(
            self.message_id,
            pan_user,
            room_id
        )
        self.queue.put(message)
        return message.message_id


class Devices:
    """
    <node>
        <interface name='org.pantalaimon1.devices'>
            <method name='List'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='aa{ss}' name='devices' direction='out'/>
            </method>

            <method name='ListUserDevices'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='aa{ss}' name='devices' direction='out'/>
            </method>

            <method name='Verify'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='Unverify'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='Blacklist'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='Unblacklist'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='StartKeyVerification'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='CancelKeyVerification'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='AcceptKeyVerification'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <method name='ConfirmKeyVerification'>
                <arg type='s' name='pan_user' direction='in'/>
                <arg type='s' name='user_id' direction='in'/>
                <arg type='s' name='device_id' direction='in'/>
                <arg type='u' name='id' direction='out'/>
            </method>

            <signal name="VerificationInvite">
                <arg direction="out" type="s" name="pan_user"/>
                <arg direction="out" type="s" name="user_id"/>
                <arg direction="out" type="s" name="device_id"/>
                <arg direction="out" type="s" name="transaction_id"/>
            </signal>

            <signal name="VerificationString">
                <arg direction="out" type="s" name="pan_user"/>
                <arg direction="out" type="s" name="user_id"/>
                <arg direction="out" type="s" name="device_id"/>
                <arg direction="out" type="s" name="transaction_id"/>
                <arg direction="out" type="a(ss)" name="emoji"/>
            </signal>

            <signal name="VerificationCancel">
                <arg direction="out" type="s" name="pan_user"/>
                <arg direction="out" type="s" name="user_id"/>
                <arg direction="out" type="s" name="device_id"/>
                <arg direction="out" type="s" name="transaction_id"/>
                <arg direction="out" type="s" name="reason"/>
                <arg direction="out" type="s" name="code"/>
            </signal>

            <signal name="VerificationDone">
                <arg direction="out" type="s" name="pan_user"/>
                <arg direction="out" type="s" name="user_id"/>
                <arg direction="out" type="s" name="device_id"/>
                <arg direction="out" type="s" name="transaction_id"/>
            </signal>

        </interface>
    </node>
    """

    VerificationInvite = signal()
    VerificationCancel = signal()
    VerificationString = signal()
    VerificationDone = signal()

    def __init__(self, queue, store, id_counter):
        self.store = store
        self.device_list = None
        self.queue = queue
        self.id_counter = id_counter
        self.update_devices()

    @property
    def message_id(self):
        return self.id_counter.message_id

    def List(self, pan_user):
        device_store = self.device_list.get(pan_user, None)

        if not device_store:
            return []

        device_list = [
            device for device_list in device_store.values() for device in
            device_list.values()
        ]

        return device_list

    def ListUserDevices(self, pan_user, user_id):
        device_store = self.device_list.get(pan_user, None)

        if not device_store:
            return []

        device_list = device_store.get(user_id, None)

        if not device_list:
            return []

        return device_list.values()

    def Verify(self, pan_user, user_id, device_id):
        message = DeviceVerifyMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def Unverify(self, pan_user, user_id, device_id):
        message = DeviceUnverifyMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def Blacklist(self, pan_user, user_id, device_id):
        message = DeviceBlacklistMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def Unblacklist(self, pan_user, user_id, device_id):
        message = DeviceUnblacklistMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def StartKeyVerification(self, pan_user, user_id, device_id):
        message = StartSasMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def CancelKeyVerification(self, pan_user, user_id, device_id):
        message = CancelSasMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def ConfirmKeyVerification(self, pan_user, user_id, device_id):
        message = ConfirmSasMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def AcceptKeyVerification(self, pan_user, user_id, device_id):
        message = AcceptSasMessage(
            self.message_id,
            pan_user,
            user_id,
            device_id
        )
        self.queue.put(message)
        return message.message_id

    def update_devices(self):
        self.device_list = self.store.load_all_devices()


@attr.s
class GlibT:
    receive_queue = attr.ib()
    send_queue = attr.ib()
    data_dir = attr.ib()
    server_list = attr.ib()
    config = attr.ib()

    loop = attr.ib(init=False)
    store = attr.ib(init=False)
    users = attr.ib(init=False)
    devices = attr.ib(init=False)
    bus = attr.ib(init=False)
    control_if = attr.ib(init=False)
    device_if = attr.ib(init=False)
    notifications = attr.ib(type=bool, default=False, init=False)

    def __attrs_post_init__(self):
        self.loop = None

        self.store = PanStore(self.data_dir)
        self.users = self.store.load_all_users()

        id_counter = IdCounter()

        self.control_if = Control(self.send_queue, self.store,
                                  self.server_list, id_counter)
        self.device_if = Devices(self.send_queue, self.store, id_counter)

        self.bus = SessionBus()
        self.bus.publish("org.pantalaimon1", self.control_if, self.device_if)

    def unverified_notification(self, message):
        notificaton = notify2.Notification(
            "Unverified devices.",
            message=(f"There are unverified devices in the room "
                     f"{message.room_display_name}.")
        )
        notificaton.set_category("im")

        def send_cb(notification, action_key, user_data):
            message = user_data
            self.control_if.SendAnyways(
                message.pan_user,
                message.room_id
            )

        def cancel_cb(notification, action_key, user_data):
            message = user_data
            self.control_if.CancelSending(
                message.pan_user,
                message.room_id
            )

        if "actions" in notify2.get_server_caps():
            notificaton.add_action(
                "send",
                "Send anyways",
                send_cb,
                message
            )
            notificaton.add_action(
                "cancel",
                "Cancel sending",
                cancel_cb,
                message
            )

        notificaton.show()

    def sas_invite_notification(self, message):
        notificaton = notify2.Notification(
            "Key verification invite",
            message=(f"{message.user_id} via {message.device_id} has started "
                     f"a key verification process.")
        )
        notificaton.set_category("im")

        def accept_cb(notification, action_key, user_data):
            message = user_data
            self.device_if.AcceptKeyVerification(
                message.pan_user,
                message.user_id,
                message.device_id
            )

        def cancel_cb(notification, action_key, user_data):
            message = user_data
            self.device_if.CancelKeyVerification(
                message.pan_user,
                message.user_id,
                message.device_id,
            )

        if "actions" in notify2.get_server_caps():
            notificaton.add_action(
                "accept",
                "Accept",
                accept_cb,
                message
            )
            notificaton.add_action(
                "cancel",
                "Cancel",
                cancel_cb,
                message
            )

        notificaton.show()

    def sas_show_notification(self, message):
        emojis = [x[0] for x in message.emoji]

        emoji_str = u"   ".join(emojis)

        notificaton = notify2.Notification(
            "Short authentication string",
            message=(f"Short authentication string for the key verification of"
                     f" {message.user_id} via {message.device_id}:\n"
                     f"{emoji_str}")
        )
        notificaton.set_category("im")

        def confirm_cb(notification, action_key, user_data):
            message = user_data
            self.device_if.ConfirmKeyVerification(
                message.pan_user,
                message.user_id,
                message.device_id
            )

        def cancel_cb(notification, action_key, user_data):
            message = user_data
            self.device_if.CancelKeyVerification(
                message.pan_user,
                message.user_id,
                message.device_id,
            )

        if "actions" in notify2.get_server_caps():
            notificaton.add_action(
                "confirm",
                "Confirm",
                confirm_cb,
                message
            )
            notificaton.add_action(
                "cancel",
                "Cancel",
                cancel_cb,
                message
            )

        notificaton.show()

    def sas_done_notification(self, message):
        notificaton = notify2.Notification(
            "Device successfully verified.",
            message=(f"Device {message.device_id} of user {message.user_id} "
                     f"successfully verified.")
        )
        notificaton.set_category("im")
        notificaton.show()

    def message_callback(self):
        try:
            message = self.receive_queue.get_nowait()
        except Empty:
            return True

        logger.debug(f"UI loop received message {message}")

        if isinstance(message, UpdateDevicesMessage):
            self.device_if.update_devices()

        elif isinstance(message, UpdateUsersMessage):
            self.control_if.update_users()

        elif isinstance(message, UnverifiedDevicesSignal):
            self.control_if.UnverifiedDevices(
                message.pan_user,
                message.room_id,
                message.room_display_name
            )

            if self.notifications:
                self.unverified_notification(message)

        elif isinstance(message, InviteSasSignal):
            self.device_if.VerificationInvite(
                message.pan_user,
                message.user_id,
                message.device_id,
                message.transaction_id
            )

            if self.notifications:
                self.sas_invite_notification(message)

        elif isinstance(message, ShowSasSignal):
            self.device_if.VerificationString(
                message.pan_user,
                message.user_id,
                message.device_id,
                message.transaction_id,
                message.emoji,
            )

            if self.notifications:
                self.sas_show_notification(message)

        elif isinstance(message, SasDoneSignal):
            self.device_if.VerificationDone(
                message.pan_user,
                message.user_id,
                message.device_id,
                message.transaction_id,
            )

            if self.notifications:
                self.sas_done_notification(message)

        elif isinstance(message, DaemonResponse):
            self.control_if.Response(
                message.message_id,
                message.pan_user,
                {
                    "code": message.code,
                    "message": message.message
                }
            )

        self.receive_queue.task_done()
        return True

    def run(self):
        self.loop = GLib.MainLoop()

        if self.config.notifications:
            try:
                notify2.init("pantalaimon", mainloop=self.loop)
                self.notifications = True
            except dbus.DBusException:
                logger.error("Notifications are enabled but no notification "
                             "server could be found, disabling notifications.")
                self.notifications = False

        GLib.timeout_add(100, self.message_callback)
        self.loop.run()

    def stop(self):
        if self.loop:
            self.loop.quit()
            self.loop = None
