# 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 json

from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import transaction
from django.db.models import Count, F
from django.db.models.query import Prefetch, Q
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, TemplateView
from django.views.generic.detail import SingleObjectMixin

from authentic2 import data_transfer, hooks
from authentic2.apps.journal.views import JournalViewWithContext
from authentic2.forms.profile import modelform_factory
from authentic2.utils.misc import redirect
from django_rbac.utils import get_ou_model, get_permission_model, get_role_model

from . import app_settings, forms, resources, tables, views
from .journal_views import BaseJournalView
from .utils import has_show_username

OU = get_ou_model()


class RolesMixin:
    service_roles = True
    admin_roles = False

    def get_queryset(self):
        qs = super().get_queryset()
        qs = qs.select_related('ou')
        Permission = get_permission_model()
        permission_ct = ContentType.objects.get_for_model(Permission)
        ct_ct = ContentType.objects.get_for_model(ContentType)
        ou_ct = ContentType.objects.get_for_model(OU)
        permission_qs = Permission.objects.filter(target_ct_id__in=[ct_ct.id, ou_ct.id]).values_list(
            'id', flat=True
        )
        # only non role-admin roles, they are accessed through the
        # RoleManager views
        if not self.admin_roles:
            qs = qs.filter(
                Q(admin_scope_ct__isnull=True)
                | Q(admin_scope_ct=permission_ct, admin_scope_id__in=permission_qs)
            )
        if not self.service_roles:
            qs = qs.filter(service__isnull=True)
        return qs


class RolesView(views.SearchOUMixin, views.HideOUColumnMixin, RolesMixin, views.BaseTableView):
    template_name = 'authentic2/manager/roles.html'
    model = get_role_model()
    table_class = tables.RoleTable
    search_form_class = forms.RoleSearchForm
    permissions = ['a2_rbac.search_role']
    title = _('Roles')

    def get_queryset(self):
        qs = super().get_queryset()
        qs = qs.annotate(member_count=Count('members'))
        return qs

    def get_search_form_kwargs(self):
        kwargs = super().get_search_form_kwargs()
        kwargs['queryset'] = self.get_queryset()
        return kwargs


listing = RolesView.as_view()


class RoleAddView(views.BaseAddView):
    template_name = 'authentic2/manager/role_add.html'
    model = get_role_model()
    title = _('Add role')
    success_view_name = 'a2-manager-role-members'
    exclude_fields = ('slug',)

    def get_initial(self):
        initial = super().get_initial()
        search_ou = self.request.GET.get('search-ou')
        if search_ou:
            initial['ou'] = search_ou
        return initial

    def get_form_class(self):
        form = forms.get_role_form_class()
        fields = [x for x in form.base_fields.keys() if x not in self.exclude_fields]
        return modelform_factory(self.model, form=form, fields=fields)

    def form_valid(self, form):
        response = super().form_valid(form)
        hooks.call_hooks(
            'event', name='manager-add-role', user=self.request.user, instance=form.instance, form=form
        )
        self.request.journal.record('manager.role.creation', role=form.instance)
        return response


add = RoleAddView.as_view()


class RolesExportView(views.ExportMixin, RolesView):
    resource_class = resources.RoleResource
    export_prefix = 'roles-export-'

    def get(self, request, *args, **kwargs):
        export_format = kwargs['format'].lower()
        if export_format == 'json':
            export = data_transfer.export_site(
                data_transfer.ExportContext(
                    role_qs=self.get_table_data(), export_roles=True, export_ous=False
                )
            )
            return self.export_response(json.dumps(export, indent=4), 'application/json', 'json')
        return super().get(request, *args, **kwargs)


export = RolesExportView.as_view()


class RoleViewMixin(RolesMixin):
    model = get_role_model()

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['ou'] = self.get_object().ou
        return ctx


class RoleEditView(RoleViewMixin, views.BaseEditView):
    template_name = 'authentic2/manager/role_edit.html'
    title = _('Edit role description')

    def get_form_class(self):
        return forms.get_role_form_class()

    def form_valid(self, form):
        response = super().form_valid(form)
        hooks.call_hooks(
            'event', name='manager-edit-role', user=self.request.user, instance=form.instance, form=form
        )
        self.request.journal.record('manager.role.edit', role=form.instance, form=form)
        return response


