from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic.detail import SingleObjectMixin
from rules.contrib.views import PermissionRequiredMixin as RulesPermissionRequiredMixin

from core.models.assemblies import Assembly
from core.models.badges import Badge
from core.models.conference import Conference, ConferenceMember
from core.models.rooms import Room
from core.models.sso import Application


class ConferenceRequiredMixinBase:
    login_url = reverse_lazy('backoffice:login')
    require_conference = False
    _conference: Conference | None = None
    _conferencemember: ConferenceMember | None = None

    def __init__(self, *args, **kwargs):
        self._conference = kwargs.pop('conference', None)
        super().__init__(*args, **kwargs)

    @property
    def conference(self):
        if self._conference is None:
            conference_id = self.request.session.get('conference')
            if conference_id is None:
                try:
                    self._conference = Conference.objects.filter(is_public=True).first()
                except Conference.DoesNotExist:
                    try:
                        if self.request.user.is_staff:
                            self._conference = Conference.objects.first()
                    except Conference.DoesNotExist:
                        pass
                if self._conference is not None:
                    self.request.session['conference'] = self._conference.slug
            else:
                try:
                    self._conference = Conference.objects.accessible_by_user(self.request.user).get(slug=conference_id)
                except Conference.DoesNotExist:
                    self.request.session['conference'] = None

        return self._conference

    @property
    def conferencemember(self):
        if self._conferencemember is None:
            self._conferencemember = ConferenceMember.get_member(
                conference=self.conference,
                user=self.request.user,
                prefetch_user=True,
            )
        return self._conferencemember

    @property
    def is_assembly_team(self):
        return self.conferencemember.user.is_authenticated and self.conferencemember.has_perms('core.assembly_team', require_staff=True)

    def dispatch(self, request, *args, **kwargs):
        if self.require_conference and self.conference is None:
            return redirect('backoffice:conferences')
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        # super() does not have to have get_context_data(), e.g. if it's a plain View
        if hasattr(super(), 'get_context_data'):
            context = super().get_context_data(**kwargs)
        else:
            context = {}

        conference_list = Conference.objects.accessible_by_user(self.request.user).all()

        context.update(
            {
                'LANGUAGES': settings.LANGUAGES,
                'conference': self.conference,
                'conferencemember': self.conferencemember,
                'conferences': conference_list,
            }
        )

        if self.request.user.is_authenticated:
            context.update(
                {
                    'has_sos': self.conferencemember is not None,
                    'has_assemblies': self.is_assembly_team,
                    'has_pages': self.conferencemember.has_perms('core.static_pages', require_staff=True),
                    'has_map': self.conferencemember.has_perms('core.map_edit', require_staff=True),
                    'has_assembly_registration_admin': self.conferencemember.has_perms('core.assembly_registration_admin', require_staff=True),
                    'has_conference_admin': self.conferencemember.has_perms('core.conference_admin', require_staff=True),
                    'has_moderation': self.conferencemember.has_perms('core.moderation', require_staff=True),
                    'has_schedules': self.conferencemember.has_perms('core.scheduleadmin', require_staff=True),
                }
            )
        else:
            context.update(
                {
                    'has_assemblies': False,
                    'has_pages': False,
                    'has_map': False,
                    'has_assembly_registration_admin': False,
                    'has_conference_admin': False,
                    'has_moderation': False,
                    'has_schedules': False,
                }
            )

        return context


class ConferenceRequiredMixin(ConferenceRequiredMixinBase, PermissionRequiredMixin):
    permission_required = []
    require_all_permissions = True
    require_staff = False

    def has_permission(self):
        perms = self.get_permission_required()
        if not perms:
            return True

        cm = self.conferencemember
        return (
            cm.has_perms(
                *perms,
                require_all=self.require_all_permissions,
                require_staff=self.require_staff,
            )
            if cm is not None
            else False
        )


class ConferenceLoginRequiredMixin(LoginRequiredMixin, ConferenceRequiredMixin):
    pass


class ConferenceRuleRequiredMixin(ConferenceRequiredMixinBase, RulesPermissionRequiredMixin):
    pass


class ConferenceRuleLoginRequiredMixin(LoginRequiredMixin, ConferenceRuleRequiredMixin):
    pass


