# channels_page.py
#
# Copyright 2024 Christopher Talbot
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

import json
import base64
from gi.repository import Adw
from gi.repository import Gtk
from datetime import datetime, timezone
import meshtastic
from meshtastic import BROADCAST_NUM

import gtk_meshtastic_client.channel_row as channel_row
import gtk_meshtastic_client.message_row as message_row
import gtk_meshtastic_client.node_row as node_row
import gtk_meshtastic_client.chat_page as chat_page
import gtk_meshtastic_client.utils as utils
import gtk_meshtastic_client.notification as notification

@Gtk.Template(resource_path='/org/kop316/meshtastic/ui/channels_page.ui')
class ChannelsPageBin(Adw.BreakpointBin):
    __gtype_name__ = 'ChannelsPageBin'

    channel_list_box = Gtk.Template.Child()
    chat_view = Gtk.Template.Child()
    dm_list_box = Gtk.Template.Child()
    empty_chat_view = Gtk.Template.Child()
    split_view = Gtk.Template.Child()

    channel_list_box_children = 0
    dm_list_box_children = 0

    def _sort_func(self, channel1, channel2, data=None):
            return channel1.index - channel2.index

    def find_channel_by_id(self, channel_id):
        for x in range(self.channel_list_box_children):
            row_to_test = self.channel_list_box.get_row_at_index(x)
            if channel_id == row_to_test.channel_id:
                return row_to_test

    def find_channel_by_channel_number(self, channel_index):
        for x in range(self.channel_list_box_children):
            row_to_test = self.channel_list_box.get_row_at_index(x)
            if channel_index == row_to_test.index:
                return row_to_test

        return

    def find_dm_by_id(self, num):
        for x in range(self.dm_list_box_children):
            row_to_test = self.dm_list_box.get_row_at_index(x)
            if num == row_to_test.channel_id:
                return row_to_test

        return

    """
    This is an Ack for Channels
    """
    def process_ack_message(self, packet, interface):
        if 'decoded' in packet:
            app = Gtk.Application.get_default()
            win = Gtk.Application.get_active_window(app)
            """
            If the packet is for Channel 0, it is not shown in the packet
            """
            if 'channel' in packet:
                channel = packet["channel"]
            else:
                channel = 0

            if packet.get('decoded', {}).get('requestId'):
                request_id = packet['decoded'].get('requestId')
            else:
               self.logger.warning("did not find request id")
               return

            channel_id = win.connection_page_bin.database.update_to_delivered_in_database(request_id)

            channel_row = self.find_channel_by_id(channel_id)
            if not channel_row:
                channel_row = self.find_dm_by_id(channel_id)
                """
                From what I see, Priority ACKs can come from any device, and when
                it comes to the python library, the "to" and "from" attributes are
                the same. These ACKs can be for channels (which are broadcasted),
                or for DMs.

                The Routing message without an ACK looks to be DM specific,
                and it comes from the recipient.

                Priority ACKs are fine for broadcast channels, but in a DM,
                we want to make sure it actually reaches the recipient. Thus,
                we need to check the "from" attribute to make sure.

                TODO: I don't know if you will get the routing message if the
                      meessage has to hop.
                """
                if channel_row:
                    if channel_row.channel_id != packet["from"]:
                        self.logger.warning("ACK Message is for DM, but not from the intended recipient")
                        return
            if channel_row:
                channel_row.nav_page.update_msg_delivered_by_id(request_id)

        else:
            self.logger.warning("error: Decoded not in ACK packet")

    """
    This is an Ack for DMs
    """
    def process_routing_app_message(self, packet, interface):
        if 'decoded' in packet:
            app = Gtk.Application.get_default()
            win = Gtk.Application.get_active_window(app)
            if packet.get('decoded', {}).get('requestId'):
                request_id = packet['decoded'].get('requestId')
            else:
               self.logger.warning("did not find request id")
               return

            channel_id = win.connection_page_bin.database.update_to_delivered_in_database(request_id)

            channel_row = self.find_channel_by_id(channel_id)
            if not channel_row:
                channel_row = self.find_dm_by_id(channel_id)
                """
                From what I see, Priority ACKs can come from any device, and when
                it comes to the python library, the "to" and "from" attributes are
                the same. These ACKs can be for channels (which are broadcasted),
                or for DMs.

                The Routing message without an ACK looks to be DM specific,
                and it comes from the recipient.

                Priority ACKs are fine for broadcast channels, but in a DM,
                we want to make sure it actually reaches the recipient. Thus,
                we need to check the "from" attribute to make sure.

                TODO: I don't know if you will get the routing message if the
                      meessage has to hop.
                """
                if channel_row:
                    if channel_row.channel_id != packet["from"]:
                        self.logger.warning("ACK Message is for DM, but not from the intended recipient")
                        return
            if channel_row:
                channel_row.nav_page.update_msg_delivered_by_id(request_id)

        else:
            self.logger.warning("error: Decoded not in ACK packet")

    def new_text_message(self, packet, interface):
        if 'decoded' in packet:
            app = Gtk.Application.get_default()
            win = Gtk.Application.get_active_window(app)

            short_name = "???"
            long_name = utils.idToHex(packet["from"])
            channel_id = "aa"

            chat_row = message_row.MessageRow()
            """
            This message is coming from the outside
            """
            chat_row.set_message_direction(utils.MsgDirection.In.value)
            chat_row.set_message_id(packet["id"])

            node_to_parse = win.nearby_nodes_page_bin.find_node_by_id(packet['from'])
            if node_to_parse:
                chat_row.set_short_name(node_to_parse.shortName)
                chat_row.set_long_name(node_to_parse.longName)
                short_name = node_to_parse.shortName
                long_name = node_to_parse.longName
            else:
                chat_row.set_long_name(utils.idToHex(packet["from"]))

            """
            If there's no date, just set to now
            """
            if 'rxTime' in packet:
                chat_row.set_date(packet["rxTime"])

            if chat_row.date == 0:
                unix_timestamp = int(datetime.now(timezone.utc).timestamp())
                chat_row.set_date(unix_timestamp)

            if packet.get("decoded", {}).get("text"):
                chat_row.set_message_content(packet["decoded"]["text"])

            #Set if the message is a DM or not
            if packet['to'] == meshtastic.BROADCAST_NUM:
                chat_row.set_direct_message(False)
            else:
                chat_row.set_direct_message(True)

            #pki_encrypted
            if 'pkiEncrypted' in packet and 'publicKey' in packet:
                if packet["pkiEncrypted"]:
                    chat_row.update_pki_encryption(True)
            else:
                chat_row.update_pki_encryption(False)

            """
            Broadcast means it goes in a channel
            """
            if packet['to'] == meshtastic.BROADCAST_NUM:
                """
                If the packet is for Channel 0, it is not shown in the packet
                """
                if 'channel' in packet:
                    chat_row.channel = packet["channel"]
                else:
                    chat_row.channel = 0

                channel_row = self.find_channel_by_channel_number(chat_row.channel)
                if channel_row:
                    """
                    If we are in the chat, don't mark messages as unread
                    """
                    visible_child_name = self.chat_view.get_visible_child_name()
                    if not visible_child_name:
                        visible_child_name = ""
                    if visible_child_name != channel_row.title:
                        channel_row.unread_messages_image.set_visible(True)

                    channel_id = channel_row.channel_id
                    channel_row.nav_page.add_message(chat_row)
                    notification._new_message_notification(channel_row.title, chat_row.message_content, long_name)
                    win.connection_page_bin.database.add_received_message_to_database(packet, interface, short_name, long_name, channel_id, chat_row.pki_encryption)
                else:
                    self.logger.warning("Couldn't find chat page!")

            else:
                channel_row = self.find_dm_by_id(packet["from"])
                if not channel_row:
                    if node_to_parse:
                        node_to_parse.create_direct_message_internal(False)
                        channel_row = self.find_dm_by_id(packet["from"])
                    else:
                        node_row.create_direct_message(packet["from"], None, None, "MA==", False, True)
                        channel_row = self.find_dm_by_id(packet["from"])

                if channel_row:
                    """
                    If we are in the chat, don't mark messages as unread
                    """
                    visible_child_name = self.chat_view.get_visible_child_name()
                    if not visible_child_name:
                        visible_child_name = ""
                    if visible_child_name != channel_row.title:
                        channel_row.unread_messages_image.set_visible(True)

                    channel_id = channel_row.channel_id
                    channel_row.nav_page.add_message(chat_row)

                    if chat_row.pki_encryption == True:
                        self.logger.debug("Packet Public key: " + packet["publicKey"])
                        self.logger.debug("Node Public key:   " + channel_row.nav_page.publicKey)
                        if channel_row.nav_page.publicKey == "MA==":
                            self.logger.warning("Message has encryption but there's no public key in the database!")
                            chat_row.update_pki_encryption(False)
                        elif packet["publicKey"] != channel_row.nav_page.publicKey:
                            self.logger.warning("public key in message does not match public key in node database!")
                            chat_row.update_pki_encryption(False)

                            app = Gtk.Application.get_default()
                            win = Gtk.Application.get_active_window(app)

                            dialog = Adw.AlertDialog.new(_("Alert"),
                                                         _("Latest message from " + channel_row.nav_page.title + " has a mismatch "
                                                           "between the message public key & node public key.\n"
                                                           "Message Public Key:\n" + packet["publicKey"] + "\n"
                                                           "Node Public Key:\n" + channel_row.nav_page.publicKey) + "\n"
                                                           "Someone may be trying to impersonate " + channel_row.nav_page.title)
                            dialog.add_response ("close",  _("_Close"))
                            dialog.present (win)
                        else:
                            self.logger.debug("Public key in message matches public key in node database")

                    notification._new_message_notification(channel_row.title, chat_row.message_content, long_name)
                    win.connection_page_bin.database.add_received_message_to_database(packet, interface, short_name, long_name, channel_id, chat_row.pki_encryption)
                else:
                    self.logger.warning("could not process direct message!")

        else:
            self.logger.warning("error: Decoded not in packet received packet")

    def remove_dm_row(self, dm_id):
        channel_row = self.find_dm_by_id(dm_id)
        if channel_row:
            self.chat_view.remove(channel_row.nav_page)
            self.dm_list_box.remove(channel_row)
            self.dm_list_box_children -= 1

    def reset_all_channels_and_dms(self):
        self.chat_view.set_visible_child(self.empty_chat_view)

        for x in range(self.channel_list_box_children):
            row = self.channel_list_box.get_row_at_index(x)
            self.chat_view.remove(row.nav_page)

        self.split_view.set_show_content (False)

        for x in range(self.dm_list_box_children):
            row = self.dm_list_box.get_row_at_index(x)
            self.chat_view.remove(row.nav_page)

        """
        Remove all doesn't put placeholder back
        """
        for x in range(self.channel_list_box_children):
            row = self.channel_list_box.get_row_at_index(0)
            if row:
                self.channel_list_box.remove(row)

        self.channel_list_box_children = 0

        """
        Remove all doesn't put placeholder back
        """
        for x in range(self.dm_list_box_children):
            row = self.dm_list_box.get_row_at_index(0)
            if row:
                self.dm_list_box.remove(row)

        self.dm_list_box_children = 0

    def populate_all_channels_and_dms(self, interface):
        app = Gtk.Application.get_default()
        win = Gtk.Application.get_active_window(app)
        node = interface.getNode('^local')
        channels = node.channels

        if channels:
            for channel in channels:
                if channel.role:
                    psk_base64 = base64.b64encode(channel.settings.psk).decode('utf-8')
                    new_channel = channel_row.ChannelRow()
                    new_channel.set_index(channel.index)
                    new_channel.set_role(channel.role)
                    new_channel.set_channel_title(channel.settings.name, psk_base64)
                    new_channel.set_channel_id(channel.settings.name, psk_base64)

                    """
                    Since we added a sort function, the rows will automatically sort when
                    appended, no need to manually specifiy
                    """
                    self.channel_list_box.append(new_channel)
                    self.channel_list_box_children += 1

                    new_nav_page = chat_page.ChatNavPage()
                    new_nav_page.set_chat_title(new_channel.title, False)
                    new_nav_page.set_channel_id(new_channel.title, psk_base64)
                    new_nav_page.set_chat_index(new_channel.index)
                    new_nav_page.set_interface(interface)
                    self.chat_view.add_named(new_nav_page, new_channel.title)

                    new_channel.set_nav_page(new_nav_page)

        win.connection_page_bin.database.populate_direct_messages(interface)

    def activate_row(self, list_box, row):
        """
        HACK:   row-activated does not work in python? so I have to use
                selected instead. However, for it to work like activated,
                I unselect everything. But when things are unselected,
                this function also gets called. So if there is no row,
                that's when everything is unsleected.
        """
        if not row:
            return

        self.logger.debug("Opening Channel: " + str(row.title))
        self.split_view.set_show_content (True)
        row.unread_messages_image.set_visible(False)

        self.chat_view.set_visible_child(row.nav_page)

        row.nav_page.update_ui()
        row.nav_page.message_bar_grab_focus()

        list_box.unselect_all()

    @Gtk.Template.Callback()
    def _channel_list_row_activated_cb(self, list_box, row):
        self.activate_row(list_box, row)

    @Gtk.Template.Callback()
    def _dm_list_row_activated_cb(self, list_box, row):
        self.activate_row(list_box, row)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        app = Gtk.Application.get_default()

        self.channel_list_box.set_sort_func(sort_func=self._sort_func)
        self.logger = app.logger
