# -*- coding: utf-8 -*-
import logging
import wx
import widgetUtils
import output
from .base import BaseBuffer
from controller.blueski import messages as blueski_messages
from wxUI.buffers.blueski import panels as BlueskiPanels
from sessions.blueski import compose
from mysc.thread_utils import call_threaded

log = logging.getLogger("controller.buffers.blueski.chat")

class ConversationListBuffer(BaseBuffer):
    """Buffer for listing conversations, similar to Mastodon's ConversationListBuffer."""

    def __init__(self, *args, **kwargs):
        kwargs["compose_func"] = "compose_convo"
        super(ConversationListBuffer, self).__init__(*args, **kwargs)
        self.type = "chat"
        self.sound = "dm_received.ogg"

    def create_buffer(self, parent, name):
        self.buffer = BlueskiPanels.ChatPanel(parent, name)
        self.buffer.session = self.session

    def bind_events(self):
        """Bind events like Mastodon's ConversationListBuffer."""
        self.buffer.set_focus_function(self.onFocus)
        widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
        widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
        widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
        # Buttons
        if hasattr(self.buffer, "post"):
            widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.on_post, self.buffer.post)
        if hasattr(self.buffer, "reply"):
            widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply)
        if hasattr(self.buffer, "new_chat"):
            widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.on_new_chat, self.buffer.new_chat)

    def get_item(self):
        """Get the last message from the selected conversation (like Mastodon)."""
        index = self.buffer.list.get_selected()
        if index > -1 and self.session.db.get(self.name) is not None and len(self.session.db[self.name]) > index:
            convo = self.session.db[self.name][index]
            # Return lastMessage for compatibility with item-based operations
            last_msg = getattr(convo, "lastMessage", None) or (convo.get("lastMessage") if isinstance(convo, dict) else None)
            return last_msg
        return None

    def get_conversation(self):
        """Get the full conversation object."""
        index = self.buffer.list.get_selected()
        if index > -1 and self.session.db.get(self.name) is not None and len(self.session.db[self.name]) > index:
            return self.session.db[self.name][index]
        return None

    def get_convo_id(self, conversation):
        """Extract conversation ID from a conversation object, handling different field names."""
        if not conversation:
            return None
        # Try different possible field names for the conversation ID
        for attr in ("id", "convo_id", "convoId"):
            val = getattr(conversation, attr, None)
            if val:
                return val
            if isinstance(conversation, dict) and attr in conversation:
                return conversation[attr]
        return None

    def _get_members_key(self, conversation):
        """Fallback key when convo id is missing: stable member DID list."""
        members = getattr(conversation, "members", None) or (conversation.get("members") if isinstance(conversation, dict) else None) or []
        dids = []
        for m in members:
            did = getattr(m, "did", None) or (m.get("did") if isinstance(m, dict) else None)
            if did:
                dids.append(did)
        if not dids:
            return None
        dids.sort()
        return tuple(dids)

    def _get_last_message_key(self, conversation):
        """Key for detecting conversation updates based on last message."""
        last_msg = getattr(conversation, "lastMessage", None) or (conversation.get("lastMessage") if isinstance(conversation, dict) else None)
        if not last_msg:
            return None

        def g(obj, key, default=None):
            if isinstance(obj, dict):
                return obj.get(key, default)
            return getattr(obj, key, default)

        for attr in ("id", "messageId", "message_id", "msgId", "msg_id", "cid", "rev"):
            val = g(last_msg, attr)
            if val:
                return str(val)

        nested = g(last_msg, "message") or g(last_msg, "record")
        if nested:
            for attr in ("id", "messageId", "message_id", "msgId", "msg_id", "cid", "rev"):
                val = g(nested, attr)
                if val:
                    return str(val)

        sent_at = g(last_msg, "sentAt") or g(last_msg, "sent_at") or g(last_msg, "createdAt") or g(last_msg, "created_at")
        sender = g(last_msg, "sender") or (g(nested, "sender") if nested else {}) or {}
        sender_did = g(sender, "did")
        text = g(last_msg, "text") or (g(nested, "text") if nested else None)
        if sent_at or sender_did or text:
            return (str(sent_at) if sent_at else None, str(sender_did) if sender_did else None, str(text) if text else None)
        return None

    def get_formatted_message(self):
        """Return last message text for current conversation."""
        conversation = self.get_conversation()
        if not conversation:
            return None
        return self.compose_function(
            conversation,
            self.session.db,
            self.session.settings,
            self.session.settings["general"].get("relative_times", False),
            self.session.settings["general"].get("show_screen_names", False),
        )[1]

    def get_message(self):
        """Return a readable summary for the selected conversation."""
        conversation = self.get_conversation()
        if not conversation:
            return None
        composed = self.compose_function(
            conversation,
            self.session.db,
            self.session.settings,
            self.session.settings["general"].get("relative_times", False),
            self.session.settings["general"].get("show_screen_names", False),
        )
        return " ".join(composed)

    def on_new_chat(self, *args, **kwargs):
        """Start a new conversation by entering a handle."""
        dlg = wx.TextEntryDialog(None, _("Enter the handle of the user (e.g., user.bsky.social):"), _("New Chat"))
        if dlg.ShowModal() == wx.ID_OK:
            handle = dlg.GetValue().strip()
            if handle:
                if handle.startswith("@"):
                    handle = handle[1:]
                def do_create():
                    try:
                        # Resolve handle to DID
                        profile = self.session.get_profile(handle)
                        if not profile:
                            wx.CallAfter(output.speak, _("User not found."), True)
                            return
                        did = getattr(profile, "did", None) or (profile.get("did") if isinstance(profile, dict) else None)
                        if not did:
                            wx.CallAfter(output.speak, _("Could not get user ID."), True)
                            return
                        # Get or create conversation
                        convo = self.session.get_or_create_convo([did])
                        if not convo:
                            wx.CallAfter(output.speak, _("Could not create conversation."), True)
                            return
                        convo_id = self.get_convo_id(convo)
                        user_handle = getattr(profile, "handle", None) or (profile.get("handle") if isinstance(profile, dict) else None) or handle
                        title = _("Chat: {0}").format(user_handle)
                        # Create the buffer under direct_messages node
                        wx.CallAfter(self._create_chat_buffer, self.controller, title, convo_id)
                        # Refresh conversation list
                        wx.CallAfter(self.start_stream, True, False)
                    except Exception:
                        log.exception("Error creating new conversation")
                        wx.CallAfter(output.speak, _("Error creating conversation."), True)
                call_threaded(do_create)
        dlg.Destroy()

    def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
        count = self.get_max_items()
        try:
            res = self.session.list_convos(limit=count)
            items = res.get("items", [])
            self._build_member_maps(items)
            return self._merge_conversations(items, play_sound, avoid_autoreading=avoid_autoreading)
        except Exception:
            log.exception("Error fetching conversations")
            output.speak(_("Error loading conversations."), True)
            return 0

    def _build_member_maps(self, convos):
        """Build DID→name maps from conversation members and store in db for chat buffers."""
        def g(obj, key, default=None):
            if isinstance(obj, dict):
                return obj.get(key, default)
            return getattr(obj, key, default)
        for convo in convos:
            convo_id = self.get_convo_id(convo)
            if not convo_id:
                continue
            members = g(convo, "members", []) or []
            member_map = {}
            for m in members:
                did = g(m, "did", None)
                if did:
                    name = g(m, "display_name") or g(m, "displayName") or g(m, "handle", "unknown")
                    member_map[did] = name
            if member_map:
                self.session.db["convo_" + str(convo_id) + "_members"] = member_map

    def _merge_conversations(self, items, play_sound=True, avoid_autoreading=False):
        """Merge conversation list, updating items without duplicating or re-alerting."""
        if self.session.db.get(self.name) is None:
            self.session.db[self.name] = []

        # Track current selection to restore after refresh
        selected_key = None
        current_convo = self.get_conversation()
        if current_convo:
            selected_key = self.get_convo_id(current_convo) or self._get_members_key(current_convo)

        existing = {}
        existing_last = {}
        for convo in self.session.db[self.name]:
            key = self.get_convo_id(convo) or self._get_members_key(convo)
            if key is None:
                continue
            existing[key] = convo
            existing_last[key] = self._get_last_message_key(convo)

        new_db = []
        new_count = 0
        first_load = len(self.session.db[self.name]) == 0
        for convo in items:
            key = self.get_convo_id(convo) or self._get_members_key(convo)
            new_db.append(convo)
            if key is None:
                if first_load:
                    new_count += 1
                continue
            if key not in existing:
                new_count += 1
                continue
            if self._get_last_message_key(convo) != existing_last.get(key):
                new_count += 1

        # Replace DB with latest ordered list from API
        self.session.db[self.name] = new_db

        # Rebuild list UI to keep ordering consistent with API
        self.buffer.list.clear()
        safe = True
        relative_times = self.session.settings["general"].get("relative_times", False)
        show_screen_names = self.session.settings["general"].get("show_screen_names", False)
        for convo in new_db:
            row = self.compose_function(convo, self.session.db, self.session.settings, relative_times, show_screen_names, safe=safe)
            self.buffer.list.insert_item(False, *row)

        # Restore selection if possible
        if selected_key is not None:
            for idx, convo in enumerate(new_db):
                key = self.get_convo_id(convo) or self._get_members_key(convo)
                if key == selected_key:
                    self.buffer.list.select_item(idx)
                    break

        # Sound and auto-read only when something actually changed
        if new_count > 0:
            if play_sound and self.sound and not self.session.settings["sound"].get("session_mute", False):
                self.session.sound.play(self.sound)
            if not avoid_autoreading:
                self.auto_read(new_count)

        return new_count

    def fav(self):
        pass

    def unfav(self):
        pass

    def can_share(self):
        return False

    def url(self, *args, **kwargs):
        """Enter key opens the chat conversation buffer."""
        self.view_chat()

    def send_message(self, *args, **kwargs):
        """Global DM shortcut - reply to conversation."""
        return self.reply()

    def reply(self, *args, **kwargs):
        """Reply to the selected conversation (like Mastodon)."""
        conversation = self.get_conversation()
        if not conversation:
            output.speak(_("No conversation selected."), True)
            return

        convo_id = self.get_convo_id(conversation)
        if not convo_id:
            log.error("Could not get conversation ID from conversation object")
            output.speak(_("Could not identify conversation."), True)
            return

        # Get participants for title
        members = getattr(conversation, "members", []) or (conversation.get("members", []) if isinstance(conversation, dict) else [])
        user_did = self.session.db.get("user_id")
        others = [m for m in members if (getattr(m, "did", None) or (m.get("did") if isinstance(m, dict) else None)) != user_did]
        if not others:
            others = members

        if others:
            first_user = others[0]
            username = getattr(first_user, "handle", None) or (first_user.get("handle") if isinstance(first_user, dict) else None) or "unknown"
        else:
            username = "unknown"

        title = _("Conversation with {0}").format(username)
        caption = _("Write your message here")
        initial_text = "@{} ".format(username)

        post = blueski_messages.post(session=self.session, title=title, caption=caption, text=initial_text)
        if post.message.ShowModal() == wx.ID_OK:
            text, files, cw_text, langs = post.get_data()
            if text:
                def do_send():
                    try:
                        self.session.send_chat_message(convo_id, text)
                        wx.CallAfter(self.session.sound.play, "dm_sent.ogg")
                        wx.CallAfter(output.speak, _("Message sent."))
                        wx.CallAfter(self.start_stream, True, False)
                    except Exception:
                        log.exception("Error sending message")
                        wx.CallAfter(output.speak, _("Failed to send message."), True)
                call_threaded(do_send)
        if hasattr(post.message, "Destroy"):
            post.message.Destroy()

    def view_chat(self):
        """Open the conversation in a separate buffer (nested under Chats node)."""
        conversation = self.get_conversation()
        if not conversation:
            output.speak(_("No conversation selected."), True)
            return

        convo_id = self.get_convo_id(conversation)
        if not convo_id:
            log.error("Could not get conversation ID from conversation object: %r", conversation)
            output.speak(_("Could not identify conversation."), True)
            return

        # Determine participants names for title
        members = getattr(conversation, "members", []) or (conversation.get("members", []) if isinstance(conversation, dict) else [])
        user_did = self.session.db.get("user_id")
        others = [m for m in members if (getattr(m, "did", None) or (m.get("did") if isinstance(m, dict) else None)) != user_did]
        if not others:
            others = members
        names = ", ".join([getattr(m, "handle", None) or (m.get("handle") if isinstance(m, dict) else None) or "unknown" for m in others])

        title = _("Chat: {0}").format(names)

        self._create_chat_buffer(self.controller, title, convo_id)

    def _create_chat_buffer(self, controller, title, convo_id):
        """Create a chat buffer under the direct_messages node, avoiding duplicates."""
        account_name = self.session.get_name()

        # Avoid duplicates: if buffer already exists, navigate to it
        existing = controller.search_buffer(title, account_name)
        if existing:
            index = controller.view.search(title, account_name)
            if index is not None:
                controller.view.change_buffer(index)
            return

        # Insert under direct_messages node (like Mastodon's ConversationBuffer)
        chats_position = controller.view.search("direct_messages", account_name)
        if chats_position is None:
            chats_position = controller.view.search(account_name, account_name)

        controller.create_buffer(
            buffer_type="chat_messages",
            session_type="blueski",
            buffer_title=title,
            parent_tab=chats_position,
            kwargs={"session": self.session, "convo_id": convo_id, "name": title},
            start=True
        )

        # Navigate to the newly created buffer and announce it
        new_index = controller.view.search(title, account_name)
        if new_index is not None:
            controller.view.change_buffer(new_index)
            buffer_obj = controller.search_buffer(title, account_name)
            if buffer_obj and hasattr(buffer_obj.buffer, "list"):
                try:
                    count = buffer_obj.buffer.list.get_count()
                    if count > 0:
                        msg = _("{0}, {1} of {2}").format(title, buffer_obj.buffer.list.get_selected()+1, count)
                    else:
                        msg = _("{0}. Empty").format(title)
                except Exception:
                    msg = _("{0}. Empty").format(title)
                output.speak(msg, True)

    def destroy_status(self):
        pass

