import collections
import copy
import inspect
import json
import re

# Python LDAP
import ldap
import awx

# Django
from django.utils import six
from django.utils.translation import ugettext_lazy as _

# Django Auth LDAP
import django_auth_ldap.config
from django_auth_ldap.config import (
    LDAPSearch,
    LDAPSearchUnion,
)

from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty, Field, SkipField

# This must be imported so get_subclasses picks it up
from awx.sso.ldap_group_types import PosixUIDGroupType  # noqa

# Tower
from awx.conf import fields
from awx.main.validators import validate_certificate
from awx.sso.validators import (  # noqa
    validate_ldap_dn,
    validate_ldap_bind_dn,
    validate_ldap_dn_with_user,
    validate_ldap_filter,
    validate_ldap_filter_with_user,
    validate_tacacsplus_disallow_nonascii,
)


def get_subclasses(cls):
    for subclass in cls.__subclasses__():
        for subsubclass in get_subclasses(subclass):
            yield subsubclass
        yield subclass


def find_class_in_modules(class_name):
    '''
    Used to find ldap subclasses by string
    '''
    module_search_space = [django_auth_ldap.config, awx.sso.ldap_group_types]
    for m in module_search_space:
        cls = getattr(m, class_name, None)
        if cls:
            return cls
    return None


class DependsOnMixin():
    def get_depends_on(self):
        """
        Get the value of the dependent field.
        First try to find the value in the request.
        Then fall back to the raw value from the setting in the DB.
        """
        from django.conf import settings
        dependent_key = next(iter(self.depends_on))

        if self.context:
            request = self.context.get('request', None)
            if request and request.data and \
                    request.data.get(dependent_key, None):
                return request.data.get(dependent_key)
        res = settings._get_local(dependent_key, validate=False)
        return res


class _Forbidden(Field):
    default_error_messages = {
        'invalid': _('Invalid field.'),
    }

    def run_validation(self, value):
        self.fail('invalid')


class HybridDictField(fields.DictField):
    """A DictField, but with defined fixed Fields for certain keys.
    """

    def __init__(self, *args, **kwargs):
        self.allow_blank = kwargs.pop('allow_blank', False)

        fields = [
            sorted(
                ((field_name, obj) for field_name, obj in cls.__dict__.items()
                 if isinstance(obj, Field) and field_name != 'child'),
                key=lambda x: x[1]._creation_counter
            )
            for cls in reversed(self.__class__.__mro__)
        ]
        self._declared_fields = collections.OrderedDict(f for group in fields for f in group)

        super().__init__(*args, **kwargs)

    def to_representation(self, value):
        fields = copy.deepcopy(self._declared_fields)
        return {
            key: field.to_representation(val) if val is not None else None
            for key, val, field in (
                (six.text_type(key), val, fields.get(key, self.child))
                for key, val in value.items()
            )
            if not field.write_only
        }

    def run_child_validation(self, data):
        result = {}

        if not data and self.allow_blank:
            return result

        errors = collections.OrderedDict()
        fields = copy.deepcopy(self._declared_fields)
        keys = set(fields.keys()) | set(data.keys())

        for key in keys:
            value = data.get(key, empty)
            key = six.text_type(key)
            field = fields.get(key, self.child)
            try:
                if field.read_only:
                    continue  # Ignore read_only fields, as Serializer seems to do.
                result[key] = field.run_validation(value)
            except ValidationError as e:
                errors[key] = e.detail
            except SkipField:
                pass

        if not errors:
            return result
        raise ValidationError(errors)


