# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import pprint
from copy import deepcopy

from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib.admin.utils import flatten_fieldsets
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.sessions.models import Session
from django.db import transaction
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.cache import never_cache

from . import app_settings, attribute_kinds, decorators, models
from .custom_user.models import DeletedUser, User
from .forms.profile import BaseUserForm, modelform_factory
from .nonce.models import Nonce
from .utils import misc as utils_misc


def cleanup_action(modeladmin, request, queryset):
    queryset.cleanup()


cleanup_action.short_description = _('Cleanup expired objects')


class CleanupAdminMixin(admin.ModelAdmin):
    def get_actions(self, request):
        actions = super().get_actions(request)
        if hasattr(self.model.objects.none(), 'cleanup'):
            actions['cleanup_action'] = cleanup_action, 'cleanup_action', cleanup_action.short_description
        return actions


class NonceModelAdmin(admin.ModelAdmin):
    list_display = ("value", "context", "not_on_or_after")


admin.site.register(Nonce, NonceModelAdmin)


class AttributeValueAdmin(admin.ModelAdmin):
    list_display = ('content_type', 'owner', 'attribute', 'content')


admin.site.register(models.AttributeValue, AttributeValueAdmin)


class LogoutUrlAdmin(admin.ModelAdmin):
    list_display = ('provider', 'logout_url', 'logout_use_iframe', 'logout_use_iframe_timeout')


admin.site.register(models.LogoutUrl, LogoutUrlAdmin)


class AuthenticationEventAdmin(admin.ModelAdmin):
    list_display = ('when', 'who', 'how', 'nonce')
    list_filter = ('how',)
    date_hierarchy = 'when'
    search_fields = ('who', 'nonce', 'how')


admin.site.register(models.AuthenticationEvent, AuthenticationEventAdmin)


class UserExternalIdAdmin(admin.ModelAdmin):
    list_display = ('user', 'source', 'external_id', 'created', 'updated')
    list_filter = ('source',)
    date_hierarchy = 'created'
    search_fields = ('user__username', 'source', 'external_id')


admin.site.register(models.UserExternalId, UserExternalIdAdmin)


DB_SESSION_ENGINES = (
    'django.contrib.sessions.backends.db',
    'django.contrib.sessions.backends.cached_db',
    'mellon.session_backends.cached_db',
)

if settings.SESSION_ENGINE in DB_SESSION_ENGINES:

    class SessionAdmin(admin.ModelAdmin):
        def _session_data(self, obj):
            return pprint.pformat(obj.get_decoded()).replace('\n', '<br>\n')

        _session_data.allow_tags = True
        _session_data.short_description = _('session data')
        list_display = ['session_key', 'ips', 'user', '_session_data', 'expire_date']
        fields = ['session_key', 'ips', 'user', '_session_data', 'expire_date']
        readonly_fields = ['ips', 'user', '_session_data']
        date_hierarchy = 'expire_date'
        actions = ['clear_expired']

        def ips(self, session):
            content = session.get_decoded()
            ips = content.get('ips', set())
            return ', '.join(ips)

        ips.short_description = _('IP adresses')

        def user(self, session):
            from django.contrib import auth
            from django.contrib.auth import models as auth_models

            content = session.get_decoded()
            if auth.SESSION_KEY not in content:
                return
            user_id = content[auth.SESSION_KEY]
            if auth.BACKEND_SESSION_KEY not in content:
                return
            backend_class = content[auth.BACKEND_SESSION_KEY]
            backend = auth.load_backend(backend_class)
            try:
                user = backend.get_user(user_id) or auth_models.AnonymousUser()
            except Exception:
                user = _('deleted user %r') % user_id
            return user

        user.short_description = _('user')

        def clear_expired(self, request, queryset):
            queryset.filter(expire_date__lt=timezone.now()).delete()

        clear_expired.short_description = _('clear expired sessions')

    admin.site.register(Session, SessionAdmin)


class ExternalUserListFilter(admin.SimpleListFilter):
    title = _('external')

    parameter_name = 'external'

    def lookups(self, request, model_admin):
        return (('1', _('Yes')), ('0', _('No')))

    def queryset(self, request, queryset):
        """
        Returns the filtered queryset based on the value
        provided in the query string and retrievable via
        `self.value()`.
        """
        if self.value() == '1':
            return queryset.filter(userexternalid__isnull=False)
        elif self.value() == '0':
            return queryset.filter(userexternalid__isnull=True)
        return queryset


class UserRealmListFilter(admin.SimpleListFilter):
    # Human-readable title which will be displayed in the
    # right admin sidebar just above the filter options.
    title = _('realm')

    # Parameter for the filter that will be used in the URL query.
    parameter_name = 'realm'

    def lookups(self, request, model_admin):
        """
        Returns a list of tuples. The first element in each
        tuple is the coded value for the option that will
        appear in the URL query. The second element is the
        human-readable name for the option that will appear
        in the right sidebar.
        """
        return app_settings.REALMS

    def queryset(self, request, queryset):
        """
        Returns the filtered queryset based on the value
        provided in the query string and retrievable via
        `self.value()`.
        """
        if self.value():
            return queryset.filter(username__endswith='@' + self.value())
        return queryset