class ChatBuffer(BaseBuffer):
    """Buffer for displaying messages in a conversation, similar to Mastodon's ConversationBuffer."""

    def __init__(self, *args, **kwargs):
        kwargs["compose_func"] = "compose_chat_message"
        super(ChatBuffer, self).__init__(*args, **kwargs)
        self.type = "chat_messages"
        self.convo_id = kwargs.get("convo_id")
        self.sound = "dm_received.ogg"
        self._member_map_loaded = False

    def create_buffer(self, parent, name):
        self.buffer = BlueskiPanels.ChatMessagePanel(parent, name)
        self.buffer.session = self.session

    def _update_member_map(self):
        """Fetch conversation members to build a DID-to-name map for sender resolution."""
        try:
            convo = self.session.get_convo(self.convo_id)
            if not convo:
                return
            member_map = {}
            for m in getattr(convo, "members", []) or []:
                did = getattr(m, "did", None)
                if did:
                    name = getattr(m, "display_name", None) or getattr(m, "handle", None) or "unknown"
                    member_map[did] = name
            self.session.db[self.name + "_members"] = member_map
        except Exception:
            log.exception("Error fetching conversation members for DID resolution")

    def start_stream(self, mandatory=False, play_sound=True):
        if not self.convo_id:
            return 0
        if not self._member_map_loaded:
            self._update_member_map()
            self._member_map_loaded = True
        count = self.get_max_items()
        try:
            res = self.session.get_convo_messages(self.convo_id, limit=count)
            items = res.get("items", [])
            items = list(reversed(items))
            return self.process_items(items, play_sound)
        except Exception:
            log.exception("Error fetching chat messages")
            return 0

    def get_more_items(self):
        output.speak(_("This action is not supported for this buffer"), True)

    def fav(self):
        pass

    def unfav(self):
        pass

    def can_share(self):
        return False

    def destroy_status(self):
        pass

    def url(self, *args, **kwargs):
        """Enter key opens reply dialog in chat."""
        self.on_reply(None)

    def on_reply(self, evt):
        """Open dialog to send a message in this conversation."""
        if not self.convo_id:
            output.speak(_("Cannot send message: no conversation selected."), True)
            return

        # Get conversation title from buffer name or use generic
        title = _("Send Message")
        if self.name and self.name.startswith(_("Chat: ")):
            title = self.name
        caption = _("Write your message here")

        post = blueski_messages.post(session=self.session, title=title, caption=caption, text="")
        if post.message.ShowModal() == wx.ID_OK:
            text, files, cw_text, langs = post.get_data()
            if text:
                def do_send():
                    try:
                        self.session.send_chat_message(self.convo_id, text)
                        wx.CallAfter(self.session.sound.play, "dm_sent.ogg")
                        wx.CallAfter(output.speak, _("Message sent."))
                        wx.CallAfter(self.start_stream, True, False)
                    except Exception:
                        log.exception("Error sending chat message")
                        wx.CallAfter(output.speak, _("Failed to send message."), True)
                call_threaded(do_send)
        if hasattr(post.message, "Destroy"):
            post.message.Destroy()

    def reply(self, *args, **kwargs):
        """Handle reply action (from menu or keyboard shortcut)."""
        self.on_reply(None)

    def send_message(self, *args, **kwargs):
        """Global shortcut for DM."""
        self.on_reply(None)

    def get_message(self):
        """Return a readable summary for the selected chat message."""
        item = self.get_item()
        if item is None:
            return None
        composed = self.compose_function(
            item, self.session.db, self.session.settings,
            self.session.settings["general"].get("relative_times", False),
            self.session.settings["general"].get("show_screen_names", False),
        )
        return " ".join(composed)

    def get_formatted_message(self):
        """Return the text content of the selected chat message."""
        item = self.get_item()
        if item is None:
            return None
        composed = self.compose_function(
            item, self.session.db, self.session.settings,
            self.session.settings["general"].get("relative_times", False),
            self.session.settings["general"].get("show_screen_names", False),
        )
        return composed[1]

    def view_item(self, item=None):
        """View the selected chat message in a dialog."""
        msg = self.get_formatted_message()
        if not msg:
            output.speak(_("No message selected."), True)
            return
        viewer = blueski_messages.text(title=_("Chat message"), text=msg)
        viewer.message.ShowModal()
        viewer.message.Destroy()

    def remove_buffer(self, force=False):
        """Allow removing this buffer."""
        from wxUI import commonMessageDialogs
        if force == False:
            dlg = commonMessageDialogs.remove_buffer()
        else:
            dlg = widgetUtils.YES
        if dlg == widgetUtils.YES:
            if self.name in self.session.db:
                self.session.db.pop(self.name)
            return True
        elif dlg == widgetUtils.NO:
            return False
