"""
The RosterInfoTab is the tab showing roster info, the list of contacts,
half of it is dedicated to showing the information buffer, and a small
rectangle shows the current contact info.

This module also includes functions to match users in the roster.
"""
import logging
import base64
import curses
import difflib
import os
import ssl
from functools import partial
from os import getenv, path
from pathlib import Path
from typing import Any, Callable, TYPE_CHECKING

from slixmpp import JID, InvalidJID, Message
from slixmpp.exceptions import IqError, IqTimeout

from poezio import windows
from poezio.common import shell_split
from poezio.config import config
from poezio.contact import Contact, Resource
from poezio.decorators import refresh_wrapper
from poezio.roster import RosterGroup, roster
from poezio.decorators import command_args_parser, deny_anonymous
from poezio.core.structs import Command, Completion
from poezio.tabs import DefaultHelpTab, Tab
from poezio.ui.types import InfoMessage

if TYPE_CHECKING:
    from poezio.core.core import Core
    from poezio.windows.inputs import Input

log = logging.getLogger(__name__)


class RosterInfoTab(DefaultHelpTab):
    """
    A tab, split in two, containing the roster and infos
    """
    plugin_commands: dict[str, Command] = {}
    plugin_keys: dict[str, Callable[..., Any]] = {}

    def __init__(self, core: 'Core') -> None:
        self.default_help_message = windows.HelpText(
            "Enter commands with “/”. “o”: toggle offline show")

        DefaultHelpTab.__init__(self, core)
        self._name = "Roster"
        self.v_separator = windows.VerticalSeparator()
        self.information_win = windows.TextWin()
        self.core.information_buffer.add_window(self.information_win)
        self.roster_win = windows.RosterWin()
        self.contact_info_win = windows.ContactInfoWin()
        self.avatar_win = windows.ImageWin()
        self.state = 'normal'
        self.key_func['^I'] = self.completion
        self.key_func["/"] = self.on_slash
        # disable most of the roster features when in anonymous mode
        if not self.core.xmpp.anon:
            self.key_func[' '] = self.on_space
            self.key_func["KEY_UP"] = self.move_cursor_up
            self.key_func["KEY_DOWN"] = self.move_cursor_down
            self.key_func["M-u"] = self.move_cursor_to_next_contact
            self.key_func["M-y"] = self.move_cursor_to_prev_contact
            self.key_func["M-U"] = self.move_cursor_to_next_group
            self.key_func["M-Y"] = self.move_cursor_to_prev_group
            self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
            self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
            self.key_func["l"] = lambda: self.command_last_activity("")
            self.key_func["o"] = self.toggle_offline_show
            self.key_func["v"] = self.get_contact_version
            self.key_func["i"] = self.show_contact_info
            self.key_func["s"] = self.start_search
            self.key_func["S"] = self.start_search_slow
            self.key_func["n"] = self.change_contact_name
        self.register_command(
            'name',
            self.command_name,
            usage='<jid> [name]',
            shortdesc='Set the given JID\'s name.',
            completion=self.completion_name)
        self.register_command(
            'groupadd',
            self.command_groupadd,
            usage='[<jid> <group>]|<group>',
            desc='Add the given JID or selected line to the given group.',
            shortdesc='Add a user to a group',
            completion=self.completion_groupadd)
        self.register_command(
            'groupmove',
            self.command_groupmove,
            usage='<jid> <old group> <new group>',
            desc='Move the given JID from the old group to the new group.',
            shortdesc='Move a user to another group.',
            completion=self.completion_groupmove)
        self.register_command(
            'groupremove',
            self.command_groupremove,
            usage='<jid> <group>',
            desc='Remove the given JID from the given group.',
            shortdesc='Remove a user from a group.',
            completion=self.completion_groupremove)
        self.register_command(
            'export',
            self.command_export,
            usage='[/path/to/file]',
            desc='Export your contacts into /path/to/file if '
            'specified, or $HOME/poezio_contacts if not.',
            shortdesc='Export your roster to a file.',
            completion=partial(self.completion_file, 1))
        self.register_command(
            'import',
            self.command_import,
            usage='[/path/to/file]',
            desc='Import your contacts from /path/to/file if '
            'specified, or $HOME/poezio_contacts if not.',
            shortdesc='Import your roster from a file.',
            completion=partial(self.completion_file, 1))
        self.register_command(
            'password',
            self.command_password,
            usage='<password>',
            shortdesc='Change your password')
        self.register_command(
            'disconnect',
            self.command_disconnect,
            desc='Disconnect from the remote server.',
            shortdesc='Disconnect from the server.')
        self.register_command(
            'clear', self.command_clear, shortdesc='Clear the info buffer.')
        self.register_command(
            'last_activity',
            self.command_last_activity,
            usage='<jid>',
            desc='Informs you of the last activity of a JID.',
            shortdesc='Get the activity of someone.',
            completion=self.core.completion.last_activity)

        self.resize()
        self.update_commands()
        self.update_keys()

    def check_blocking(self, features: dict[str, Any]) -> None:
        if 'urn:xmpp:blocking' in features and not self.core.xmpp.anon:
            self.register_command(
                'list_blocks',
                self.command_list_blocks,
                shortdesc='Show the blocked contacts.')
            self.core.xmpp.del_event_handler('session_start',
                                             self.check_blocking)
            self.core.xmpp.add_event_handler('blocked_message',
                                             self.on_blocked_message)

    def check_saslexternal(self, features: dict[str, Any]) -> None:
        if 'urn:xmpp:saslcert:1' in features and not self.core.xmpp.anon:
            self.register_command(
                'certs',
                self.command_certs,
                desc='List the fingerprints of certificates'
                ' which can connect to your account.',
                shortdesc='List allowed client certs.')
            self.register_command(
                'cert_add',
                self.command_cert_add,
                desc='Add a client certificate to the authorized ones. '
                'It must have an unique name and be contained in '
                'a PEM file. [management] is a boolean indicating'
                ' if a client connected using this certificate can'
                ' manage the certificates itself.',
                shortdesc='Add a client certificate.',
                usage='<name> <certificate path> [management]',
                completion=self.completion_cert_add)
            self.register_command(
                'cert_disable',
                self.command_cert_disable,
                desc='Remove a certificate from the list '
                'of allowed ones. Clients currently '
                'using this certificate will not be '
                'forcefully disconnected.',
                shortdesc='Disable a certificate',
                usage='<name>')
            self.register_command(
                'cert_revoke',
                self.command_cert_revoke,
                desc='Remove a certificate from the list '
                'of allowed ones. Clients currently '
                'using this certificate will be '
                'forcefully disconnected.',
                shortdesc='Revoke a certificate',
                usage='<name>')
            self.register_command(
                'cert_fetch',
                self.command_cert_fetch,
                desc='Retrieve a certificate with its '
                'name. It will be stored in <path>.',
                shortdesc='Fetch a certificate',
                usage='<name> <path>',
                completion=self.completion_cert_fetch)

    @property
    def selected_row(self) -> Contact | Resource | RosterGroup | None:
        return self.roster_win.get_selected_row()

    @command_args_parser.ignored
    async def command_certs(self) -> None:
        """
        /certs
        """
        try:
            iq = await self.core.xmpp.plugin['xep_0257'].get_certs(timeout=3)
        except (IqError, IqTimeout):
            self.information(
                'Unable to retrieve the certificate list.', 'Error')
            return
        certs = []
        for item in iq['sasl_certs']['items']:
            users = '\n'.join(item['users'])
            certs.append((item['name'], users))

        if not certs:
            self.information('No certificates found', 'Info')
            return
        msg = 'Certificates:\n'
        msg += '\n'.join(
            (('  %s%s' % (item[0] + (': ' if item[1] else ''), item[1]))
             for item in certs))
        self.information(msg, 'Info')

    @command_args_parser.quoted(2, 1)
    async def command_cert_add(self, args: list[str] | None) -> None:
        """
        /cert_add <name> <certfile> [cert-management]
        """
        if not args or len(args) < 2:
            return self.core.command.help('cert_add')
        name = args[0]

        try:
            with open(args[1]) as fd:
                crt = fd.read()
            crt = crt.replace(ssl.PEM_FOOTER, '').replace(
                ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '')
        except Exception as e:
            self.information(
                'Unable to read the certificate: %s' % e,
                'Error',
            )
            return

        bool_management = True
        if len(args) > 2:
            management = args[2]
            if not management or management.lower() in ('false', '0'):
                bool_management = False

        try:
            await self.core.xmpp.plugin['xep_0257'].add_cert(
                name,
                crt,
                allow_management=bool_management
            )
            self.information('Certificate added.', 'Info')
        except (IqError, IqTimeout):
            self.information(
                'Unable to add the certificate.',
                'Error',
            )

    def completion_cert_add(self, the_input: 'Input') -> Completion | None:
        """
        completion for /cert_add <name> <path> [management]
        """
        n = the_input.get_argument_position()
        log.debug('%s %s %s', the_input.text, n, the_input.pos)
        if n == 1:
            return None
        elif n == 2:
            return self.completion_file(2, the_input)
        elif n == 3:
            return Completion(the_input.new_completion, ['true', 'false'], n)
        return None

    @command_args_parser.quoted(1)
    async def command_cert_disable(self, args: list[str] | None) -> None:
        """
        /cert_disable <name>
        """
        if not args:
            return self.core.command.help('cert_disable')
        name = args[0]
        try:
            await self.core.xmpp.plugin['xep_0257'].disable_cert(name)
            self.information('Certificate disabled.', 'Info')
        except (IqError, IqTimeout):
            self.information(
                'Unable to disable the certificate.',
                'Error',
            )

    @command_args_parser.quoted(1)
    async def command_cert_revoke(self, args: list[str] | None) -> None:
        """
        /cert_revoke <name>
        """
        if not args:
            return self.core.command.help('cert_revoke')
        name = args[0]
        try:
            await self.core.xmpp.plugin['xep_0257'].revoke_cert(name)
            self.information('Certificate revoked.', 'Info')
        except (IqError, IqTimeout):
            self.information(
                'Unable to revoke the certificate.',
                'Error',
            )

    @command_args_parser.quoted(2)
    async def command_cert_fetch(self, args: list[str] | None) -> None:
        """
        /cert_fetch <name> <path>
        """
        if not args or len(args) < 2:
            return self.core.command.help('cert_fetch')
        name = args[0]
        path = args[1]

        try:
            iq = await self.core.xmpp.plugin['xep_0257'].get_certs()
        except (IqError, IqTimeout):
            self.information(
                'Unable to fetch the certificate.',
                'Error',
            )
            return
        bytes_cert = None
        for item in iq['sasl_certs']['items']:
            if item['name'] == name:
                bytes_cert = base64.b64decode(item['x509cert'])
                break
        if not bytes_cert:
            self.information('Certificate not found.', 'Info')
            return
        cert = ssl.DER_cert_to_PEM_cert(bytes_cert)
        with open(path, 'w') as fd:
            fd.write(cert)
        self.information('File stored at %s' % path, 'Info')

    def completion_cert_fetch(self, the_input: 'Input') -> Completion | None:
        """
        completion for /cert_fetch <name> <path>
        """
        n = the_input.get_argument_position()
        log.debug('%s %s %s', the_input.text, n, the_input.pos)
        if n == 1:
            return None
        elif n == 2:
            return self.completion_file(2, the_input)
        return None

    def on_blocked_message(self, message: Message) -> None:
        """
        When we try to send a message to a blocked contact
        """
        tab = self.core.get_conversation_by_jid(message['from'], False)
        if not tab:
            log.debug('Received message from nonexistent tab: %s',
                      message['from'])
            return
        msg = 'Cannot send message to %(jid)s: contact blocked' % {
            'jid': message['from'],
        }
        tab.add_message(InfoMessage(msg))

    @command_args_parser.ignored
    async def command_list_blocks(self) -> None:
        """
        /list_blocks
        """
        try:
            iq = await self.core.xmpp.plugin['xep_0191'].get_blocked()
        except (IqError, IqTimeout):
            self.information(
                'Could not retrieve the blocklist.', 'Error')
            return
        s = 'List of blocked JIDs:\n'
        items = (str(item['jid']) for item in iq['blocklist']['items'])
        jids = '\n'.join(items)
        if jids:
            s += jids
        else:
            s = 'No blocked JIDs.'
        self.information(s, 'Info')

    @command_args_parser.ignored
    def command_disconnect(self) -> None:
        """
        /disconnect
        """
        self.core.disconnect()

    @command_args_parser.quoted(0, 1)
    def command_last_activity(self, args: list[str]) -> None:
        """
        /activity [jid]
        """
        item = self.roster_win.selected_row
        jid: str | JID
        if args:
            jid = args[0]
        elif isinstance(item, Contact):
            jid = item.bare_jid
        elif isinstance(item, Resource):
            jid = item.jid
        else:
            self.information('No JID selected.', 'Error')
            return
        self.core.register_task(
            self.core.command.last_activity(str(jid))
        )

    def resize(self) -> None:
        self.need_resize = False
        if self.size.tab_degrade_x:
            display_info = False
            roster_width = self.width
            display_contact_win = False
            contact_win_h = 0
        else:
            display_info = True
            roster_width = self.width // 2
            if self.size.tab_degrade_y:
                display_contact_win = False
                contact_win_h = 0
            else:
                display_contact_win = True
                contact_win_h = 8
        if self.size.tab_degrade_y:
            tab_win_height = 0
        else:
            tab_win_height = Tab.tab_win_height()

        info_width = self.width - roster_width - 1
        if display_info:
            self.v_separator.resize(self.height - 1 - tab_win_height, 1, 0,
                                    roster_width)
            self.information_win.resize(
                self.height - 1 - tab_win_height - contact_win_h, info_width,
                0, roster_width + 1, self.core.information_buffer,
                force=self.ui_config_changed)
            self.ui_config_changed = False
            if display_contact_win:
                y = self.height - tab_win_height - contact_win_h - 1
                avatar_width = contact_win_h * 2
                self.contact_info_win.resize(contact_win_h,
                                             info_width - avatar_width, y,
                                             roster_width + 1)
                self.avatar_win.resize(contact_win_h, avatar_width, y,
                                       self.width - avatar_width)
        self.roster_win.resize(self.height - 1 - Tab.tab_win_height(),
                               roster_width, 0, 0)
        self.input.resize(1, self.width, self.height - 1, 0)
        self.default_help_message.resize(1, self.width, self.height - 1, 0)

    def completion_file(self, complete_number: int, the_input: 'Input') -> Completion | None:
        """
        Generic quoted completion for files/paths
        (use functools.partial to use directly as a completion
        for a command)
        """
        text = the_input.get_text()
        args = shell_split(text)
        n = the_input.get_argument_position()
        if n == complete_number:
            if args[n - 1] == '' or len(args) < n + 1:
                home = os.getenv('HOME', default='/')
                return Completion(
                    the_input.new_completion, [home, '/tmp'], n, quotify=True)
            path_ = Path(args[n])
            if path_.is_dir():
                dir_ = path_
                base = ''
            else:
                dir_ = path_.parent
                base = path_.name
            try:
                names = list(dir_.iterdir())
            except OSError:
                names = []
            names_filtered = [
                name for name in names if str(name).startswith(base)
            ]
            if names_filtered:
                names = names_filtered
            if not names:
                names = [path_]
            end_list = []
            for name in names:
                if not str(name).startswith('.'):
                    value = dir_ / name
                    end_list.append(str(value))

            return Completion(
                the_input.new_completion, end_list, n, quotify=True)
        return None

    @command_args_parser.ignored
    def command_clear(self) -> None:
        """
        /clear
        """
        self.core.information_buffer.messages = []
        self.information_win.rebuild_everything(self.core.information_buffer)
        self.core.information_win.rebuild_everything(
            self.core.information_buffer)
        self.refresh()

    @deny_anonymous
    @command_args_parser.quoted(1)
    async def command_password(self, args: list[str] | None) -> None:
        """
        /password <password>
        """
        if not args:
            self.information('No password provided.', 'Error')
            return
        try:
            await self.core.xmpp.plugin['xep_0077'].change_password(
                args[0]
            )
            self.information('Password updated', 'Account')
            if config.getstr('password'):
                config.silent_set('password', args[0])
        except (IqError, IqTimeout):
            self.information(
                'Unable to change the password',
                'Account',
            )

    @deny_anonymous
    @command_args_parser.quoted(1, 1)
    async def command_name(self, args: list[str] | None) -> None:
        """
        Set a name for the specified JID in your roster
        """
        if not args:
            return self.core.command.help('name')
        try:
            jid = JID(JID(args[0]).bare)
        except InvalidJID:
            self.information(f'Invalid JID: {args[0]}', 'Error')
            return
        name = args[1] if len(args) == 2 else ''

        contact = roster[jid]
        if contact is None:
            self.information('No such JID in roster', 'Error')
            return

        groups = set(contact.groups)
        if 'none' in groups:
            groups.remove('none')
        subscription = contact.subscription
        try:
            update_roster = self.core.xmpp.update_roster(
                jid,
                name=name,
                groups=groups,
                subscription=subscription
            )
            if update_roster:
                await update_roster
        except (IqError, IqTimeout):
            self.information('The name could not be set.', 'Error')

    @deny_anonymous
    @command_args_parser.quoted(1, 1)
    async def command_groupadd(self, args: list[str] | None) -> None:
        """
        Add the specified JID to the specified group
        """
        if not args:
            return self.core.command.help('groupadd')
        jid: str | JID
        if len(args) == 1:
            group = args[0]
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            elif isinstance(item, Resource):
                jid = item.jid
            else:
                self.core.command.help('groupadd')
                return
        else:
            try:
                jid = JID(args[0]).bare
            except InvalidJID:
                self.information(f'Invalid JID: {args[0]}', 'Error')
                return
            group = args[1]

        contact = roster[jid]
        if contact is None:
            self.information('No such JID in roster', 'Error')
            return

        new_groups = set(contact.groups)
        if group in new_groups:
            self.information('JID already in group', 'Error')
            return

        roster.modified()
        new_groups.add(group)
        try:
            new_groups.remove('none')
        except KeyError:
            pass

        name = contact.name
        subscription = contact.subscription

        try:
            update_roster = self.core.xmpp.update_roster(
                JID(jid),
                name=name,
                groups=new_groups,
                subscription=subscription,
            )
            if update_roster:
                await update_roster
            roster.update_contact_groups(str(jid))
        except (IqError, IqTimeout):
            self.information('The group could not be set.', 'Error')

    @deny_anonymous
    @command_args_parser.quoted(3)
    async def command_groupmove(self, args: list[str] | None) -> None:
        """
        Remove the specified JID from the first specified group and add it to the second one
        """
        if not args:
            return self.core.command.help('groupmove')
        try:
            jid = JID(args[0]).bare
        except InvalidJID:
            self.information(f'Invalid JID: {args[0]}', 'Error')
            return
        group_from = args[1]
        group_to = args[2]

        contact = roster[jid]
        if not contact:
            self.information('No such JID in roster', 'Error')
            return

        new_groups = set(contact.groups)
        if 'none' in new_groups:
            new_groups.remove('none')

        if group_to == 'none' or group_from == 'none':
            self.information('"none" is not a group.', 'Error')
            return

        if group_from not in new_groups:
            self.information('JID not in first group', 'Error')
            return

        if group_to in new_groups:
            self.information('JID already in second group', 'Error')
            return

        if group_to == group_from:
            self.information('The groups are the same.', 'Error')
            return

        roster.modified()
        new_groups.add(group_to)
        if 'none' in new_groups:
            new_groups.remove('none')

        new_groups.remove(group_from)
        name = contact.name
        subscription = contact.subscription
        try:
            update_roster = self.core.xmpp.update_roster(
                JID(jid),
                name=name,
                groups=new_groups,
                subscription=subscription,
            )
            if update_roster:
                await update_roster
            roster.update_contact_groups(contact)
        except (IqError, IqTimeout):
            self.information('The group could not be set', 'Error')

    @deny_anonymous
    @command_args_parser.quoted(2)
    async def command_groupremove(self, args: list[str] | None) -> None:
        """
        Remove the specified JID from the specified group
        """
        if not args:
            self.core.command.help('groupremove')
            return

        try:
            jid = JID(args[0]).bare
        except InvalidJID:
            self.information(f'Invalid JID: {args[0]}', 'Error')
            return
        group = args[1]

        contact = roster[jid]
        if contact is None:
            self.information('No such JID in roster', 'Error')
            return

        new_groups = set(contact.groups)
        try:
            new_groups.remove('none')
        except KeyError:
            pass
        if group not in new_groups:
            self.information('JID not in group', 'Error')
            return

        roster.modified()

        new_groups.remove(group)
        name = contact.name
        subscription = contact.subscription
        try:
            update_roster = self.core.xmpp.update_roster(
                JID(jid),
                name=name,
                groups=new_groups,
                subscription=subscription,
            )
            if update_roster:
                await update_roster
            roster.update_contact_groups(jid)
        except (IqError, IqTimeout):
            self.information('The group could not be set')

    @deny_anonymous
    @command_args_parser.quoted(0, 1)
    def command_import(self, args: list[str]) -> None:
        """
        Import the contacts
        """
        if args:
            if args[0].startswith('/'):
                filepath = args[0]
            else:
                filepath = path.join(getenv('HOME', ''), args[0])
        else:
            filepath = path.join(getenv('HOME', ''), 'poezio_contacts')
        if not path.isfile(filepath):
            self.information('The file %s does not exist' % filepath,
                             'Error')
            return
        try:
            handle = open(filepath, 'r', encoding='utf-8')
            lines = handle.readlines()
            handle.close()
        except IOError:
            self.information('Could not open %s' % filepath, 'Error')
            log.exception('Unable to correct a message')
            return
        for jid in lines:
            self.core.command.add(jid.lstrip('\n'))
        self.information('Contacts imported from %s' % filepath, 'Info')

    @deny_anonymous
    @command_args_parser.quoted(0, 1)
    def command_export(self, args: list[str]) -> None:
        """
        Export the contacts
        """
        if args:
            if args[0].startswith('/'):
                filepath = args[0]
            else:
                filepath = path.join(getenv('HOME', ''), args[0])
        else:
            filepath = path.join(getenv('HOME', ''), 'poezio_contacts')
        if path.isfile(filepath):
            self.information('The file already exists', 'Error')
            return
        elif not path.isdir(path.dirname(filepath)):
            self.information('Parent directory not found', 'Error')
            return
        if roster.export(filepath):
            self.information('Contacts exported to %s' % filepath, 'Info')
        else:
            self.information(
                'Failed to export contacts to %s' % filepath,
                'Info',
            )

    def completion_remove(self, the_input: 'Input') -> Completion:
        """
        Completion for /remove
        """
        jids = [str(jid) for jid in roster.jids()]
        return Completion(the_input.auto_completion, jids, '', quotify=False)

    def completion_name(self, the_input: 'Input') -> Completion | None:
        """Completion for /name"""
        n = the_input.get_argument_position()
        if n == 1:
            jids = [str(jid) for jid in roster.jids()]
            return Completion(the_input.new_completion, jids, n, quotify=True)
        return None

    def completion_groupadd(self, the_input: 'Input') -> Completion | None:
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(str(jid) for jid in roster.jids())
            return Completion(
                the_input.new_completion, jids, n, '', quotify=True)
        elif n == 2:
            groups = sorted(
                group for group in roster.groups if group != 'none')
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
        return None

    def completion_groupmove(self, the_input: 'Input') -> Completion | None:
        args = shell_split(the_input.text)
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(str(jid) for jid in roster.jids())
            return Completion(
                the_input.new_completion, jids, n, '', quotify=True)
        elif n == 2:
            contact = roster[args[1]]
            if not contact:
                return None
            groups = list(contact.groups)
            if 'none' in groups:
                groups.remove('none')
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
        elif n == 3:
            groups = sorted(group for group in roster.groups)
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
        return None

    def completion_groupremove(self, the_input: 'Input') -> Completion | None:
        args = shell_split(the_input.text)
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(str(jid) for jid in roster.jids())
            return Completion(
                the_input.new_completion, jids, n, '', quotify=True)
        elif n == 2:
            contact = roster[args[1]]
            if contact is None:
                return None
            groups = sorted(contact.groups)
            try:
                groups.remove('none')
            except ValueError:
                pass
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
        return None

    def refresh(self) -> None:
        if self.need_resize:
            self.resize()
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)

        display_info = not self.size.tab_degrade_x
        display_contact_win = not self.size.tab_degrade_y

        self.roster_win.refresh(roster)
        if display_info:
            self.v_separator.refresh()
            self.information_win.refresh()
            if display_contact_win:
                row = self.roster_win.get_selected_row()
                if row:
                    self.contact_info_win.refresh(row)
                    if isinstance(row, Contact):
                        self.avatar_win.refresh(row.avatar)
                    else:
                        self.avatar_win.refresh(None)
        self.refresh_tab_win()
        self.input.refresh()

    def on_input(self, key: str, raw: bool) -> None:
        selected_row = None
        if key == '^M':
            selected_row = self.roster_win.get_selected_row()
        res = self.input.do_command(key, raw=raw)
        if res:
            return
        if key == '^M':
            if selected_row:
                self.core.on_roster_enter_key(selected_row)
            return
        elif not raw and key in self.key_func:
            self.key_func[key]()
            return

    @refresh_wrapper.conditional
    def toggle_offline_show(self) -> bool:
        """
        Show or hide offline contacts
        """
        option = 'roster_show_offline'
        value = config.getbool(option)
        success = config.silent_set(option, str(not value))
        roster.modified()
        if not success:
            self.information(
                'Unable to write in the config file',
                'Error',
            )
        return True

    def on_lose_focus(self) -> None:
        self.state = 'normal'

    def on_gain_focus(self) -> None:
        self.state = 'current'
        if isinstance(self.input, windows.HelpText):
            curses.curs_set(0)
        else:
            curses.curs_set(1)

    @refresh_wrapper.conditional
    def move_cursor_down(self) -> bool:
        if isinstance(self.input,
                      windows.Input) and not self.input.history_disabled:
            return False
        return self.roster_win.move_cursor_down()

    @refresh_wrapper.conditional
    def move_cursor_up(self) -> bool:
        if isinstance(self.input,
                      windows.Input) and not self.input.history_disabled:
            return False
        return self.roster_win.move_cursor_up()

    def move_cursor_to_prev_contact(self) -> None:
        self.roster_win.move_cursor_up()
        while not isinstance(self.roster_win.get_selected_row(), Contact):
            if not self.roster_win.move_cursor_up():
                break
        self.roster_win.refresh(roster)

    def move_cursor_to_next_contact(self) -> None:
        self.roster_win.move_cursor_down()
        while not isinstance(self.roster_win.get_selected_row(), Contact):
            if not self.roster_win.move_cursor_down():
                break
        self.roster_win.refresh(roster)

    def move_cursor_to_prev_group(self) -> None:
        self.roster_win.move_cursor_up()
        while not isinstance(self.roster_win.get_selected_row(), RosterGroup):
            if not self.roster_win.move_cursor_up():
                break
        self.roster_win.refresh(roster)

    def move_cursor_to_next_group(self) -> None:
        self.roster_win.move_cursor_down()
        while not isinstance(self.roster_win.get_selected_row(), RosterGroup):
            if not self.roster_win.move_cursor_down():
                break
        self.roster_win.refresh(roster)

    def on_scroll_down(self) -> bool:
        return self.roster_win.move_cursor_down(self.height // 2)

    def on_scroll_up(self) -> bool:
        return self.roster_win.move_cursor_up(self.height // 2)

    @refresh_wrapper.conditional
    def on_space(self) -> bool:
        if isinstance(self.input, windows.Input):
            return False
        selected_row = self.roster_win.get_selected_row()
        if isinstance(selected_row, RosterGroup):
            selected_row.toggle_folded()
            roster.modified()
            return True
        elif isinstance(selected_row, Contact):
            group = "none"
            found_group = False
            pos = self.roster_win.pos
            while not found_group and pos >= 0:
                row = self.roster_win.roster_cache[pos]
                pos -= 1
                if isinstance(row, RosterGroup):
                    found_group = True
                    group = row.name
            selected_row.toggle_folded(group)
            roster.modified()
            return True
        return False

    def get_contact_version(self) -> None:
        """
        Show the versions of the resource(s) currently selected
        """
        selected_row = self.roster_win.get_selected_row()
        if isinstance(selected_row, Contact):
            for resource in selected_row.resources:
                self.core.register_task(
                    self.core.command.version(str(resource.jid))
                )
        elif isinstance(selected_row, Resource):
            self.core.register_task(
                self.core.command.version(str(selected_row.jid))
            )
        else:
            self.information('Nothing to get versions from', 'Info')

    def show_contact_info(self) -> None:
        """
        Show the contact info (resource number, status, presence, etc)
        when 'i' is pressed.
        """
        selected_row = self.roster_win.get_selected_row()
        if isinstance(selected_row, Contact):
            cont = selected_row
            res = selected_row.get_highest_priority_resource()
            acc = []
            acc.append('Contact: %s (%s)' % (cont.bare_jid, res.presence
                                             if res else 'unavailable'))
            if res:
                acc.append(
                    '%s connected resource%s' % (len(cont), ''
                                                 if len(cont) == 1 else 's'))
                acc.append('Current status: %s' % res.status)
            msg = '\n'.join(acc)
        elif isinstance(selected_row, Resource):
            res = selected_row
            msg = 'Resource: %s (%s)\nCurrent status: %s\nPriority: %s' % (
                res.jid, res.presence, res.status, res.priority)
        elif isinstance(selected_row, RosterGroup):
            rg = selected_row
            msg = 'Group: %s [%s/%s] contacts online' % (
                rg.name,
                rg.get_nb_connected_contacts(),
                len(rg),
            )
        else:
            msg = None
        if msg:
            self.information(msg, 'Info')

    def change_contact_name(self) -> None:
        """
        Auto-fill a /name command when 'n' is pressed
        """
        selected_row = self.roster_win.get_selected_row()
        if isinstance(selected_row, Contact):
            jid = selected_row.bare_jid
        elif isinstance(selected_row, Resource):
            jid = JID(JID(selected_row.jid).bare)
        else:
            return
        self.on_slash()
        self.input.text = '/name "%s" ' % jid
        self.input.key_end()
        self.input.refresh()

    @refresh_wrapper.always
    def start_search(self) -> bool:
        """
        Start the search. The input should appear with a short instruction
        in it.
        """
        curses.curs_set(1)
        self.input = windows.CommandInput("[Search]", self.on_search_terminate,
                                          self.on_search_terminate,
                                          self.set_roster_filter)
        self.input.resize(1, self.width, self.height - 1, 0)
        self.input.disable_history()
        roster.modified()
        self.refresh()
        return True

    @refresh_wrapper.always
    def start_search_slow(self) -> bool:
        curses.curs_set(1)
        self.input = windows.CommandInput("[Search]", self.on_search_terminate,
                                          self.on_search_terminate,
                                          self.set_roster_filter_slow)
        self.input.resize(1, self.width, self.height - 1, 0)
        self.input.disable_history()
        return True

    def set_roster_filter_slow(self, txt: str) -> bool:
        roster.contact_filter = (jid_and_name_match_slow, txt)
        roster.modified()
        self.refresh()
        return False

    def set_roster_filter(self, txt: str) -> bool:
        roster.contact_filter = (jid_and_name_match, txt)
        roster.modified()
        self.refresh()
        return False

    @refresh_wrapper.always
    def on_search_terminate(self, txt: str) -> bool:
        curses.curs_set(0)
        roster.contact_filter = roster.DEFAULT_FILTER
        self.reset_help_message()
        roster.modified()
        return True

    def on_close(self) -> None:
        return


def diffmatch(search: str, string: str) -> bool:
    """
    Use difflib and a loop to check if search_pattern can
    be 'almost' found INSIDE a string.
    'almost' being defined by difflib
    """
    if len(search) > len(string):
        return False
    length = len(search)
    ratio = 0.7
    for i in range(len(string) - length + 1):
        if difflib.SequenceMatcher(None, search,
                                   string[i:i + length]).ratio() >= ratio:
            return True
    return False


def jid_and_name_match(contact: Contact, txt: str) -> bool:
    """
    Match jid with text precisely
    """
    if not txt:
        return True
    txt = txt.lower()
    try:
        if txt in JID(contact.bare_jid).bare.lower():
            return True
    except InvalidJID:
        pass
    if txt in contact.name.lower():
        return True
    return False


def jid_and_name_match_slow(contact: Contact, txt: str) -> bool:
    """
    A function used to know if a contact in the roster should
    be shown in the roster
    """
    if not txt:
        return True  # Everything matches when search is empty
    try:
        user = JID(contact.bare_jid).bare
        if diffmatch(txt, user):
            return True
    except InvalidJID:
        pass
    if contact.name and diffmatch(txt, contact.name):
        return True
    return False