class UserChangeForm(BaseUserForm):
    error_messages = {
        'missing_credential': _("You must at least give a username or an email to your user"),
    }

    password = ReadOnlyPasswordHashField(
        label=_("Password"),
        help_text=_(
            "Raw passwords are not stored, so there is no way to see "
            "this user's password, but you can change the password "
            "using <a href=\"password/\">this form</a>."
        ),
    )

    class Meta:
        model = User
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        f = self.fields.get('user_permissions', None)
        if f is not None:
            f.queryset = f.queryset.select_related('content_type')

    def clean_password(self):
        # Regardless of what the user provides, return the initial value.
        # This is done here, rather than on the field, because the
        # field does not have access to the initial value
        return self.initial["password"]

    def clean(self):
        if not self.cleaned_data.get('username') and not self.cleaned_data.get('email'):
            raise forms.ValidationError(
                self.error_messages['missing_credential'],
                code='missing_credential',
            )

    def is_field_locked(self, name):
        return False


class UserCreationForm(BaseUserForm):
    """
    A form that creates a user, with no privileges, from the given username and
    password.
    """

    error_messages = {
        'password_mismatch': _("The two password fields didn't match."),
        'missing_credential': _("You must at least give a username or an email to your user"),
    }
    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
    password2 = forms.CharField(
        label=_("Password confirmation"),
        widget=forms.PasswordInput,
        help_text=_("Enter the same password as above, for verification."),
    )

    class Meta:
        model = User
        fields = ("username",)

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError(
                self.error_messages['password_mismatch'],
                code='password_mismatch',
            )
        return password2

    def clean(self):
        if not self.cleaned_data.get('username') and not self.cleaned_data.get('email'):
            raise forms.ValidationError(
                self.error_messages['missing_credential'],
                code='missing_credential',
            )

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user


class AuthenticUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('uuid', 'ou', 'password')}),
        (_('Personal info'), {'fields': ('username', 'first_name', 'last_name', 'email')}),
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined', 'deactivation')}),
    )
    add_fieldsets = (
        (
            None,
            {
                'classes': ('wide',),
                'fields': ('ou', 'username', 'first_name', 'last_name', 'email', 'password1', 'password2'),
            },
        ),
    )
    readonly_fields = ('uuid',)
    list_filter = UserAdmin.list_filter + (UserRealmListFilter, ExternalUserListFilter)
    list_display = ['__str__', 'ou', 'first_name', 'last_name', 'email']
    actions = UserAdmin.actions + ['mark_as_inactive']

    def get_fieldsets(self, request, obj=None):
        fieldsets = deepcopy(super().get_fieldsets(request, obj))
        if obj:
            if not request.user.is_superuser:
                fieldsets[2][1]['fields'] = filter(lambda x: x != 'is_superuser', fieldsets[2][1]['fields'])
            qs = models.Attribute.objects.all()
            insertion_idx = 2
        else:
            qs = models.Attribute.objects.filter(required=True)
            insertion_idx = 1
        if qs.exists():
            fieldsets = list(fieldsets)
            fieldsets.insert(
                insertion_idx,
                (
                    _('Attributes'),
                    {'fields': [at.name for at in qs if at.name not in ['first_name', 'last_name']]},
                ),
            )
        return fieldsets

    def get_form(self, request, obj=None, **kwargs):
        self.form = modelform_factory(
            self.model, form=UserChangeForm, fields=models.Attribute.objects.values_list('name', flat=True)
        )
        self.add_form = modelform_factory(
            self.model,
            form=UserCreationForm,
            fields=models.Attribute.objects.filter(required=True).values_list('name', flat=True),
        )
        if 'fields' in kwargs:
            fields = kwargs.pop('fields')
        else:
            fields = flatten_fieldsets(self.get_fieldsets(request, obj))
        if obj:
            qs = models.Attribute.objects.all()
        else:
            qs = models.Attribute.objects.filter(required=True)
        non_model_fields = {a.name for a in qs} - {'first_name', 'last_name'}
        fields = list(set(fields) - set(non_model_fields))
        kwargs['fields'] = fields
        return super().get_form(request, obj=obj, **kwargs)

    @transaction.atomic
    def mark_as_inactive(self, request, queryset):
        timestamp = timezone.now()
        for user in queryset:
            user.mark_as_inactive(timestamp=timestamp)

    mark_as_inactive.short_description = _('Mark as inactive')


admin.site.register(User, AuthenticUserAdmin)


class AttributeForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        choices = self.kind_choices()
        self.fields['kind'].choices = choices
        self.fields['kind'].widget = forms.Select(choices=choices)

    @decorators.to_iter
    def kind_choices(self):
        return attribute_kinds.get_choices()

    class Meta:
        model = models.Attribute
        fields = '__all__'


class AttributeAdmin(admin.ModelAdmin):
    form = AttributeForm
    list_display = (
        'label',
        'disabled',
        'name',
        'kind',
        'order',
        'required',
        'asked_on_registration',
        'user_editable',
        'user_visible',
    )
    list_editable = ('order',)

    def get_queryset(self, request):
        return self.model.all_objects.all()


admin.site.register(models.Attribute, AttributeAdmin)


class DeletedUserAdmin(admin.ModelAdmin):
    list_display = ['deleted', 'old_user_id', 'old_uuid', 'old_email']
    date_hierarchy = 'deleted'
    search_fields = ['=old_user_id', '^old_uuid', 'old_email']


admin.site.register(DeletedUser, DeletedUserAdmin)


@never_cache
def login(request, extra_context=None):
    return utils_misc.redirect_to_login(request, login_url=utils_misc.get_manager_login_url())


admin.site.login = login


@never_cache
def logout(request, extra_context=None):
    return utils_misc.redirect_to_login(request, login_url='auth_logout')


admin.site.logout = logout

admin.site.register(models.PasswordReset)
