# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
# it under the terms of the GPL-3.0+ license. See the COPYING file.
"""
Defines the Roster and RosterGroup classes
"""
import logging

from poezio.config import config
from poezio.contact import Contact
from poezio.roster_sorting import SORTING_METHODS, GROUP_SORTING_METHODS

from os import path as p
from collections.abc import Iterator
from datetime import datetime
from slixmpp.exceptions import IqError, IqTimeout
from slixmpp.roster.single import RosterNode
from slixmpp import JID, InvalidJID
from typing import Callable

log = logging.getLogger(__name__)


class Roster:
    """
    The proxy class to get the roster from slixmpp.
    Caches Contact and RosterGroup objects.
    """

    DEFAULT_FILTER = (lambda _, __: True, '')

    __node: RosterNode
    length: int
    connected: int
    folded_groups: set[str]
    last_built: datetime
    last_modified: datetime
    groups: dict[str, 'RosterGroup']
    contacts: dict[str | JID, Contact]
    contact_filter: tuple[
        Callable[[Contact, str], bool],
        str,
    ]

    def __init__(self) -> None:
        self.__node = RosterNode(None, None)  # type: ignore

        # A tuple(function, *args) function to filter contacts
        # on search, for example
        self.contact_filter = self.DEFAULT_FILTER
        self.groups = {}
        self.contacts = {}
        self.length = 0
        self.connected = 0
        self.folded_groups = set()

        # Used for caching roster infos
        self.last_built = datetime.now()
        self.last_modified = datetime.now()

    def reset(self) -> None:
        """
        node: the RosterSingle from slixmpp
        """
        self.__node = RosterNode(None, None)  # type: ignore

        # A tuple(function, *args) function to filter contacts
        # on search, for example
        self.contact_filter = self.DEFAULT_FILTER
        self.folded_groups = set(
            config.getlist('folded_roster_groups', section='var')
        )
        self.groups = {}
        self.contacts = {}
        self.length = 0
        self.connected = 0

        # Used for caching roster infos
        self.last_built = datetime.now()
        self.last_modified = datetime.now()

    def modified(self) -> None:
        self.last_modified = datetime.now()

    @property
    def needs_rebuild(self) -> bool:
        return self.last_modified >= self.last_built

    def __getitem__(self, key: str | JID) -> Contact | None:
        """Get a Contact from his bare JID"""
        try:
            key = JID(key).bare
        except InvalidJID:
            return None
        if key in self.contacts and self.contacts[key] is not None:
            return self.contacts[key]
        if key in self.jids():
            contact = Contact(self.__node[key])
            self.contacts[key] = contact
            return contact
        return None

    def __setitem__(self, key: str | JID, value: Contact) -> None:
        """Set the a Contact value for the bare jid key"""
        self.contacts[key] = value

    def remove(self, jid: str | JID) -> None:
        """Send a removal iq to the server"""
        try:
            jid = JID(jid).bare
        except InvalidJID:
            return
        if self.__node[jid]:
            try:
                self.__node[jid].send_presence(ptype='unavailable')
                self.__node.remove(jid)
            except (IqError, IqTimeout):
                log.debug('IqError when removing %s:', jid, exc_info=True)

    def __delitem__(self, jid: str | JID) -> None:
        """Remove a contact from the roster view"""
        try:
            jid = JID(jid).bare
        except InvalidJID:
            return
        contact = self[jid]
        if not contact:
            return
        del self.contacts[contact.bare_jid]

        for group in list(self.groups.values()):
            group.remove(contact)
            if not group:
                del self.groups[group.name]
        self.modified()

    def __iter__(self) -> Iterator[Contact]:
        """Iterate over the jids of the contacts"""
        return iter(self.contacts.values())

    def __contains__(self, key: str | JID) -> bool:
        """True if the bare jid is in the roster, false otherwise"""
        try:
            return JID(key).bare in self.jids()
        except InvalidJID:
            return False

    @property
    def jid(self) -> JID | str:
        """Our JID"""
        return self.__node.jid

    def get_and_set(self, jid: str | JID) -> Contact:
        contact = self.contacts.get(jid)
        if contact is None:
            contact = Contact(self.__node[jid])
            self.contacts[jid] = contact
            return contact
        return contact

    def set_node(self, value: RosterNode) -> None:
        """Set the slixmpp RosterSingle for our roster"""
        self.__node = value

    def get_groups(self, sort: str = '') -> list['RosterGroup']:
        """Return a list of the RosterGroups"""
        group_list = sorted(
            (group for group in self.groups.values() if group),
            key=lambda x: x.name.lower() if x.name else '')

        for sorting in sort.split(':'):
            if sorting == 'reverse':
                group_list = list(reversed(group_list))
            else:
                method = GROUP_SORTING_METHODS.get(sorting, lambda _: 0)
                group_list = sorted(group_list, key=method)
        return group_list

    def get_group(self, name: str) -> 'RosterGroup':
        """Return a group or create it if not present"""
        if name in self.groups:
            return self.groups[name]
        group = RosterGroup(
            name, folded=name in self.folded_groups
        )
        self.groups[name] = group
        return group

    def add(self, jid: str | JID) -> None:
        """Subscribe to a jid"""
        self.__node.subscribe(jid)

    def jids(self) -> list[JID | str]:
        """List of the contact JIDs"""
        jids: list[str | JID] = []
        for key in self.__node.keys():
            contact = self.get_and_set(key)
            if key and key != self.jid and (contact and self.exists(contact)):
                jids.append(key)
        self.update_size(jids)
        return jids

    def update_size(self, jids: list[JID | str] | None = None) -> None:
        if jids is None:
            jids = self.jids()
        self.length = len(jids)

    def get_contacts(self) -> list[Contact]:
        """
        Return a list of all the contacts
        """
        return [self.contacts[jid] for jid in self.jids()]

    def get_contacts_sorted_filtered(self, sort: str = '') -> list[Contact]:
        """
        Return a list of all the contacts sorted with a criteria
        """
        contact_list = []
        for contact in self.get_contacts():
            if contact.bare_jid != self.jid:
                if self.contact_filter is not self.DEFAULT_FILTER:
                    if self.contact_filter[0](contact, self.contact_filter[1]):
                        contact_list.append(contact)
                else:
                    contact_list.append(contact)
        contact_list = sorted(contact_list, key=SORTING_METHODS['name'])

        for sorting in sort.split(':'):
            if sorting == 'reverse':
                contact_list = list(reversed(contact_list))
            else:
                method = SORTING_METHODS.get(sorting, lambda _: 0)
                contact_list = sorted(contact_list, key=method)
        return contact_list

    def save_to_config_file(self) -> bool:
        """
        Save various information to the config file
        e.g. the folded groups
        """
        folded_groups = ':'.join([
            group.name for group in self.groups.values()
            if group.folded
        ])
        log.debug('folded:%s\n', folded_groups)
        return config.silent_set('folded_roster_groups', folded_groups, 'var')

    def get_nb_connected_contacts(self) -> int:
        """
        Get the number of connected contacts
        """
        return self.connected

    def update_contact_groups(self, contact: Contact | str) -> None:
        """Regenerate the RosterGroups when receiving a contact update"""
        if not isinstance(contact, Contact):
            contact = self.get_and_set(contact)
        if not contact:
            return
        for name, group in self.groups.items():
            if name in contact.groups and contact not in group:
                group.add(contact)
            elif contact in group and name not in contact.groups:
                group.remove(contact)

        for name in contact.groups:
            if name not in self.groups:
                self.groups[name] = RosterGroup(
                    name, folded=name in self.folded_groups)
                self.groups[name].add(contact)

    def __len__(self) -> int:
        """
        Return the number of contacts
        (used to return the display size, but now we have
        the display cache in RosterWin for that)
        """
        return self.length

    def __repr__(self) -> str:
        ret = '== Roster:\nContacts:\n'
        for contact in self.contacts.values():
            ret += '%s\n' % (contact, )
        ret += 'Groups\n'
        for group in self.groups:
            ret += '%s\n' % (group, )
        return ret + '\n'

    def export(self, path: str) -> bool:
        """Export a list of bare jids to a given file"""
        if p.isfile(path):
            return False
        try:
            f = open(path, 'w+', encoding='utf-8')
            f.writelines([
                str(i) + "\n" for i in self.contacts
                if self.contacts[i] and (self.contacts[i].subscription == "both" or self.contacts[i].ask)
            ])
            f.close()
        except OSError:
            return False
        return True

    def exists(self, contact: Contact | None) -> bool:
        if not contact:
            return False
        for group in contact.groups:
            if contact not in self.groups.get(group, tuple()):
                return False
        return True