class AssemblyMixinBase:
    assembly_url_param = 'pk'

    def __init__(self, *args, **kwargs):
        self._assembly = kwargs.pop('assembly', None)

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

        # if _can_manage is set, user can edit assembly. This is a cache variable for the can_manage property
        self._can_manage = None
        # if _staff_access is set, extra staff fields can be modified (internal comment, is_official)
        self._staff_access = False
        # if _assembly_staff_access is set, the field state can be modified
        self._assembly_staff_access = False

        # configures the staff warning
        self._staff_mode = False

    def _get_assembly(self):
        if self._assembly is not None:
            return self._assembly

        assembly = Assembly.objects.get(conference=self.conference, pk=self.request.resolver_match.kwargs.get(self.assembly_url_param))

        # check if it's the assembly team
        if self.conferencemember.has_perms('core.assembly_team', 'core.change_assembly', require_staff=True):
            self._assembly_staff_access = True
            self._staff_access = self._staff_access
            self._staff_mode = True
        # neither owner/manager nor assembly team? go away
        if not assembly.has_user(self.request.user) and not self._assembly_staff_access:
            raise PermissionDenied

        self._assembly = assembly
        return assembly

    @property
    def assembly(self):
        return self._get_assembly()

    @property
    def can_manage(self):
        if self._can_manage is None:
            if not self.request.user.is_authenticated:
                # guests are not allowed to manage anything
                self._can_manage = False
                return False

            self._can_manage = self.assembly.user_can_manage(self.request.user, staff_can_manage=True)

        return self._can_manage

    @property
    def staff_access(self):
        return self._staff_access

    @property
    def assembly_staff_access(self):
        return self._assembly_staff_access

    @property
    def staff_mode(self):
        return self._staff_mode

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

        context['can_manage'] = can_manage = self.can_manage
        context['staff_access'] = self.staff_access
        context['staff_mode'] = self.staff_mode

        sidebar = []
        context['sidebar'] = {
            'back_link': {'link': reverse('backoffice:index'), 'caption': _('my_assemblies')},
            'title': assembly.slug,
            'title_link': reverse('backoffice:assembly', kwargs={'pk': assembly.id}),
            'items': sidebar,
        }

        # load backlink from the session if it set. Set by Assemblyteam / Channelsteam pages as there are a bunch of
        # pages that will link to assembly views
        if 'assembly_back' in self.request.session:
            assembly_back = self.request.session['assembly_back']
            if 'link' in assembly_back and 'title' in assembly_back:
                context['sidebar']['back_link'] = {'link': assembly_back['link'], 'caption': assembly_back['title']}

        organisation = []
        sidebar.append({'caption': _('backoffice:assembly-organizational-data'), 'children': organisation})
        organisation.append(
            {
                'caption': _('backoffice:assembly-basic-data'),
                'link': reverse('backoffice:assembly-edit', kwargs={'pk': assembly.id}),
            }
        )

        if assembly.is_cluster:
            organisation.append(
                {
                    'caption': 'Sub-Assemblies',
                    'link': reverse('backoffice:assembly-editchildren', kwargs={'pk': assembly.id}),
                }
            )

        organisation.append(
            {
                'caption': 'Links',
                'link': reverse('backoffice:assembly-editlinks', kwargs={'pk': assembly.id}),
            }
        )

        organisation.append(
            {
                'caption': _('Assembly__members'),
                'link': reverse('backoffice:assembly-members', kwargs={'pk': assembly.id}),
                'count': assembly.members.count(),
            }
        )

        if can_manage:
            apps = [
                {
                    'caption': f'{app["name"]}',
                    'link': reverse('backoffice:assembly-auth-app', kwargs={'assembly': assembly.id, 'pk': app['id']}),
                    'classes': [],
                }
                for app in Application.objects.filter(assembly=self.assembly).values('id', 'name')
            ]
            sidebar.append(
                {
                    'caption': _('Assembly__authentication'),
                    'children': apps,
                    'count': len(apps),
                    'link': reverse('backoffice:assembly-auth', kwargs={'assembly': assembly.id}),
                }
            )

            if (voucher_count := assembly.get_voucher_count(with_always_public=True)) is not None:
                organisation.append(
                    {
                        'caption': _('Vouchers'),
                        'count': voucher_count,
                        'link': reverse('backoffice:assembly-vouchers', kwargs={'pk': assembly.id}),
                    }
                )

        rooms = [
            {
                'caption': f'{room["name"]} ({room["room_type"]})',
                'link': reverse('backoffice:assembly-room', kwargs={'assembly': assembly.id, 'pk': room['id']}),
                'classes': ['blocked'] if room['blocked'] else [],
            }
            for room in assembly.rooms.exclude(room_type=Room.RoomType.PROJECT).values('id', 'name', 'room_type', 'blocked')
        ]
        sidebar.append(
            {
                'caption': _('backoffice:assembly-rooms'),
                'children': rooms,
                'count': len(rooms),
                'add_link': reverse('backoffice:assembly-create-room', kwargs={'assembly': assembly.id}) if can_manage else None,
            }
        )

        projects = [
            {
                'caption': prj['name'],
                'link': reverse('backoffice:assembly-project', kwargs={'assembly': assembly.id, 'pk': prj['id']}),
                'classes': ['blocked'] if prj['blocked'] else [],
            }
            for prj in assembly.projects.all().values('id', 'name', 'is_public', 'blocked')
        ]
        sidebar.append(
            {
                'caption': _('backoffice:assembly-projects'),
                'link': reverse('backoffice:assembly-projects', kwargs={'assembly': assembly.id}),
                'children': projects,
                'count': len(projects),
                'add_link': reverse('backoffice:assembly-create-project', kwargs={'assembly': assembly.id}) if can_manage else None,
            }
        )

        events = [
            {
                'caption': ev['name'],
                'link': reverse('backoffice:assembly-event', kwargs={'assembly': assembly.id, 'pk': ev['id']}),
                'classes': ['blocked'] if not ev['is_public'] else [],
            }
            for ev in assembly.events.values('id', 'name', 'is_public')
        ]
        sidebar.append(
            {
                'caption': 'Events',
                'link': reverse('backoffice:assembly-events', kwargs={'assembly': assembly.id}),
                'children': events,
                'count': len(events),
                'add_link': reverse('backoffice:assembly-create-event', kwargs={'assembly': assembly.id}) if can_manage else None,
            }
        )

        badges = [
            {
                'caption': b['name'],
                'link': reverse('backoffice:assembly-badge', kwargs={'assembly': assembly.id, 'pk': b['id']}),
                'classes': ['blocked'] if b['state'] == Badge.State.PLANNED else [],
            }
            for b in assembly.badges.values('id', 'name', 'state')
        ]
        sidebar.append(
            {
                'caption': 'Badges',
                'link': reverse('backoffice:assembly-badges', kwargs={'assembly': assembly.id}),
                'children': badges,
                'count': len(badges),
                'add_link': reverse('backoffice:assembly-create-badge', kwargs={'assembly': assembly.id}) if can_manage else None,
            }
        )

        # try to guess 'active' sidebar item
        guess_active_sidebar_item(self.request, context['sidebar']['items'])

        return context