class AuthenticationBackendsField(fields.StringListField):

    # Mapping of settings that must be set in order to enable each
    # authentication backend.
    REQUIRED_BACKEND_SETTINGS = collections.OrderedDict([
        ('awx.sso.backends.LDAPBackend', [
            'AUTH_LDAP_SERVER_URI',
        ]),
        ('awx.sso.backends.LDAPBackend1', [
            'AUTH_LDAP_1_SERVER_URI',
        ]),
        ('awx.sso.backends.LDAPBackend2', [
            'AUTH_LDAP_2_SERVER_URI',
        ]),
        ('awx.sso.backends.LDAPBackend3', [
            'AUTH_LDAP_3_SERVER_URI',
        ]),
        ('awx.sso.backends.LDAPBackend4', [
            'AUTH_LDAP_4_SERVER_URI',
        ]),
        ('awx.sso.backends.LDAPBackend5', [
            'AUTH_LDAP_5_SERVER_URI',
        ]),
        ('awx.sso.backends.RADIUSBackend', [
            'RADIUS_SERVER',
        ]),
        ('social_core.backends.google.GoogleOAuth2', [
            'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY',
            'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET',
        ]),
        ('social_core.backends.github.GithubOAuth2', [
            'SOCIAL_AUTH_GITHUB_KEY',
            'SOCIAL_AUTH_GITHUB_SECRET',
        ]),
        ('social_core.backends.github.GithubOrganizationOAuth2', [
            'SOCIAL_AUTH_GITHUB_ORG_KEY',
            'SOCIAL_AUTH_GITHUB_ORG_SECRET',
            'SOCIAL_AUTH_GITHUB_ORG_NAME',
        ]),
        ('social_core.backends.github.GithubTeamOAuth2', [
            'SOCIAL_AUTH_GITHUB_TEAM_KEY',
            'SOCIAL_AUTH_GITHUB_TEAM_SECRET',
            'SOCIAL_AUTH_GITHUB_TEAM_ID',
        ]),
        ('social_core.backends.azuread.AzureADOAuth2', [
            'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY',
            'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET',
        ]),
        ('awx.sso.backends.SAMLAuth', [
            'SOCIAL_AUTH_SAML_SP_ENTITY_ID',
            'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT',
            'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY',
            'SOCIAL_AUTH_SAML_ORG_INFO',
            'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT',
            'SOCIAL_AUTH_SAML_SUPPORT_CONTACT',
            'SOCIAL_AUTH_SAML_ENABLED_IDPS',
        ]),
        ('django.contrib.auth.backends.ModelBackend', []),
    ])

    @classmethod
    def get_all_required_settings(cls):
        all_required_settings = set(['LICENSE'])
        for required_settings in cls.REQUIRED_BACKEND_SETTINGS.values():
            all_required_settings.update(required_settings)
        return all_required_settings

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('default', self._default_from_required_settings)
        super(A
uthenticationBackendsField, self).__init__(*args, **kwargs)

    def _default_from_required_settings(self):
        from django.conf im
port settings
        try:
            backends = settings._awx_conf_settings._get_default('AUTHENTICATION_BACKENDS')
        except AttributeError:
            backends = self.REQUIRED_BACKEND_SETTINGS.keys()
        # Filter which authentication backends are enabled based on their
        # required settings being defined and non-empty.
        for backend, required_settings in self.REQUIRED_BACKEND_SETTINGS.items():
            if backend not in backends:
                continue
            if all([getattr(settings, rs, None) for rs in required_settings]):
                continue
            backends = [x for x in backends if x != backend]
        return backends


class LDAPServerURIField(fields.URLField):

    def __init__(self, **kwargs):
        kwargs.setdefault('schemes', ('ldap', 'ldaps'))
        kwargs.setdefault('allow_plain_hostname', True)
        super(LDAPServerURIField, self).__init__(**kwargs)

    def run_validators(self, value):
        for url in filter(None, re.split(r'[, ]', (value or ''))):
            super(LDAPServerURIField, self).run_validators(url)
        return value


class LDAPConnectionOptionsField(fields.DictField):

    default_error_messages = {
        'invalid_options': _('Invalid connection option(s): {invalid_options}.'),
    }

    def to_representation(