"""
Global commands which are to be linked to the Core class
"""

from urllib.parse import unquote
from typing import TYPE_CHECKING, cast, Any
from xml.etree import ElementTree as ET
import logging

from slixmpp import JID, InvalidJID
from slixmpp.types import PresenceTypes
from slixmpp.exceptions import XMPPError, IqError, IqTimeout
from slixmpp.xmlstream.xmlstream import NotConnectedError
from slixmpp.xmlstream.stanzabase import StanzaBase
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath

from poezio import common, config as config_module, tabs, multiuserchat as muc
from poezio.bookmarks import Bookmark
from poezio.config import config, DEFAULT_CONFIG
from poezio.contact import Contact
from poezio.decorators import deny_anonymous
from poezio.plugin import PluginConfig
from poezio.roster import roster
from poezio.theming import dump_tuple, get_theme
from poezio.decorators import command_args_parser, command_jid_from_context
from poezio.core.structs import Command, POSSIBLE_SHOW

if TYPE_CHECKING:
    from poezio.core.core import Core

log = logging.getLogger(__name__)


class CommandCore:
    def __init__(self, core: "Core") -> None:
        self.core = core

    @command_args_parser.ignored
    def rotate_rooms_left(self) -> None:
        self.core.rotate_rooms_left()

    @command_args_parser.ignored
    def rotate_rooms_right(self) -> None:
        self.core.rotate_rooms_right()

    @command_args_parser.quoted(0, 1)
    def help(self, args: list[str]) -> None:
        """
        /help [command_name]
        """
        if not args:
            color = dump_tuple(get_theme().COLOR_HELP_COMMANDS)
            acc = []
            buff = ['Global commands:']
            for name, command in self.core.commands.items():
                if isinstance(command, Command):
                    acc.append('  \x19%s}%s\x19o - %s' % (color, name,
                                                          command.short_desc))
                else:
                    acc.append('  \x19%s}%s\x19o' % (color, name))
            acc = sorted(acc)
            buff.extend(acc)
            acc = []
            buff.append('Tab-specific commands:')
            tab_commands = self.core.tabs.current_tab.commands
            for name, command in tab_commands.items():
                if isinstance(command, Command):
                    acc.append('  \x19%s}%s\x19o - %s' % (color, name,
                                                          command.short_desc))
                else:
                    acc.append('  \x19%s}%s\x19o' % (color, name))
            acc = sorted(acc)
            buff.extend(acc)

            msg = '\n'.join(buff)
            msg += "\nType /help <command_name> to know what each command does"
        else:
            command_str = args[0].lstrip('/').strip()

            tab_commands = self.core.tabs.current_tab.commands
            if command_str in tab_commands:
                tup = tab_commands[command_str]
            elif command_str in self.core.commands:
                tup = self.core.commands[command_str]
            else:
                self.core.information(f'Unknown command: {command_str}', 'Error')
                return
            if isinstance(tup, Command):
                msg = 'Usage: /%s %s\n' % (command_str, tup.usage)
                msg += tup.desc
            else:
                msg = tup[1]
        self.core.information(msg, 'Help')

    @command_args_parser.quoted(1)
    def runkey(self, args: list[str]) -> None:
        """
        /runkey <key>
        """

        def replace_line_breaks(key: str) -> str:
            "replace ^J with \n"
            if key == '^J':
                return '\n'
            return key

        if not args:
            return self.help('runkey')
        char = args[0]
        func = self.core.key_func.get(char, None)
        if func is not None:
            func()
        else:
            self.core.do_command(replace_line_breaks(char), False)

    @command_args_parser.quoted(1, 1, [None])
    def status(self, args: list[str]) -> None:
        """
        /status <status> [msg]
        """
        if not args:
            return self.help('status')

        if args[0] not in POSSIBLE_SHOW.keys():
            return self.help('status')

        show = POSSIBLE_SHOW[args[0]]
        msg = args[1]

        pres = self.core.xmpp.make_presence()
        if msg:
            pres['status'] = msg
        pres['type'] = show
        self.core.events.trigger('send_normal_presence', pres)
        pres.send()
        current = self.core.tabs.current_tab
        if isinstance(current, tabs.MucTab) and current.joined and show in ('away', 'xa'):
            current.send_chat_state('inactive')
        for tab in self.core.tabs:
            if isinstance(tab, tabs.MucTab) and tab.joined:
                muc.change_show(self.core.xmpp, tab.jid, tab.own_nick, show,
                                msg)
            if hasattr(tab, 'directed_presence'):
                del tab.directed_presence
        self.core.set_status(show or '', msg)
        if isinstance(current, tabs.MucTab) and current.joined and show not in ('away', 'xa'):
            current.send_chat_state('active')

    @command_args_parser.quoted(1, 2, [None, None])
    def presence(self, args: list[str] | None) -> None:
        """
        /presence <JID> [type] [status]
        """
        if not args:
            return self.help('presence')

        ptype: str | None
        jid_str, ptype, status = args[0], args[1], args[2]
        if jid_str == '.' and isinstance(self.core.tabs.current_tab, tabs.ChatTab):
            jid = self.core.tabs.current_tab.jid
        else:
            try:
                jid = JID(jid_str)
            except InvalidJID:
                self.core.information(f'Invalid JID: {jid_str}', 'Error')
                return
        final_ptype: PresenceTypes | None
        if ptype not in ('error', 'probe', 'subscribe', 'subscribed',
                         'unavailable', 'unsubscribe', 'unsubscribed'):
            final_ptype = None
        else:
            final_ptype = cast(PresenceTypes, ptype)
        try:
            pres = self.core.xmpp.make_presence(
                pto=jid, ptype=final_ptype, pstatus=status)
            self.core.events.trigger('send_normal_presence', pres)
            pres.send()
        except (XMPPError, NotConnectedError):
            self.core.information('Could not send directed presence', 'Error')
            log.debug(
                'Could not send directed presence to %s', jid, exc_info=True)
            return
        tab = self.core.tabs.by_name(str(jid))
        if tab and isinstance(tab, tabs.ChatTab):
            if ptype in ('xa', 'away'):
                tab.directed_presence = False
                chatstate = 'inactive'
            else:
                tab.directed_presence = True
                chatstate = 'active'
            if tab == self.core.tabs.current_tab:
                tab.send_chat_state(chatstate, True)
            if isinstance(tab, tabs.MucTab):
                for private in tab.privates:
                    private.directed_presence = tab.directed_presence
                if self.core.tabs.current_tab in tab.privates:
                    self.core.tabs.current_tab.send_chat_state(chatstate, True)

    @command_args_parser.quoted(1)
    def theme(self, args: list[str] | None = None) -> None:
        """/theme <theme name>"""
        if not args:
            return self.help('theme')
        self.set_cmd('theme %s' % (args[0], ))

    @command_args_parser.quoted(1)
    def win(self, args: list[str] | str | None) -> None:
        """
        /win <number or name>
        """
        if not args:
            return self.help('win')

        name = args[0]
        try:
            number = int(name)
        except ValueError:
            number = -1
            name = name.lower()
        if number != -1 and self.core.tabs.current_tab.nb == number:
            return
        prev_nb = self.core.previous_tab_nb
        self.core.previous_tab_nb = self.core.tabs.current_tab.nb
        if 0 <= number < len(self.core.tabs):
            if not self.core.tabs[number]:
                self.core.previous_tab_nb = prev_nb
                return
            self.core.tabs.set_current_index(number)
        else:
            match = self.core.tabs.find_match(name)
            if match is None:
                return
            self.core.tabs.set_current_tab(match)

    @command_args_parser.quoted(1)
    def wup(self, args: list[str] | None) -> None:
        """
        /wup <prefix of name>
        """
        if not args:
            return self.help('wup')

        prefix = args[0]
        _, match = self.core.tabs.find_by_unique_prefix(prefix)
        if match is None:
            return
        self.core.tabs.set_current_tab(match)

    @command_args_parser.quoted(2)
    def move_tab(self, args: list[str]) -> None:
        """
        /move_tab old_pos new_pos
        """
        if not args:
            return self.help('move_tab')

        current_tab = self.core.tabs.current_tab
        if args[0] == '.':
            args[0] = str(current_tab.nb)
        if args[1] == '.':
            args[1] = str(current_tab.nb)

        def get_nb_from_value(value: str) -> int | None:
            "parse the cmdline to guess the tab the users wants"
            ref = None
            try:
                ref = int(value)
            except ValueError:
                old_tab = None
                for tab in self.core.tabs:
                    if not old_tab and value == tab.name:
                        old_tab = tab
                if not old_tab:
                    self.core.information("Tab %s does not exist" % args[0],
                                          "Error")
                    return None
                ref = old_tab.nb
            return ref

        old = get_nb_from_value(args[0])
        new = get_nb_from_value(args[1])
        if new is None or old is None:
            self.core.information('Unable to move the tab.', 'Info')
            return
        result = self.core.insert_tab(old, new)
        if not result:
            self.core.information('Unable to move the tab.', 'Info')
        self.core.refresh_window()

    @command_args_parser.quoted(0, 1)
    def list_cmd(self, args: list[str]) -> None:
        """
        /list [server]
        Opens a MucListTab containing the list of the room in the specified server
        """
        if not args:
            return self.help('list')
        elif args:
            try:
                jid = JID(args[0])
            except InvalidJID:
                self.core.information('Invalid server %r' % args[0], 'Error')
                return
        else:
            if not isinstance(self.core.tabs.current_tab, tabs.MucTab):
                self.core.information('Please provide a server',
                                      'Error')
                return
            jid = self.core.tabs.current_tab.jid
        if jid is None or not jid.domain:
            return
        self.core.register_task(
            self._list_async(jid)
        )

    async def _list_async(self, jid: JID) -> None:
        jid = JID(jid.domain)
        list_tab = tabs.MucListTab(self.core, jid)
        self.core.add_tab(list_tab, True)
        iq = await self.core.xmpp.plugin['xep_0030'].get_items(jid=jid)
        list_tab.on_muc_list_item_received(iq)

    @command_args_parser.quoted(1)
    async def version(self, args: list[str] | str | None) -> None:
        """
        /version <jid>
        """
        if not args:
            self.help('version')
            return
        try:
            jid = JID(args[0])
        except InvalidJID:
            self.core.information(
                'Invalid JID for /version: %s' % args[0],
                'Error'
            )
            return
        if jid.resource or jid not in roster or not roster.contacts[jid].resources:
            iq = await self.core.xmpp.plugin['xep_0092'].get_version(jid)
            self.core.handler.on_version_result(iq)
        elif jid in roster:
            for resource in roster.contacts[jid].resources:
                iq = await self.core.xmpp.plugin['xep_0092'].get_version(
                    JID(resource.jid)
                )
                self.core.handler.on_version_result(iq)

    def _empty_join(self) -> tuple[str | None, str | None]:
        tab = self.core.tabs.current_tab
        if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)):
            return (None, None)
        room = tab.jid.bare
        nick = tab.own_nick
        return (room, nick)

    def _parse_join_jid(self, jid_string: str
                        ) -> tuple[str, str | None] | tuple[None, None]:
        # we try to join a server directly
        server_root = False
        if jid_string.startswith('xmpp:') and jid_string.endswith('?join'):
            jid_string = unquote(jid_string[5:-5])
        try:
            if jid_string.startswith('@'):
                server_root = True
                info = JID(jid_string[1:])
            else:
                info = JID(jid_string)
                server_root = False
        except InvalidJID:
            info = JID('')

        set_nick: str | None = ''
        if len(jid_string) > 1 and jid_string.startswith('/'):
            set_nick = jid_string[1:]
        elif info.resource:
            set_nick = info.resource

        # happens with /join /nickname, which is OK
        if info.bare == '':
            tab = self.core.tabs.current_tab
            if not isinstance(tab, tabs.MucTab):
                return (None, None)
            else:
                room = tab.jid.bare
                if not set_nick:
                    set_nick = tab.own_nick
        else:
            room = info.bare
            # no server is provided, like "/join hello":
            # use the server of the current room if available
            # check if the current room's name has a server
            if room.find('@') == -1 and not server_root:
                tab = self.core.tabs.current_tab
                if isinstance(tab, tabs.MucTab) and tab.jid.domain:
                    room += '@%s' % tab.jid.domain
        return (room, set_nick)

    @command_args_parser.quoted(0, 2)
    async def join(self, args: list[str] | str) -> None:
        """
        /join [room][/nick] [password]
        """
        if not args:
            room, nick = self._empty_join()
        else:
            room, nick = self._parse_join_jid(args[0])
        if not room:
            return  # nothing was parsed

        room = room.lower()

        # Has the nick been picked from configuration, or chosen
        # explicitly when joining?
        config_nick = False
        if not nick:
            config_nick = True
            nick = self.core.own_nick

        # a password is provided
        if len(args) == 2:
            password = args[1]
        else:
            password = config.get_by_tabname('password', JID(room), fallback=False)

        if room in self.core.pending_invites:
            del self.core.pending_invites[room]

        tab = self.core.tabs.by_name_and_class(room, tabs.MucTab)
        # New tab
        if tab is None:
            tab = self.core.open_new_room(JID(room), nick, password=password)
            tab.join()
        else:
            self.core.focus_tab(tab)
            if tab.own_nick == nick and tab.joined:
                self.core.information('/join: Nothing to do.', 'Info')
            else:
                tab.command_part('')
                tab.own_nick = nick
                tab.password = password
                tab.join()

        if config.getbool('synchronise_open_rooms') and room not in self.core.bookmarks:
            method = 'remote' if config.getbool(
                'use_remote_bookmarks') else 'local'
            await self._add_bookmark(
                room=room,
                nick=nick if not config_nick else None,
                autojoin=True,
                password=password,
                method=method,
            )

        if tab == self.core.tabs.current_tab:
            tab.refresh()
            self.core.doupdate()

    @command_args_parser.quoted(0, 2)
    def bookmark_local(self, args: list[str]) -> None:
        """
        /bookmark_local [room][/nick] [password]
        """
        tab = self.core.tabs.current_tab
        nick: str | None
        if not args:
            if not isinstance(tab, tabs.MucTab):
                return
            room = tab.jid.bare
            nick = tab.own_nick
            password = tab.password
        else:
            tmp_room, tmp_nick = self._parse_join_jid(args[0] if args else '')
            if tmp_room is None:
                return
            room, nick = tmp_room, tmp_nick
            password = args[1] if len(args) > 1 else None

        self.core.register_task(
            self._add_bookmark(
                room=str(room),
                nick=nick,
                autojoin=True,
                password=password,
                method='local',
            )
        )

    @command_args_parser.quoted(0, 3)
    def bookmark(self, args: list[str]) -> None:
        """
        /bookmark [room][/nick] [autojoin] [password]
        """
        tab = self.core.tabs.current_tab
        nick: str | None
        if not args:
            if not isinstance(tab, tabs.MucTab):
                return
            room = tab.jid.bare
            nick = tab.own_nick
            password = tab.password
        else:
            tmp_room, tmp_nick = self._parse_join_jid(args[0])
            if tmp_room is None:
                return
            room, nick = tmp_room, tmp_nick
            password = args[2] if len(args) > 2 else None

        method = 'remote' if config.getbool('use_remote_bookmarks') else 'local'
        autojoin = (method == 'local' or
                    (len(args) > 1 and args[1].lower() == 'true'))

        self.core.register_task(
            self._add_bookmark(str(room), nick, autojoin, password, method)
        )

    async def _add_bookmark(
        self,
        room: str,
        nick: str | None,
        autojoin: bool,
        password: str | None,
        method: str,
    ) -> None:
        '''
        Adds a bookmark.

        Args:
            room: room Jid.
            nick: optional nick. Will always be added to the bookmark if
                specified. This takes precedence over tab.own_nick which takes
                precedence over core.own_nick (global config).
            autojoin: set the bookmark to join automatically.
            password: room password.
            method: 'local' or 'remote'.
        '''

        if room == '*':
            return await self._add_wildcard_bookmarks(method)

        # Once we found which room to bookmark, find corresponding tab if it
        # exists and fill nickname if none was specified and not default.
        tab = self.core.tabs.by_name_and_class(room, tabs.MucTab)
        if tab and isinstance(tab, tabs.MucTab) and \
           tab.joined and tab.own_nick != self.core.own_nick:
            nick = nick or tab.own_nick

        # Validate / Normalize
        try:
            if not nick:
                jid = JID(room)
            else:
                jid = JID('{}/{}'.format(room, nick))
            room = jid.bare
            nick = jid.resource or None
        except InvalidJID:
            self.core.information(f'Invalid address for bookmark: {room}/{nick}', 'Error')
            return

        bookmark = self.core.bookmarks[room]
        if bookmark is None:
            bookmark = Bookmark(room)
            self.core.bookmarks.append(bookmark)
        bookmark.method = method
        bookmark.autojoin = autojoin
        if nick:
            bookmark.nick = nick
        if password:
            bookmark.password = password

        self.core.bookmarks.save_local()
        try:
            result = await self.core.bookmarks.save_remote(
                self.core.xmpp,
            )
            self.core.handler.on_bookmark_result(result)
        except (IqError, IqTimeout) as iq:
            self.core.handler.on_bookmark_result(iq)

    async def _add_wildcard_bookmarks(self, method: str) -> None:
        new_bookmarks = []
        for tab in self.core.get_tabs(tabs.MucTab):
            bookmark = self.core.bookmarks[tab.jid.bare]
            if not bookmark:
                bookmark = Bookmark(tab.jid.bare, autojoin=True, method=method)
                new_bookmarks.append(bookmark)
            else:
                bookmark.method = method
                new_bookmarks.append(bookmark)
                self.core.bookmarks.remove(bookmark)
        new_bookmarks.extend(self.core.bookmarks.bookmarks)
        self.core.bookmarks.set(new_bookmarks)
        self.core.bookmarks.save_local()
        try:
            iq = await self.core.bookmarks.save_remote(self.core.xmpp)
            self.core.handler.on_bookmark_result(iq)
        except IqError as iq:
            self.core.handler.on_bookmark_result(iq)

    @command_args_parser.ignored
    def bookmarks(self) -> None:
        """/bookmarks"""
        tab = self.core.tabs.by_name_and_class('Bookmarks', tabs.BookmarksTab)
        if tab:
            self.core.tabs.set_current_tab(tab)
        else:
            tab = tabs.BookmarksTab(self.core, self.core.bookmarks)
            self.core.tabs.append(tab)
            self.core.tabs.set_current_tab(tab)

    @command_args_parser.quoted(0, 1)
    def remove_bookmark(self, args: list[str]) -> None:
        """/remove_bookmark [jid]"""
        jid = None
        if not args:
            tab = self.core.tabs.current_tab
            if isinstance(tab, tabs.MucTab):
                jid = tab.jid.bare
        else:
            jid = args[0]

        if jid:
            self.core.register_task(
                self._remove_bookmark_routine(jid)
            )

    async def _remove_bookmark_routine(self, jid: str) -> None:
        """Asynchronously remove a bookmark"""
        if self.core.bookmarks[jid]:
            self.core.bookmarks.remove(jid)
            try:
                await self.core.bookmarks.save(self.core.xmpp)
                self.core.information('Bookmark deleted', 'Info')
            except (IqError, IqTimeout):
                self.core.information('Error while deleting the bookmark',
                                      'Error')
        else:
            self.core.information('No bookmark to remove', 'Info')

    @deny_anonymous
    @command_args_parser.quoted(0, 1)
    def accept(self, args: list[str]) -> None:
        """
        Accept a JID. Authorize it AND subscribe to it
        """
        if not args:
            tab = self.core.tabs.current_tab
            RosterInfoTab = tabs.RosterInfoTab
            if not isinstance(tab, RosterInfoTab):
                self.core.information('No JID specified', 'Error')
                return
            else:
                item = tab.selected_row
                if isinstance(item, Contact):
                    bare_jid = item.bare_jid.bare
                else:
                    self.core.information('No subscription to accept', 'Warning')
                    return
        else:
            try:
                bare_jid = JID(args[0]).bare
            except InvalidJID:
                self.core.information('Invalid JID for /accept: %s' % args[0], 'Error')
                return
        jid = JID(bare_jid)
        nodepart = jid.user
        # crappy transports putting resources inside the node part
        if '\\2f' in nodepart:
            jid.user = nodepart.split('\\2f')[0]
        contact = roster[jid]
        if contact is None:
            self.core.information('No subscription to accept', 'Warning')
            return
        contact.pending_in = False
        roster.modified()
        self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
        self.core.xmpp.client_roster.send_last_presence()
        if contact.subscription in ('from',
                                    'none') and not contact.pending_out:
            self.core.xmpp.send_presence(
                pto=jid, ptype='subscribe', pnick=self.core.own_nick)
        self.core.information('%s is now authorized' % jid, 'Roster')

    @deny_anonymous
    @command_args_parser.quoted(1)
    def add(self, args: list[str]) -> None:
        """
        Add the specified JID to the roster, and automatically
        accept the reverse subscription
        """
        if not args:
            tab = self.core.tabs.current_tab
            ConversationTab = tabs.ConversationTab
            if isinstance(tab, ConversationTab):
                jid = tab.general_jid
                if jid in roster and roster.contacts[jid].subscription in ('to', 'both'):
                    self.core.information('Already subscribed.', 'Roster')
                    return
                roster.add(jid)
                roster.modified()
                self.core.information('%s was added to the roster' % jid, 'Roster')
            else:
                self.core.information('No JID specified', 'Error')
        try:
            jid = JID(JID(args[0]).bare)
        except InvalidJID:
            self.core.information('Invalid JID for /add: %s' % args[0], 'Error')
            return
        if jid in roster and roster.contacts[jid].subscription in ('to', 'both'):
            self.core.information('Already subscribed.', 'Roster')
            return
        roster.add(jid)
        roster.modified()
        self.core.information('%s was added to the roster' % jid, 'Roster')

    @deny_anonymous
    @command_args_parser.quoted(0, 1)
    def deny(self, args: list[str]) -> None:
        """
        /deny [jid]
        Denies a JID from our roster
        """
        jid = None
        if not args:
            tab = self.core.tabs.current_tab
            if isinstance(tab, tabs.RosterInfoTab):
                item = tab.roster_win.selected_row
                if isinstance(item, Contact):
                    jid = item.bare_jid
        else:
            try:
                jid = JID(JID(args[0]).bare)
            except InvalidJID:
                self.core.information('Invalid JID for /deny: %s' % args[0], 'Error')
                return
            if jid not in [jid for jid in roster.jids()]:
                jid = None
        if jid is None:
            self.core.information('No subscription to deny', 'Warning')
            return

        contact = roster[jid]
        if contact:
            contact.unauthorize()
            self.core.information('Subscription to %s was revoked' % jid,
                                  'Roster')

    @deny_anonymous
    @command_args_parser.quoted(0, 1)
    def remove(self, args: list[str]) -> None:
        """
        Remove the specified JID from the roster. i.e.: unsubscribe
        from its presence, and cancel its subscription to our.
        """
        jid = None
        if args:
            try:
                jid = JID(JID(args[0]).bare)
            except InvalidJID:
                self.core.information('Invalid JID for /remove: %s' % args[0], 'Error')
                return
        else:
            tab = self.core.tabs.current_tab
            if isinstance(tab, tabs.RosterInfoTab):
                item = tab.roster_win.selected_row
                if isinstance(item, Contact):
                    jid = item.bare_jid
        if jid is None:
            self.core.information('No roster item to remove', 'Error')
            return
        roster.remove(jid)
        del roster[jid]

    @command_args_parser.ignored
    def command_reconnect(self) -> None:
        """
        /reconnect
        """
        if self.core.xmpp.is_connected():
            self.core.disconnect(reconnect=True)
        else:
            self.core.xmpp.start()

    @command_args_parser.quoted(0, 3)
    def set_cmd(self, args: list[str] | str) -> None:
        """
        /set [module|][section] <option> [value]
        """
        info = ("", "")
        if len(args) == 3 and args[1] == '=':
            args = [args[0], args[2]]
        if not args:
            config_dict = config.to_dict()
            lines = []
            theme = get_theme()
            for section_name, section_dict in config_dict.items():
                lines.append(
                    '\x19%(section_col)s}[%(section)s]\x19o' % {
                        'section': section_name,
                        'section_col': dump_tuple(
                            theme.COLOR_INFORMATION_TEXT),
                    })
                for option_name, option_value in section_dict.items():
                    if (isinstance(option_name, str) and
                            'password' in option_name and
                            'eval_password' not in option_name):
                        option_value = '********'
                    lines.append(
                        '%s\x19%s}=\x19o%s' %
                        (option_name, dump_tuple(
                            theme.COLOR_REVISIONS_MESSAGE), option_value))
            info = ('Current  options:\n%s' % '\n'.join(lines), 'Info')
        elif len(args) == 1:
            option = args[0]
            if '=' in option:
                args = option.split('=', 1)
            else:
                value = config.get(option)
                if (isinstance(option, str) and
                        'password' in option and
                        'eval_password' not in option and
                        value is not None):
                    value = '********'
                info = ('%s=%s' % (option, value), 'Info')
        if len(args) == 2:
            if '|' in args[0]:
                plugin_name, section = args[0].split('|')[:2]
                if not section:
                    section = plugin_name
                option = args[1]
                if plugin_name not in self.core.plugin_manager.plugins:
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
                    plugin = self.core.plugin_manager.plugins[plugin_name]
                    if plugin:
                        plugin_config = plugin.config
                    else:
                        return
                value = plugin_config.get(option, default='', section=section)
                info = ('%s=%s' % (option, value), 'Info')
            else:
                possible_section = args[0]
                if (not config.has_option(section='Poezio', option=possible_section)
                        and config.has_section(possible_section)):
                    section = possible_section
                    option = args[1]
                    value = config.getstr(option, section=section)
                    info = ('%s=%s' % (option, value), 'Info')
                else:
                    option = args[0]
                    value = args[1]
                    info = config.set_and_save(option, value)
                    self.core.trigger_configuration_change(option, value)
        elif len(args) == 3:
            if '|' in args[0]:
                plugin_name, section = args[0].split('|')[:2]
                if not section:
                    section = plugin_name
                option = args[1]
                value = args[2]
                if plugin_name not in self.core.plugin_manager.plugins:
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
                    plugin = self.core.plugin_manager.plugins[plugin_name]
                    if plugin is not None:
                        plugin_config = plugin.config
                    else:
                        return
                info = plugin_config.set_and_save(option, value, section)
            else:
                current_tab = self.core.tabs.current_tab
                if args[0] == '.' and isinstance(current_tab, tabs.ChatTab):
                    name = current_tab.jid.bare
                    if not name:
                        self.core.information(
                            'Invalid tab to use the "." argument.', 'Error')
                        return
                    section = name
                else:
                    section = args[0]
                option = args[1]
                value = args[2]
                info = config.set_and_save(option, value, section)
                self.core.trigger_configuration_change(option, value, section=section)
        elif len(args) > 3:
            return self.help('set')
        self.core.information(*info)

    @command_args_parser.quoted(1, 2)
    def set_default(self, args: list[str] | None) -> None:
        """
        /set_default [section] <option>
        """
        if not args:
            return self.help('set_default')
        if len(args) == 1:
            option = args[0]
            section = 'Poezio'
        elif len(args) == 2:
            section = args[0]
            option = args[1]
        else:
            return self.help('set_default')

        default_config = DEFAULT_CONFIG.get(section, {})
        if option not in default_config:
            info = ("Option %s has no default value" % (option), "Error")
            self.core.information(*info)
            return
        self.set_cmd('%s %s %s' % (section, option, default_config[option]))

    @command_args_parser.quoted(1)
    def toggle(self, args: list[str] | None) -> None:
        """
        /toggle <option>
        shortcut for /set <option> toggle
        """
        if not args:
            return self.help('toggle')

        if args[0]:
            self.set_cmd('%s toggle' % args[0])

    @command_args_parser.quoted(1, 1)
    def server_cycle(self, args: list[str] | None) -> None:
        """
        Do a /cycle on each room of the given server.
        If none, do it on the server of the current tab
        """
        tab = self.core.tabs.current_tab
        message = ""
        if args:
            try:
                domain = JID(args[0]).domain
            except InvalidJID:
                self.core.information(
                    "Invalid server domain: %s" % args[0],
                    "Error"
                )
                return
            if len(args) == 2:
                message = args[1]
        else:
            if isinstance(tab, tabs.MucTab):
                domain = tab.jid.domain
            else:
                self.core.information("No server specified", "Error")
                return
        for tab in self.core.get_tabs(tabs.MucTab):
            if tab.jid.domain == domain:
                tab.leave_room(message)
                tab.join()

    @command_args_parser.quoted(1)
    async def last_activity(self, args: list[str] | str | None) -> None:
        """
        /last_activity <jid>
        """

        if not args:
            self.help('last_activity')
            return
        try:
            jid = JID(args[0])
        except InvalidJID:
            self.core.information(
                'Invalid JID for /last_activity: %s' % args[0],
                'Error',
            )
            return

        try:
            iq = await self.core.xmpp.plugin['xep_0012'].get_last_activity(jid)
        except IqError as error:
            if error.etype == 'auth':
                msg = 'You are not allowed to see the activity of %s' % jid
            else:
                msg = 'Error retrieving the activity of %s: %s' % (jid, error)
            self.core.information(msg, 'Error')
            return
        except IqTimeout:
            self.core.information(
                'Timeout while retrieving the last activity of %s' % jid,
                'Error',
            )
            return

        seconds = iq['last_activity']['seconds']
        status = iq['last_activity']['status']
        from_ = iq['from']
        if not from_.user:
            msg = 'The uptime of %s is %s.' % (
                from_, common.parse_secs_to_str(seconds))
        else:
            msg = 'The last activity of %s was %s ago%s' % (
                from_, common.parse_secs_to_str(seconds),
                (' and their last status was %s' % status)
                if status else '')
        self.core.information(msg, 'Info')

    @command_args_parser.quoted(2, 1, [None])
    async def invite(self, args: list[str] | str | None) -> None:
        """/invite <to> <room> [reason]"""

        if not args:
            self.help('invite')
            return

        reason = args[2]
        try:
            to = JID(args[0])
        except InvalidJID:
            self.core.information('Invalid JID specified for invite: %s' % args[0], 'Error')
            return None
        try:
            room = JID(JID(args[1]).bare)
        except InvalidJID:
            self.core.information('Invalid room JID specified to invite: %s' % args[1], 'Error')
            return
        result = await self.core.invite(to, room, reason=reason)
        if result:
            self.core.information('Invited %s to %s' % (to.bare, room), 'Info')

    @command_args_parser.quoted(1, 0)
    def impromptu(self, args: list[str] | str | None) -> None:
        """/impromptu <jid> [<jid> ...]"""

        if not args:
            self.help('impromptu')
            return

        jids = set()
        current_tab = self.core.tabs.current_tab
        if isinstance(current_tab, tabs.ConversationTab):
            jids.add(current_tab.general_jid)

        for jid in common.shell_split(' '.join(args)):
            try:
                bare = JID(jid).bare
            except InvalidJID:
                self.core.information('Invalid JID for /impromptu: %s' % args[0], 'Error')
                return
            jids.add(JID(bare))

        self.core.register_task(self.core.impromptu(jids))

    @command_args_parser.quoted(1, 1, [''])
    def decline(self, args: list[str] | None) -> None:
        """/decline <room@server.tld> [reason]"""
        if not args:
            self.help('decline')
            return
        try:
            jid = JID(args[0])
        except InvalidJID:
            self.core.information('Invalid JID for /decline: %s' % args[0], 'Error')
            return
        if jid.bare not in self.core.pending_invites:
            return
        reason = args[1]
        del self.core.pending_invites[jid.bare]
        self.core.xmpp.plugin['xep_0045'].decline_invite(
            jid.bare, self.core.pending_invites[jid.bare], reason)

    @command_args_parser.quoted(0, 1)
    @command_jid_from_context
    async def block(self, jid: JID | None) -> None:
        """Block a JID"""
        if not jid:
            self.help("block")
            return
        try:
            await self.core.xmpp.plugin['xep_0191'].block(jid)
        except (IqError, IqTimeout):
            self.core.information(
                'Could not block %s.' % jid, 'Error',
            )
        else:
            self.core.information('Blocked %s.' % jid, 'Info')

    @command_args_parser.quoted(0, 1)
    @command_jid_from_context
    async def unblock(self, jid: JID | None) -> None:
        """Unblock a JID"""
        if not jid:
            self.help("unblock")
            return
        try:
            await self.core.xmpp.plugin['xep_0191'].unblock(jid)
        except (IqError, IqTimeout):
            self.core.information('Could not unblock the contact.',
                                  'Error')
        else:
            self.core.information('Unblocked %s.' % jid, 'Info')

    @command_args_parser.quoted(0, 1)
    @command_jid_from_context
    async def report(self, jid: JID | None) -> None:
        """Reports a JID to the account server"""
        if not jid:
            self.help("report")
            return
        try:
            await self.core.xmpp.plugin['xep_0377'].report(jid)
        except (IqError, IqTimeout):
            self.core.information(f'Could not report {jid} to the server.', 'Error')
        else:
            self.core.information(f'Reported {jid}.', 'Info')