edit = RoleEditView.as_view()


class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTableView):
    template_name = 'authentic2/manager/role_members.html'
    table_class = tables.RoleMembersTable
    form_class = forms.ChooseUserForm
    success_url = '.'
    search_form_class = forms.UserSearchForm
    permissions = ['a2_rbac.view_role']

    @property
    def title(self):
        return self.get_instance_name()

    @property
    def can_manage_members(self):
        return self.object.can_manage_members and getattr(self, '_can_manage_members', False)

    @can_manage_members.setter
    def can_manage_members(self, value):
        self._can_manage_members = value

    def get_table_queryset(self):
        children = self.object.children(include_self=False)
        via_prefetch = Prefetch('roles', queryset=children, to_attr='via')
        return self.object.all_members().prefetch_related(via_prefetch)

    def form_valid(self, form):
        user = form.cleaned_data['user']
        action = form.cleaned_data['action']
        if self.can_manage_members:
            if action == 'add':
                if self.object.members.filter(pk=user.pk).exists():
                    messages.warning(self.request, _('User already in this role.'))
                else:
                    self.object.members.add(user)
                    hooks.call_hooks(
                        'event',
                        name='manager-add-role-member',
                        user=self.request.user,
                        role=self.object,
                        member=user,
                    )
                    self.request.journal.record(
                        'manager.role.membership.grant', role=self.object, member=user
                    )
            elif action == 'remove':
                if not self.object.members.filter(pk=user.pk).exists():
                    messages.warning(self.request, _('User was not in this role.'))
                else:
                    self.object.members.remove(user)
                    hooks.call_hooks(
                        'event',
                        name='manager-remove-role-member',
                        user=self.request.user,
                        role=self.object,
                        member=user,
                    )
                    self.request.journal.record(
                        'manager.role.membership.removal', role=self.object, member=user
                    )
        else:
            messages.warning(self.request, _('You are not authorized'))
        return super().form_valid(form)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        # if role's members can only be from the same OU we filter user based on the role's OU
        if app_settings.ROLE_MEMBERS_FROM_OU:
            kwargs['ou'] = self.object.ou
        return kwargs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['children'] = views.filter_view(
            self.request, self.object.children(include_self=False, annotate=True)
        )
        ctx['parents'] = views.filter_view(
            self.request,
            self.object.parents(include_self=False, annotate=True).order_by(
                F('ou').asc(nulls_first=True), 'name'
            ),
        )
        ctx['has_multiple_ou'] = OU.objects.count() > 1
        ctx['admin_roles'] = views.filter_view(
            self.request, self.object.get_admin_role().children(include_self=False, annotate=True)
        )
        ctx['from_ldap'] = self._can_manage_members and not self.can_manage_members
        return ctx

    def is_ou_specified(self):
        return self.search_form.is_valid() and self.search_form.cleaned_data.get('ou')

    def get_table(self, **kwargs):
        show_username = has_show_username()
        if not show_username and self.is_ou_specified():
            show_username = self.is_ou_specified().show_username
        if not show_username:
            exclude = kwargs.setdefault('exclude', [])
            if 'username' not in exclude:
                exclude.append('username')
        return super().get_table(**kwargs)


members = RoleMembersView.as_view()


class RoleDeleteView(RoleViewMixin, views.BaseDeleteView):
    title = _('Delete role')
    template_name = 'authentic2/manager/role_delete.html'

    def post(self, request, *args, **kwargs):
        if not self.can_delete:
            raise PermissionDenied
        return super().post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('a2-manager-roles')

    def delete(self, request, *args, **kwargs):
        role = self.get_object()

        hooks.call_hooks('event', name='manager-delete-role', user=request.user, role=role)
        self.request.journal.record('manager.role.deletion', role=role)
        return super().delete(request, *args, **kwargs)


delete = RoleDeleteView.as_view()


