"""authentik LDAP Authentication Backend"""

from django.http import HttpRequest
from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
from structlog.stdlib import get_logger

from authentik.core.auth import InbuiltBackend
from authentik.core.models import User
from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAPSource

LOGGER = get_logger()


class LDAPBackend(InbuiltBackend):
    """Authenticate users against LDAP Server"""

    def authenticate(self, request: HttpRequest, **kwargs):
        """Try to authenticate a user via ldap"""
        if "password" not in kwargs:
            return None
        for source in LDAPSource.objects.filter(enabled=True):
            LOGGER.debug("LDAP Auth attempt", source=source)
            user = self.auth_user(request, source, **kwargs)
            if user:
                self.set_method("ldap", request, source=source)
                return user
        return None

    def auth_user(
        self, request: HttpRequest, source: LDAPSource, password: str, **filters: str
    ) -> User | None:
        """Try to bind as either user_dn or mail with password.
        Returns True on success, otherwise False"""
        users = User.objects.filter(**filters)
        if not users.exists():
            return None
        user: User = users.first()
        if LDAP_DISTINGUISHED_NAME not in user.attributes:
            LOGGER.debug("User doesn't have DN set, assuming not LDAP imported.", user=user)
            return None
        # Either has unusable password,
        # or has a password, but couldn't be authenticated by ModelBackend.
        # This means we check with a bind to see if the LDAP password has changed
        if self.auth_user_by_bind(source, user, password):
            if source.password_login_update_internal_password:
                # Password given successfully binds to LDAP, so we save it in our Database
                LOGGER.debug("Updating user's password in DB", user=user)
                user.set_password(password, sender=source, request=request)
                user.save()
            return user
        # Password doesn't match
        LOGGER.debug("Failed to bind, password invalid")
        return None

    def auth_user_by_bind(self, source: LDAPSource, user: User, password: str) -> User | None:
        """Attempt authentication by binding to the LDAP server as `user`. This
        method should be avoided as its slow to do the bind."""
        # Try to bind as new user
        LOGGER.debug("Attempting to bind as user", user=user)
        try:
            # source.connection also attempts to bind
            source.connection(
                connection_kwargs={
                    "user": user.attributes.get(LDAP_DISTINGUISHED_NAME),
                    "password": password,
                }
            )
            return user
        except LDAPInvalidCredentialsResult as exc:
            LOGGER.debug("invalid LDAP credentials", user=user, exc=exc)
        except LDAPException as exc:
            LOGGER.warning("failed to bind to LDAP", exc=exc)
        return None