class AssemblyMixin(AssemblyMixinBase, ConferenceLoginRequiredMixin):
    assembly_management = False

    def dispatch(self, request, *args, **kwargs):
        # for authenticated users check if they have assembly management permissions in case those are required
        try:
            if self.request.user.is_authenticated and self.assembly_management and not self.can_manage:
                return HttpResponse('You do not have assembly management permissions.', status=401)
        except PermissionDenied:
            return HttpResponse('You do not have assembly management permissions.', status=401)

        return super().dispatch(request, *args, **kwargs)


class AssemblyRuleMixin(AssemblyMixinBase, ConferenceRuleLoginRequiredMixin):
    pass


def guess_active_sidebar_item(request: HttpRequest, sidebar_items: dict, with_query_string: bool = True) -> None:
    query_string = ''
    if with_query_string and (qs := request.META.get('QUERY_STRING')):
        query_string = '?' + qs

    request_url = request.META.get('PATH_INFO') + query_string
    for sidebar_item in sidebar_items:
        if request_url == sidebar_item.get('link'):
            sidebar_item['active'] = True
            sidebar_item['expanded'] = True
            continue

        if request_url == sidebar_item.get('add_link'):
            sidebar_item['active'] = True
            sidebar_item['expanded'] = False
            continue

        if 'children' in sidebar_item:
            for sidebar_child in sidebar_item.get('children') or []:
                if 'link' in sidebar_child and sidebar_child['link'] == request_url:
                    sidebar_child['active'] = True
                    sidebar_item['child_active'] = True
                    sidebar_item['expanded'] = True
        else:
            sidebar_item['children'] = None


class PasswordMixin:
    def get_context_data(self, *args, **kwargs):
        try:
            context = super().get_context_data(*args, **kwargs)
        except AttributeError:
            # super() does not have .get_context_data(), e.g. if it's a plain View
            context = {}

        context.update(
            {
                'LANGUAGES': settings.LANGUAGES,
            }
        )

        return context


class SingleUUIDObjectMixin(SingleObjectMixin):
    uuid_url_kwarg = 'uuid'

    def get_object(self, queryset=None):
        """
        Return the object the view is displaying.

        Require `self.queryset` and a `uuid` argument in the URLconf.
        Subclasses can override this to return any object.
        """
        # Use a custom queryset if provided; this is required for subclasses
        # like DateDetailView
        if queryset is None:
            queryset = self.get_queryset()

        uuid = self.kwargs.get(self.uuid_url_kwarg)
        if uuid is not None:
            queryset = queryset.filter(uuid=uuid)
        else:
            raise AttributeError(f'Generic detail view {self.__class__.__name__} must be called with an object uuid in the URLconf.')

        try:
            # Get the single item from the filtered queryset
            obj = queryset.get()
        except queryset.model.DoesNotExist as exc:
            raise Http404(_('No %(verbose_name)s found matching the query') % {'verbose_name': queryset.model._meta.verbose_name}) from exc
        return obj