class RolePermissionsView(RoleViewMixin, views.BaseSubTableView):
    template_name = 'authentic2/manager/role_permissions.html'
    table_class = tables.PermissionTable
    form_class = forms.ChoosePermissionForm
    success_url = '.'
    permissions = ['a2_rbac.admin_permission']
    title = _('Permissions')

    def get_table_queryset(self):
        return self.object.permissions.all()

    def form_valid(self, form):
        if self.can_change:
            operation = form.cleaned_data.get('operation')
            ou = form.cleaned_data.get('ou')
            target = form.cleaned_data.get('target')
            action = form.cleaned_data.get('action')
            Permission = get_permission_model()
            if action == 'add' and operation and target:
                perm, created = Permission.objects.get_or_create(
                    operation=operation,
                    ou=ou,
                    target_ct=ContentType.objects.get_for_model(target),
                    target_id=target.pk,
                )
                self.object.permissions.add(perm)
                hooks.call_hooks(
                    'event',
                    name='manager-add-permission',
                    user=self.request.user,
                    role=self.object,
                    permission=perm,
                )
            elif action == 'remove':
                try:
                    permission_id = int(self.request.POST.get('permission', ''))
                    perm = Permission.objects.get(id=permission_id)
                except (ValueError, Permission.DoesNotExist):
                    pass
                else:
                    if self.object.permissions.filter(id=permission_id).exists():
                        self.object.permissions.remove(perm)
                        hooks.call_hooks(
                            'event',
                            name='manager-remove-permission',
                            user=self.request.user,
                            role=self.object,
                            permission=perm,
                        )
        else:
            messages.warning(self.request, _('You are not authorized'))
        return super().form_valid(form)


permissions = RolePermissionsView.as_view()


class RoleMembersExportView(views.ExportMixin, RoleMembersView):
    resource_class = resources.UserResource
    permissions = ['a2_rbac.view_role']

    def get_data(self):
        return self.get_table_data()


members_export = RoleMembersExportView.as_view()


class RoleAddChildView(
    views.AjaxFormViewMixin,
    views.TitleMixin,
    views.PermissionMixin,
    views.FormNeedsRequest,
    SingleObjectMixin,
    FormView,
):
    title = _('Add child role')
    model = get_role_model()
    form_class = forms.RolesForm
    success_url = '..'
    template_name = 'authentic2/manager/form.html'
    permissions = ['a2_rbac.manage_members_role']

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        parent = self.get_object()
        for role in form.cleaned_data['roles']:
            parent.add_child(role)
            hooks.call_hooks(
                'event', name='manager-add-child-role', user=self.request.user, parent=parent, child=role
            )
            self.request.journal.record('manager.role.inheritance.addition', parent=parent, child=role)
        return super().form_valid(form)


add_child = RoleAddChildView.as_view()


class RoleAddParentView(
    views.AjaxFormViewMixin, views.TitleMixin, views.FormNeedsRequest, SingleObjectMixin, FormView
):
    title = _('Add parent role')
    model = get_role_model()
    form_class = forms.RoleParentsForm
    success_url = '..'
    template_name = 'authentic2/manager/form.html'

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        if self.object.is_internal():
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        child = self.get_object()
        for role in form.cleaned_data['roles']:
            child.add_parent(role)
            hooks.call_hooks(
                'event', name='manager-add-child-role', user=self.request.user, parent=role, child=child
            )
            self.request.journal.record('manager.role.inheritance.addition', parent=role, child=child)
        return super().form_valid(form)


add_parent = RoleAddParentView.as_view()


class RoleRemoveChildView(views.AjaxFormViewMixin, SingleObjectMixin, views.PermissionMixin, TemplateView):
    title = _('Remove child role')
    model = get_role_model()
    success_url = '../..'
    template_name = 'authentic2/manager/role_remove_child.html'
    permissions = ['a2_rbac.manage_members_role']

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        self.child = self.get_queryset().get(pk=kwargs['child_pk'])
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['child'] = self.child
        return ctx

    def post(self, request, *args, **kwargs):
        self.object.remove_child(self.child)
        hooks.call_hooks(
            'event',
            name='manager-remove-child-role',
            user=self.request.user,
            parent=self.object,
            child=self.child,
        )
        self.request.journal.record('manager.role.inheritance.removal', parent=self.object, child=self.child)
        return redirect(self.request, self.success_url)


