# 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 importlib import util

UI_ENABLED = (
    util.find_spec("gi.repository") is not None and util.find_spec("pydbus") is not None
)

if UI_ENABLED:
    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,
    )

    UI_ENABLED = True

    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(set)

        def update_users(self, message):
            self.users[message.server].add((message.user_id, message.device_id))

        @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 = dict()
            self.queue = queue
            self.id_counter = id_counter

        @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, message):
            if message.pan_user not in self.device_list:
                self.device_list[message.pan_user] = defaultdict(dict)

            device_list = self.device_list.get(message.pan_user)

            for user_devices in message.devices.values():
                for device in user_devices.values():
                    if device["deleted"]:
                        try:
                            device_list[device["user_id"]].pop(device["device_id"])
                        except KeyError:
                            pass
                        continue

                    device.pop("deleted")
                    device_list[device["user_id"]][device["device_id"]] = device

    @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)

            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 = "   ".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(message)

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

            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