### Commands without a completion in this class ###

    @command_args_parser.ignored
    def invitations(self) -> None:
        """/invitations"""
        build = []
        for room, inviter in self.core.pending_invites.items():
            try:
                bare = JID(inviter).bare
            except InvalidJID:
                self.core.information(
                    f'Invalid JID found in /invitations: {inviter}',
                    'Error'
                )
            build.append(f'{room} by {bare}')
        if build:
            message = 'You are invited to the following rooms:\n' + ','.join(build)
        else:
            message = 'You do not have any pending invitations.'
        self.core.information(message, 'Info')

    @command_args_parser.quoted(0, 1, [None])
    def quit(self, args: list[str]) -> None:
        """
        /quit [message]
        """
        if not self.core.xmpp.is_connected():
            self.core.exit()
            return

        msg = args[0]
        self.core.save_config()
        self.core.plugin_manager.disable_plugins()
        self.core.xmpp.add_event_handler(
            "disconnected", self.core.exit, disposable=True)
        self.core.disconnect(msg)

    @command_args_parser.quoted(0, 3, ['', '', ''])
    def destroy_room(self, args: list[str]) -> None:
        """
        /destroy_room [JID [reason [alternative room JID]]]
        """
        async def do_destroy(room: JID, reason: str, altroom: JID | None) -> None:
            try:
                await self.core.xmpp['xep_0045'].destroy(room, reason, altroom)
            except (IqError, IqTimeout) as e:
                self.core.information('Unable to destroy room %s: %s' % (room, e), 'Info')
            else:
                self.core.information('Room %s destroyed' % room, 'Info')

        room: JID | None
        if not args[0] and isinstance(self.core.tabs.current_tab, tabs.MucTab):
            room = self.core.tabs.current_tab.general_jid
        else:
            try:
                room = JID(args[0])
            except InvalidJID:
                room = None
            else:
                if room.resource:
                    room = None

        if room is None:
            self.core.information('Invalid room JID: "%s"' % args[0], 'Error')
            return

        reason = args[1]
        altroom = None
        if args[2]:
            try:
                altroom = JID(args[2])
            except InvalidJID:
                self.core.information('Invalid alternative room JID: "%s"' % args[2], 'Error')
                return

        self.core.register_task(do_destroy(room, reason, altroom))

    @command_args_parser.quoted(1, 1, [''])
    def bind(self, args: list[str]) -> None:
        """
        Bind a key.
        """
        if not args:
            return self.help('bind')

        if not config.silent_set(args[0], args[1], section='bindings'):
            self.core.information('Unable to write in the config file',
                                  'Error')

        if args[1]:
            self.core.information('%s is now bound to %s' % (args[0], args[1]),
                                  'Info')
        else:
            self.core.information(
                '%s is now reset to the default binding' % args[0], 'Info')

    @command_args_parser.raw
    def rawxml(self, args: str) -> None:
        """
        /rawxml <xml stanza>
        """

        if not args:
            return

        raw_stanza = args
        try:
            stanza = StanzaBase(self.core.xmpp, xml=ET.fromstring(raw_stanza))
            if stanza.xml.tag == 'iq' and stanza.xml.attrib.get('type') in (
                    'get', 'set'):
                iq_id = stanza.xml.attrib.get('id')
                if not iq_id:
                    iq_id = self.core.xmpp.new_id()
                    stanza['id'] = iq_id

                def iqfunc(iq: StanzaBase) -> None:
                    "handler for an iq reply"
                    self.core.information(str(iq), 'Iq')
                    self.core.xmpp.remove_handler('Iq %s' % iq_id)

                self.core.xmpp.register_handler(
                    Callback('Iq %s' % iq_id, StanzaPath('iq@id=%s' % iq_id),
                             iqfunc))
            stanza.send()
        except Exception:
            self.core.information('Could not send custom stanza', 'Error')
            log.debug(
                '/rawxml: Could not send custom stanza (%s)',
                repr(raw_stanza),
                exc_info=True)

    @command_args_parser.quoted(1, 256)
    def load(self, args: list[str]) -> None:
        """
        /load <plugin> [<otherplugin> …]
        # TODO: being able to load more than 256 plugins at once, hihi.
        """

        usage = '/load <plugin> [<otherplugin> …]'
        if not args:
            self.core.information(usage, 'Error')
            return

        for plugin in args:
            self.core.plugin_manager.load(plugin)

    @command_args_parser.quoted(1, 256)
    def unload(self, args: list[str]) -> None:
        """
        /unload <plugin> [<otherplugin> …]
        """

        usage = '/unload <plugin> [<otherplugin> …]'
        if not args:
            self.core.information(usage, 'Error')
            return

        for plugin in args:
            self.core.plugin_manager.unload(plugin)

    @command_args_parser.ignored
    def plugins(self) -> None:
        """
        /plugins
        """
        self.core.information(
            "Plugins currently in use: %s" % repr(
                list(self.core.plugin_manager.plugins.keys())), 'Info')

    @command_args_parser.quoted(1, 1)
    async def message(self, args: list[str] | None) -> None:
        """
        /message <jid> [message]
        """
        if not args:
            return self.help('message')
        try:
            jid = JID(args[0])
        except InvalidJID:
            self.core.information('Invalid JID for /message: %s' % args[0], 'Error')
            return
        if not jid.user and not jid.domain and not jid.resource:
            self.core.information('Invalid JID.', 'Error')
            return
        tab: tabs.ChatTab | None
        tab = self.core.get_conversation_by_jid(
            jid.full, False, fallback_barejid=False)
        muc = self.core.tabs.by_name_and_class(jid.bare, tabs.MucTab)
        if not tab and not muc:
            tab = self.core.open_conversation_window(JID(jid.full), focus=True)
        elif muc is not None:
            if jid.resource:
                tab = self.core.tabs.by_name_and_class(jid.full,
                                                       tabs.PrivateTab)
                if tab:
                    self.core.focus_tab(tab)
                else:
                    tab = self.core.open_private_window(jid.bare, jid.resource)
            else:
                tab = muc
        elif tab is not None:
            self.core.focus_tab(tab)
        if len(args) == 2 and isinstance(tab, tabs.ChatTab):
            await tab.command_say(args[1])

    @command_args_parser.ignored
    def xml_tab(self) -> None:
        """/xml_tab"""
        xml_tab = self.core.focus_tab_named('XMLTab', tabs.XMLTab)
        if not xml_tab:
            tab = tabs.XMLTab(self.core)
            self.core.add_tab(tab, True)
            self.core.xml_tab = tab

    @command_args_parser.quoted(1)
    async def adhoc(self, args: list[str]) -> None:
        if not args:
            return self.help('ad-hoc')
        try:
            jid = JID(args[0])
        except InvalidJID:
            self.core.information(
                'Invalid JID for ad-hoc command: %s' % args[0],
                'Error',
            )
            return
        list_tab = tabs.AdhocCommandsListTab(self.core, jid)
        self.core.add_tab(list_tab, True)
        iq = await self.core.xmpp.plugin['xep_0050'].get_commands(
            jid=jid,
            local=False
        )
        list_tab.on_list_received(iq)

    @command_args_parser.ignored
    def self_(self) -> None:
        """
        /self
        """
        status = self.core.get_status()
        show, message = status.show, status.message
        nick = self.core.own_nick
        jid = self.core.xmpp.boundjid.full
        info = ('Your JID is %s\nYour current status is "%s" (%s)'
                '\nYour default nickname is %s\nYou are running poezio %s' %
                (jid, message if message else '', show
                 if show else 'available', nick, self.core.custom_version))
        self.core.information(info, 'Info')

    @command_args_parser.ignored
    def reload(self) -> None:
        """
        /reload
        """
        self.core.reload_config()

    @command_args_parser.raw
    def debug(self, args: str) -> None:
        """/debug [filename]"""
        if not args.strip():
            config_module.setup_logging('')
            self.core.information('Debug logging disabled!', 'Info')
        elif args:
            config_module.setup_logging(args)
            self.core.information(f'Debug logging to {args} enabled!', 'Info')


def dumb_callback(*args: Any, **kwargs: Any) -> None:
    "mock callback"