remove_child = RoleRemoveChildView.as_view()


class RoleRemoveParentView(views.AjaxFormViewMixin, SingleObjectMixin, TemplateView):
    title = _('Remove parent role')
    model = get_role_model()
    success_url = '../..'
    template_name = 'authentic2/manager/role_remove_parent.html'

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        if self.object.is_internal():
            raise PermissionDenied
        self.parent = self.get_queryset().get(pk=kwargs['parent_pk'])
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['parent'] = self.parent
        return ctx

    def post(self, request, *args, **kwargs):
        if not self.request.user.has_perm('a2_rbac.manage_members_role', self.parent):
            raise PermissionDenied
        self.object.remove_parent(self.parent)
        hooks.call_hooks(
            'event',
            name='manager-remove-child-role',
            user=self.request.user,
            parent=self.parent,
            child=self.object,
        )
        self.request.journal.record('manager.role.inheritance.removal', parent=self.parent, child=self.object)
        return redirect(self.request, self.success_url)


remove_parent = RoleRemoveParentView.as_view()


class RoleAddAdminRoleView(
    views.AjaxFormViewMixin,
    views.TitleMixin,
    views.PermissionMixin,
    views.FormNeedsRequest,
    SingleObjectMixin,
    FormView,
):
    title = _('Add admin role')
    model = get_role_model()
    form_class = forms.RolesForm
    success_url = '..'
    template_name = 'authentic2/manager/form.html'
    permissions = ['a2_rbac.change_role']

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        administered_role = self.get_object()
        for role in form.cleaned_data['roles']:
            administered_role.get_admin_role().add_child(role)
            hooks.call_hooks(
                'event',
                name='manager-add-admin-role',
                user=self.request.user,
                role=administered_role,
                admin_role=role,
            )
            self.request.journal.record(
                'manager.role.administrator.role.addition', role=administered_role, admin_role=role
            )
        return super().form_valid(form)


add_admin_role = RoleAddAdminRoleView.as_view()


class RoleRemoveAdminRoleView(
    views.TitleMixin, views.AjaxFormViewMixin, SingleObjectMixin, views.PermissionMixin, TemplateView
):
    title = _('Remove admin role')
    model = get_role_model()
    success_url = '../..'
    template_name = 'authentic2/manager/role_remove_admin_role.html'
    permissions = ['a2_rbac.change_role']

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        self.child = self.get_queryset().get(pk=kwargs['role_pk'])
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['child'] = self.child
        return ctx

    def post(self, request, *args, **kwargs):
        self.object.get_admin_role().remove_child(self.child)
        hooks.call_hooks(
            'event',
            name='manager-remove-admin-role',
            user=self.request.user,
            role=self.object,
            admin_role=self.child,
        )
        self.request.journal.record(
            'manager.role.administrator.role.removal', role=self.object, admin_role=self.child
        )
        return redirect(self.request, self.success_url)


remove_admin_role = RoleRemoveAdminRoleView.as_view()


class RoleAddAdminUserView(
    views.AjaxFormViewMixin,
    views.TitleMixin,
    views.PermissionMixin,
    views.FormNeedsRequest,
    SingleObjectMixin,
    FormView,
):
    title = _('Add admin user')
    model = get_role_model()
    form_class = forms.UsersForm
    success_url = '..'
    template_name = 'authentic2/manager/form.html'
    permissions = ['a2_rbac.change_role']

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        administered_role = self.get_object()
        for user in form.cleaned_data['users']:
            administered_role.get_admin_role().members.add(user)
            hooks.call_hooks(
                'event',
                name='manager-add-admin-role-user',
                user=self.request.user,
                role=administered_role,
                admin=user,
            )
            self.request.journal.record(
                'manager.role.administrator.user.addition', role=administered_role, admin_user=user
            )
        return super().form_valid(form)


add_admin_user = RoleAddAdminUserView.as_view()


