from logging import getLogger
from string import ascii_lowercase

try:
    from ldap import REFERRAL
    from node.ext.ldap.interfaces import ILDAPGroupsConfig
    from node.ext.ldap.interfaces import ILDAPUsersConfig
    from node.ext.ldap.ugm import Ugm
except ImportError:
    ILDAPGroupsConfig = ILDAPUSersConfig = Ugm = None

    class REFERRAL(Exception):
        """dummy"""


from plone import api
from plone.memoize.view import memoize_contextless
from plone.namedfile.file import NamedBlobImage
from ploneintranet import api as pi_api
from ploneintranet.userprofile.browser.sync.base import BaseSyncManyView
from ploneintranet.userprofile.browser.sync.base import BaseSyncView
from ploneintranet.userprofile.browser.sync.base import NO_VALUE

logger = getLogger(__name__)


class SyncProperties(BaseSyncView):
    """This view wants to sync the Plone user properties to match the ones
    recorded in the external source
    """

    @property
    def property_mapping(self):
        return api.portal.get_registry_record(
            "ploneintranet.userprofile.property_sheet_mapping"
        )

    def _sync(self):
        if not self.external_source:
            return

        member = api.user.get(username=self.context.username)
        if not member:
            logger.info(f"Cannot find {self.context.username}")
            return

        try:
            sheet = self.external_source.getPropertiesForUser(member)
        except REFERRAL:
            sheet = None
        if not sheet:
            logger.warning("No info for %r in %r", self.context, self.external_source)
            return

        changed = False

        for property_name in self.property_mapping:
            value = sheet.getProperty(property_name, default=NO_VALUE)
            current_value = getattr(self.context, property_name, None)

            # Special handling for portraits, they need wrapping
            if property_name == "portrait":
                if value and value is not NO_VALUE:
                    current_value = (
                        self.context.portrait and self.context.portrait.data or None
                    )
                    if current_value == value:
                        # profile image is the same, no need to save new image
                        continue
                    value = NamedBlobImage(
                        data=value, filename="portrait-%s.jpg" % self.context.username
                    )
                elif self.context.portrait is not None:
                    self.context.portrait = None

            if value is not NO_VALUE and value != current_value:
                setattr(self.context, property_name, value)
                changed = True

        if changed:
            self.record_last_sync()
            self._notify_modified()
            logger.info(f"Properties updated for {self.context.username}")


class AllUserprofilesSync(BaseSyncManyView):
    """Sync all users from the "canonical" source of external users.

    Compares local membrane profiles to the list of users found
    in the canonical pas source.
    Deactivates any profiles for users no longer in the canonical source,
    and creates profiles for any new users found in the canonical source.
    """

    @property
    @memoize_contextless
    def principals(self):
        """Limit the synchronization to the users that have been created recently
        TODO: they should be synced immediately after their creation
        """
        return self._create_missing_principals()

    @property
    @memoize_contextless
    def external_principal_ids(self):
        """Return the user ids managed by the external source

        Raise an exception if the external source returns no principals
        to avoid disabling them all
        """
        ids = {principal["id"] for principal in self.external_source.enumerateUsers()}
        # if we're unlucky, the logs would show this:
        # SIZELIMIT_EXCEEDED: {'desc': u'Size limit exceeded'}
        # This should prolly be fixed upstream, but hey. Look at that code.
        if not ids:
            logger.info("Nevermind SIZELIMIT_EXCEEDED, working around that now.")
            ids = self._external_principal_ids_sharded
        if not ids:
            raise RuntimeError(
                "The external source plugin is not returning any principal id!"
            )
        return ids

    @property
    def _external_principal_ids_sharded(self):
        """pasldap internals are so borked we are bypassing them here.

        Cache invalidation throws errors, cache disabling has no effect,
        monkeypatching does not work. Bypass enumerateUsers() completely by
        reimplementing only the required bare essentials of what we need for
        this specific use case of listing userids.

        We shard the userid space to avoid running into SIZELIMIT_EXCEEDED,
        which comes from deep in the stack, disregarding
        settings.server.page_size.
        """
        if Ugm is None:
            logger.warning("Cannot shard LDAP userids without node.ext.ldap")
            return set()

        ucfg = ILDAPUsersConfig(self.external_source)
        gcfg = ILDAPGroupsConfig(self.external_source)
        props = self.external_source._ldap_props
        ugm = Ugm(props=props, ucfg=ucfg, gcfg=gcfg, rcfg=None)

        def search(needle):
            return {
                x[0]
                for x in ugm.users.search(
                    criteria=dict(cn=f"{needle}*"),
                    attrlist=("login",),
                    exact_match=False,
                )
            }

        logger.info("Retrying LDAP query for each a,b,c in alphabet")
        ids = set()
        # this search is case insensitive
        for letter in list(ascii_lowercase):
            found = search(letter)
            if found:
                ids.update(found)
                continue
            # Do we need an inner loop? Be paranoid without DoSing ourselves.
            # This is executed only if a letter is empty, and we did find
            # something before or this is the first letter.
            if ids or letter == "a":
                logger.info("Retrying LDAP query for inner %sa, .b, .c etc", letter)
                for inner_letter in list(ascii_lowercase):
                    ids.update(search(f"{letter}{inner_letter}"))
        return ids

    @property
    @memoize_contextless
    def local_principal_ids(self):
        """Return the user ids managed by Plone"""
        return set(self.context)

    def _get_principal_by_id(self, principal_id):
        return pi_api.userprofile.get(principal_id)

    def _create_principal_by_id(self, principal_id):
        """This creates a userprofile in a state where he can already login"""
        return pi_api.userprofile.create(
            username=principal_id,
            approve=True,
            properties={"source": self.external_source_id},
        )


class AllUserprofilesPropertySync(BaseSyncManyView):
    """This view is used to trigger the synchronization of the user properties
    from the external source for all the userprofiles we have
    """

    def _sync(self):
        self._sync_many()