class RosterGroup:
    """
    A RosterGroup is a group containing contacts
    It can be Friends/Family etc, but also can be
    Online/Offline or whatever
    """

    def __init__(self, name: str | None,
                 contacts: list[Contact] | None = None,
                 folded: bool = False) -> None:
        if not contacts:
            contacts = []
        self.contacts = set(contacts)
        self.name = name if name is not None else ''
        self.folded = folded  # if the group content is to be shown

    def __iter__(self) -> Iterator[Contact]:
        """Iterate over the contacts"""
        return iter(self.contacts)

    def __repr__(self) -> str:
        return '<Roster_group: %s; %s>' % (self.name, self.contacts)

    def __len__(self) -> int:
        """Number of contacts in the group"""
        return len(self.contacts)

    def __contains__(self, contact: Contact) -> bool:
        """
        Return a bool, telling if the contact is in the group
        """
        return contact in self.contacts

    def add(self, contact: Contact) -> None:
        """Add a contact to the group"""
        self.contacts.add(contact)

    def remove(self, contact: Contact) -> None:
        """Remove a contact from the group if present"""
        if contact in self.contacts:
            self.contacts.remove(contact)

    def get_contacts(self,
                     contact_filter: tuple[
                         Callable[[Contact, str], bool], str
                     ] | None = None,
                     sort: str = '') -> list[Contact]:
        """Return the group contacts, filtered and sorted"""
        if contact_filter is Roster.DEFAULT_FILTER or contact_filter is None:
            contact_list = list(self.contacts)
        else:
            contact_list = [
                contact for contact in self.contacts
                if contact_filter[0](contact, contact_filter[1])
            ]
        contact_list = sorted(contact_list, key=SORTING_METHODS['name'])

        for sorting in sort.split(':'):
            if sorting == 'reverse':
                contact_list = list(reversed(contact_list))
            else:
                method = SORTING_METHODS.get(sorting, lambda _: 0)
                contact_list = sorted(contact_list, key=method)
        return contact_list

    def toggle_folded(self) -> None:
        """Fold/unfold the group in the roster"""
        self.folded = not self.folded
        if self.folded:
            if self.name not in roster.folded_groups:
                roster.folded_groups.add(self.name)
        else:
            if self.name in roster.folded_groups:
                roster.folded_groups.remove(self.name)

    def get_nb_connected_contacts(self) -> int:
        """Return the number of connected contacts"""
        return len([1 for contact in self.contacts if len(contact)])


# Shared roster object
roster = Roster()