class RoleRemoveAdminUserView(
    views.TitleMixin, views.AjaxFormViewMixin, SingleObjectMixin, views.PermissionMixin, TemplateView
):
    title = _('Remove admin user')
    model = get_role_model()
    success_url = '../..'
    template_name = 'authentic2/manager/role_remove_admin_user.html'
    permissions = ['a2_rbac.change_role']

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        self.user = get_user_model().objects.get(pk=kwargs['user_pk'])
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['user'] = self.user
        return ctx

    def post(self, request, *args, **kwargs):
        self.object.get_admin_role().members.remove(self.user)
        hooks.call_hooks(
            'event',
            name='remove-remove-admin-role-user',
            user=self.request.user,
            role=self.object,
            admin=self.user,
        )
        self.request.journal.record(
            'manager.role.administrator.user.removal', role=self.object, admin_user=self.user
        )
        return redirect(self.request, self.success_url)


remove_admin_user = RoleRemoveAdminUserView.as_view()


class RolesImportView(
    views.PermissionMixin, views.TitleMixin, views.MediaMixin, views.FormNeedsRequest, FormView
):
    form_class = forms.RolesImportForm
    model = get_role_model()
    template_name = 'authentic2/manager/import_form.html'
    title = _('Roles Import')

    def get_initial(self):
        initial = super().get_initial()
        search_ou = self.request.GET.get('search-ou')
        if search_ou:
            initial['ou'] = search_ou
        return initial

    def post(self, request, *args, **kwargs):
        if not self.can_add:
            raise PermissionDenied
        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
        self.ou = form.cleaned_data['ou']
        try:
            context = data_transfer.ImportContext(import_ous=False, set_ou=self.ou)
            with transaction.atomic():
                data_transfer.import_site(form.cleaned_data['site_json'], context)
        except ValidationError as e:
            form.add_error('site_json', e)
            return self.form_invalid(form)

        return super().form_valid(form)

    def get_success_url(self):
        messages.success(
            self.request,
            _('Roles have been successfully imported inside "%s" organizational unit.') % self.ou,
        )
        return reverse('a2-manager-roles') + '?search-ou=%s' % self.ou.pk


roles_import = RolesImportView.as_view()


class RolesCsvImportView(
    views.PermissionMixin, views.TitleMixin, views.MediaMixin, views.FormNeedsRequest, FormView
):
    form_class = forms.RolesCsvImportForm
    model = get_role_model()
    template_name = 'authentic2/manager/roles_csv_import_form.html'
    title = _('Roles CSV Import')

    def get_initial(self):
        initial = super().get_initial()
        search_ou = self.request.GET.get('search-ou')
        if search_ou:
            initial['ou'] = search_ou
        return initial

    def post(self, request, *args, **kwargs):
        if not self.can_add:
            raise PermissionDenied
        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
        self.ou = form.cleaned_data['ou']
        for role in form.roles:
            role.save()
        return super().form_valid(form)

    def get_success_url(self):
        messages.success(
            self.request,
            _('Roles have been successfully imported inside "%s" organizational unit.') % self.ou,
        )
        return reverse('a2-manager-roles') + '?search-ou=%s' % self.ou.pk


roles_csv_import = RolesCsvImportView.as_view()


class RolesCsvImportSampleView(TemplateView):
    template_name = 'authentic2/manager/sample_roles.txt'
    content_type = 'text/csv'


roles_csv_import_sample = RolesCsvImportSampleView.as_view()


class RoleJournal(views.PermissionMixin, JournalViewWithContext, BaseJournalView):
    template_name = 'authentic2/manager/role_journal.html'
    permissions = ['a2_rbac.view_role']
    title = _('Journal')

    @cached_property
    def context(self):
        return get_object_or_404(get_role_model(), pk=self.kwargs['pk'])

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['object'] = self.context
        return ctx


journal = RoleJournal.as_view()


class RolesJournal(views.SearchOUMixin, views.PermissionMixin, JournalViewWithContext, BaseJournalView):
    template_name = 'authentic2/manager/roles_journal.html'
    permissions = ['a2_rbac.view_role']
    title = _('Journal')

    @cached_property
    def context(self):
        return get_role_model()


roles_journal = RolesJournal.as_view()
