"""InputMixin — key handling and command routing for the kirbus UI."""
from __future__ import annotations

import curses
import threading

from kirbus.ui.models import SCRATCH_PEER, BACK_ENTRY, RESERVED_CHANNELS
from kirbus.ui.theme import list_themes, set_theme


class InputMixin:
    """Key handling and slash-command routing mixed into UI.

    Relies on state and helpers provided by UI and DrawMixin:
        self.focus, self.peer_cursor, self.peers, self.history,
        self.history_idx, self.history_draft, self.input_buf, self.cursor,
        self.scroll, self.active_peer, self.view, self.channels, self.unread,
        self.theme, self.handle, self.inbox, self.outbox, self.messages,
        self.chat_h, self._system(), self._error(), self._chat(),
        self._presence_rows(), self._resize()
    """

    _HELP = """\
/help                        show this message
/theme <name>                switch theme
/themes                      list available themes
/ai <prompt>                 ask the AI; response sent to current conversation
/ai-peer                     forward the last peer message to the AI
/accept [peer]               accept a new or key-changed peer (default: current peer)
/block [peer]                mark a peer as blocked (⊘)
/unblock [peer]              remove blocked mark from a peer
/servers                     refresh server list from registry
/connect <name>              connect to a server by name
/disconnect                  leave current server, return to server list
/clear                       clear chat history
/quit  (or /q)               exit kirbus
/channel create <name>       create a new channel
/channel join <name>         join an existing channel
/channel invite <peer> [ch]  invite a peer (channel inferred if inside one)
/channel leave <name>        leave a channel
---
Tab                 focus peer list  (↑/↓ navigate, Enter select, Esc cancel)
↑ / ↓              command history (when input focused)
PgUp / PgDn        scroll chat"""

    _SU_HELP = """\
/kick <peer>                 disconnect a peer from the server
/ban <peer>                  kick + revoke pubkey from allowlist
/unban <peer>                re-add a previously banned peer
/allow <peer>                pre-approve a pubkey
/who                         list connected peers with details
/server-password <new>       rotate the server password
/server-mode <mode>          switch auth mode (open/password/allowlist)"""

    # ------------------------------------------------------------------
    # Input helpers
    # ------------------------------------------------------------------
    def _set_input(self, text: str) -> None:
        self.input_buf = list(text)
        self.cursor    = len(self.input_buf)

    # ------------------------------------------------------------------
    # Command routing
    # ------------------------------------------------------------------
    def _handle_command(self, raw: str) -> None:
        parts = raw.strip().split(maxsplit=1)
        cmd   = parts[0].lower()
        arg   = parts[1] if len(parts) > 1 else ""

        if cmd in ("/quit", "/q", "/exit"):
            raise SystemExit(0)

        elif cmd == "/help":
            for line in self._HELP.splitlines():
                self._system(line)
            if getattr(self, "is_su", False):
                self._system("--- su commands ---")
                for line in self._SU_HELP.splitlines():
                    self._system(line)

        elif cmd == "/themes":
            self._system("Available themes: " + "  ".join(list_themes()))

        elif cmd == "/theme":
            if not arg:
                self._error("Usage: /theme <name>")
                return
            try:
                self.theme = set_theme(arg)
                self.theme.activate()
                self._system(f"Theme changed to {self.theme.name}")
            except ValueError as exc:
                self._error(str(exc))

        elif cmd == "/channel":
            self._handle_channel_command(arg)

        elif cmd == "/accept":
            peer = arg.strip().lstrip("@") or self.active_peer
            if not peer:
                self._error("Usage: /accept <peer>  (or select a peer first)")
                return
            status = self.peer_key_status.get(peer)
            if status in ("new", "changed"):
                self.peer_key_status.pop(peer, None)
                self._system(f"Accepted {peer}")
            else:
                self._error(f"{peer} has no pending key alert")

        elif cmd == "/block":
            peer = arg.strip().lstrip("@") or self.active_peer
            if not peer:
                self._error("Usage: /block <peer>  (or select a peer first)")
                return
            self.blocked_peers.add(peer)
            from kirbus.store.peers import set_peer_blocked
            set_peer_blocked(peer, True)
            self._system(f"Blocked {peer}")

        elif cmd == "/unblock":
            peer = arg.strip().lstrip("@") or self.active_peer
            if not peer:
                self._error("Usage: /unblock <peer>  (or select a peer first)")
                return
            self.blocked_peers.discard(peer)
            from kirbus.store.peers import set_peer_blocked
            set_peer_blocked(peer, False)
            self._system(f"Unblocked {peer}")

        elif cmd == "/disconnect":
            self.outbox.put(("__disconnect__", "", ""))
            self.connected_server = ""
            self.is_su = False
            self.peers.clear()
            self.active_peer = ""
            self.view = "top"
            self._system("Disconnected from server")

        elif cmd == "/servers":
            self.outbox.put(("__refresh_servers__", "", ""))
            self._system("Refreshing server list...")

        elif cmd == "/connect":
            parts = arg.strip().split(maxsplit=1)
            if not parts:
                self._error("Usage: /connect <server-name> [password]")
                return
            name = parts[0]
            password = parts[1] if len(parts) > 1 else ""
            self.outbox.put(("__select_server__", name, password))

        elif cmd in ("/kick", "/ban", "/unban", "/who",
                      "/server-password", "/server-mode", "/allow"):
            if not getattr(self, "is_su", False):
                self._error("Admin commands require su access (--su on localhost)")
                return
            self.outbox.put(("__admin_cmd__", cmd, arg))

        elif cmd == "/clear":
            self.messages.clear()
            self.scroll = 0

        elif cmd == "/ai-peer":
            channel    = self.view if self.view != "top" else ""
            convo      = f"#{channel}" if channel else self.active_peer
            peer_name  = self.active_peer if not channel else None
            # Find the last message from someone other than us in this conversation
            last = next(
                (m for m in reversed(self.messages)
                 if m.peer == (convo if convo else "")
                 and m.sender != self.handle
                 and m.kind == "chat"
                 and not m.sender.endswith("→ ai")
                 and m.sender != "ai"),
                None,
            )
            if not last:
                self._error("No peer message found in current conversation")
                return
            prompt = f"[{last.sender}]: {last.text}"
            # Reuse /ai logic
            self._handle_command(f"/ai {prompt}")
            return

        elif cmd == "/ai":
            if not arg:
                self._error("Usage: /ai <prompt>")
                return
            self._system("AI: thinking…")
            active_peer = self.active_peer
            channel     = self.view if self.view != "top" else ""
            convo       = f"#{channel}" if channel else active_peer
            history     = list(self._ai_history.get(convo, []))

            def _ai_call() -> None:
                try:
                    from kirbus.ai import ask
                    text = ask(arg, history=history)
                except Exception as exc:
                    import traceback
                    self.inbox.put(("system_event", f"AI error: {type(exc).__name__}: {exc}"))
                    self.inbox.put(("system_event", traceback.format_exc().splitlines()[-1]))
                    return
                self.inbox.put(("__ai_response__", text, active_peer, channel, arg))

            threading.Thread(target=_ai_call, daemon=True).start()

        elif cmd == "/accept-game":
            invite = getattr(self, "_pending_game_invite", None)
            if not invite:
                self._error("No pending game invite.")
                return
            self.outbox.put((
                invite["agent"],
                f"\x00accept_invite\x00{invite['invite_id']}"
            ))
            self._system(f"Accepted {invite['game']} with {invite['from']}!")
            self._pending_game_invite = None

        elif cmd == "/decline-game":
            invite = getattr(self, "_pending_game_invite", None)
            if not invite:
                self._error("No pending game invite.")
                return
            self.outbox.put((
                invite["agent"],
                f"\x00decline_invite\x00{invite['invite_id']}"
            ))
            self._system("Game invite declined.")
            self._pending_game_invite = None

        else:
            self._error(f"Unknown command: {cmd}  (type /help for commands)")

    def _handle_channel_command(self, arg: str) -> None:
        from kirbus.ui.models import Channel, RESERVED_CHANNELS
        parts  = arg.strip().split(maxsplit=2)
        sub    = parts[0].lower() if parts else ""
        name   = parts[1] if len(parts) > 1 else ""
        extra  = parts[2] if len(parts) > 2 else ""

        if sub == "create":
            if not name:
                self._error("Usage: /channel create <name>")
                return
            if name.lower() in RESERVED_CHANNELS or name.startswith((".", "/")):
                self._error(f"'{name}' is a reserved name")
                return
            if name in self.channels:
                self._error(f"Channel #{name} already exists")
                return
            self.channels[name] = Channel(name=name, members=[self.handle])
            self._system(f"Created channel #{name}")

        elif sub == "join":
            if not name:
                self._error("Usage: /channel join <name>")
                return
            if name not in self.channels:
                self.channels[name] = Channel(name=name, members=[self.handle])
                self._system(f"Joined channel #{name}")
            else:
                ch = self.channels[name]
                if self.handle not in ch.members:
                    ch.members.append(self.handle)
                self._system(f"Already in #{name}")

        elif sub == "invite":
            if not name:
                self._error("Usage: /channel invite <peer> [channel]")
                return
            peer    = name
            ch_name = extra or (self.view if self.view != "top" else "")
            if not ch_name:
                self._error("Not in a channel — use: /channel invite <peer> <channel>")
                return
            if ch_name not in self.channels:
                self._error(f"Channel #{ch_name} does not exist")
                return
            ch = self.channels[ch_name]
            if peer not in ch.members:
                ch.members.append(peer)
            self.outbox.put((peer, f"\x00channel_invite\x00{ch_name}"))
            self._system(f"Invited {peer} to #{ch_name}")

        elif sub == "leave":
            if not name:
                self._error("Usage: /channel leave <name>")
                return
            if name not in self.channels:
                self._error(f"Not in channel #{name}")
                return
            ch = self.channels.pop(name)
            if self.active_peer == ch.key:
                self.active_peer = SCRATCH_PEER
                self.view        = "top"
            self._system(f"Left channel #{name}")

        else:
            self._error("Usage: /channel create|join|invite|leave ...")

    # ------------------------------------------------------------------
    # Key handling
    # ------------------------------------------------------------------
    def _handle_key(self, ch: int) -> None:
        if ch == curses.KEY_RESIZE:
            self._resize()
            return

        if ch == curses.KEY_MOUSE:
            try:
                _, _mx, _my, _mz, bstate = curses.getmouse()
                step = max(1, self.chat_h - 4)
                if bstate & curses.BUTTON4_PRESSED:   # scroll up
                    self.scroll += step
                elif bstate & curses.BUTTON5_PRESSED:  # scroll down
                    self.scroll = max(0, self.scroll - step)
            except curses.error:
                pass
            return

        if ch == ord("\t"):
            if self.focus == "input":
                self.focus       = "presence"
                self.peer_cursor = max(0, min(self.peer_cursor, len(self.peers) - 1))
                curses.curs_set(0)
            else:
                self.focus = "input"
                curses.curs_set(1)
            return

        if ch == 27:
            self.focus = "input"
            curses.curs_set(1)
            return

        if self.focus == "presence":
            rows       = self._presence_rows()
            _HDR = ("\x00ch_", "\x00dm_", "\x00srv_",
                    "\x00menu_", "\x00peer_", "\x00pick_",
                    "\x00agent_header")
            selectable = [i for i, (k, _, _) in enumerate(rows)
                          if not any(k.startswith(p) for p in _HDR)]
            if ch == curses.KEY_UP:
                idx = selectable.index(self.peer_cursor) if self.peer_cursor in selectable else 0
                self.peer_cursor = selectable[max(0, idx - 1)]
            elif ch == curses.KEY_DOWN:
                idx = selectable.index(self.peer_cursor) if self.peer_cursor in selectable else 0
                self.peer_cursor = selectable[min(len(selectable) - 1, idx + 1)]
            elif ch in (curses.KEY_ENTER, ord("\n"), ord("\r")):
                if not rows or self.peer_cursor >= len(rows):
                    return
                key, label, _ = rows[self.peer_cursor]
                menu = getattr(self, "agent_menu", None)
                if key == BACK_ENTRY and menu is not None:
                    # Navigate back within agent menu
                    if getattr(self, "agent_session", ""):
                        # End session → back to menu
                        self.outbox.put((menu.agent, "\x00back\x00"))
                        self.agent_session = ""
                        self.peer_cursor = 0
                    elif getattr(self, "agent_picking_peer", ""):
                        # Cancel peer pick → back to menu
                        self.agent_picking_peer = ""
                        self.peer_cursor = 0
                    else:
                        # Exit agent menu entirely
                        self.agent_menu = None
                        self.active_peer = ""
                        self.peer_cursor = 0
                elif key == BACK_ENTRY:
                    self.view        = "top"
                    self.peer_cursor = 0
                elif key.startswith("\x00entry:"):
                    # Agent menu entry selected
                    entry_key = key[7:]
                    entry = next((e for e in menu.entries if e.key == entry_key), None)
                    if entry and entry.type == "multi":
                        self.agent_picking_peer = entry_key
                        self.peer_cursor = 0
                    elif entry:
                        self.outbox.put((menu.agent, f"\x00select\x00{entry_key}"))
                        self.focus = "input"
                        curses.curs_set(1)
                elif key.startswith("\x00pick:"):
                    # Multiplayer opponent selected
                    opponent = key[6:]
                    entry_key = getattr(self, "agent_picking_peer", "")
                    self.outbox.put((menu.agent, f"\x00select\x00{entry_key}\x00{opponent}"))
                    self.agent_picking_peer = ""
                    self.focus = "input"
                    curses.curs_set(1)
                elif key.startswith("\x00agent:"):
                    # Re-enter an agent menu
                    agent_handle = key[7:]
                    agent_menus = getattr(self, "agent_menus", {})
                    if agent_handle in agent_menus:
                        self.agent_menu = agent_menus[agent_handle]
                        self.active_peer = agent_handle
                        self.agent_session = ""
                        self.agent_picking_peer = ""
                        self.peer_cursor = 0
                elif key.startswith("\x00srv:"):
                    server_name = key[5:]
                    # Check if server requires a password
                    servers = getattr(self, "registry_servers", [])
                    srv = next((s for s in servers if s["name"] == server_name), None)
                    if srv and srv.get("access") == "password":
                        # Pre-fill the command so user can type the password
                        self._set_input(f"/connect {server_name} ")
                        self._system(f"Server '{server_name}' requires a password")
                        self.focus = "input"
                        curses.curs_set(1)
                    else:
                        self.outbox.put(("__select_server__", server_name, ""))
                        self.focus = "input"
                        curses.curs_set(1)
                elif key.startswith("#"):
                    self.view        = key[1:]
                    self.active_peer = key
                    self.unread.pop(key, None)
                    self.peer_cursor = 0
                    self.focus       = "input"
                    curses.curs_set(1)
                else:
                    # Check if this peer has a cached agent menu
                    agent_menus = getattr(self, "agent_menus", {})
                    if key in agent_menus:
                        self.agent_menu = agent_menus[key]
                        self.active_peer = key
                        self.agent_session = ""
                        self.agent_picking_peer = ""
                        self.peer_cursor = 0
                        return
                    self.active_peer = key
                    self.unread.pop(key, None)
                    if key != SCRATCH_PEER:
                        self._system(f"Now chatting with {label}")
                    self.focus = "input"
                    curses.curs_set(1)
            return

        # Input-focused keys
        if ch in (curses.KEY_ENTER, ord("\n"), ord("\r")):
            text = "".join(self.input_buf).strip()
            self.input_buf.clear()
            self.cursor       = 0
            self.scroll       = 0
            self.history_idx  = -1
            self.history_draft = ""
            if not text:
                return
            if not self.history or self.history[-1] != text:
                self.history.append(text)
            if text.startswith("/"):
                self._handle_command(text)
            else:
                menu = getattr(self, "agent_menu", None)
                session = getattr(self, "agent_session", "")
                if menu is not None and session:
                    # In an agent session — send to agent, show locally
                    self._chat(self.handle, text, convo=menu.agent)
                    self.outbox.put((menu.agent, text))
                elif menu is not None:
                    # In agent menu but no session — ignore or show error
                    self._error("Select a game first")
                else:
                    channel = self.view if self.view != "top" else ""
                    self._chat(self.handle, text, channel=channel)
                    if channel and channel in self.channels:
                        # Broadcast to all online channel members.
                        ch = self.channels[channel]
                        for member in ch.members:
                            if member == self.handle:
                                continue
                            if any(h == member and on for h, on in self.peers):
                                self.outbox.put((member, text, channel))
                    elif self.active_peer and self.active_peer != SCRATCH_PEER:
                        if not any(h == self.active_peer and on for h, on in self.peers):
                            self._error(f"{self.active_peer} is offline")
                        else:
                            self.outbox.put((self.active_peer, text, channel))

        elif ch in (curses.KEY_BACKSPACE, 127, 8):
            if self.cursor > 0:
                self.cursor -= 1
                del self.input_buf[self.cursor]

        elif ch == curses.KEY_DC:
            if self.cursor < len(self.input_buf):
                del self.input_buf[self.cursor]

        elif ch == curses.KEY_LEFT:
            self.cursor = max(0, self.cursor - 1)
        elif ch == curses.KEY_RIGHT:
            self.cursor = min(len(self.input_buf), self.cursor + 1)
        elif ch == curses.KEY_HOME:
            self.cursor = 0
        elif ch == curses.KEY_END:
            self.cursor = len(self.input_buf)

        elif ch == curses.KEY_UP:
            if not self.history:
                pass
            elif self.history_idx == -1:
                self.history_draft = "".join(self.input_buf)
                self.history_idx   = len(self.history) - 1
                self._set_input(self.history[self.history_idx])
            elif self.history_idx > 0:
                self.history_idx -= 1
                self._set_input(self.history[self.history_idx])
        elif ch == curses.KEY_DOWN:
            if self.history_idx == -1:
                pass
            elif self.history_idx < len(self.history) - 1:
                self.history_idx += 1
                self._set_input(self.history[self.history_idx])
            else:
                self.history_idx   = -1
                self._set_input(self.history_draft)
                self.history_draft = ""

        elif ch == curses.KEY_PPAGE:
            self.scroll += max(1, self.chat_h - 4)
        elif ch == curses.KEY_NPAGE:
            self.scroll = max(0, self.scroll - max(1, self.chat_h - 4))

        elif 32 <= ch <= 126:
            self.input_buf.insert(self.cursor, chr(ch))
            self.cursor += 1
