from django.db import transaction
from django.urls import path
from rest_framework import routers, viewsets, status
from rest_framework.decorators import authentication_classes, api_view, permission_classes, action
from rest_framework.exceptions import NotFound, PermissionDenied
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from authentication.models import ToolshedUser, KnownIdentity, Group
from authentication.signature_auth import SignatureAuthentication, split_userhandle_or_throw
from files.models import File
from toolshed.models import InventoryItem, StorageLocation, WorkflowInstance
from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer, WorkflowInstanceSerializer

router = routers.SimpleRouter()


def inventory_items(identity):
    try:
        user = identity.user.get()
        if user:
            for item in user.inventory_items.all():
                yield item
    except ToolshedUser.DoesNotExist:
        pass
    for friend in identity.friends.all():
        friend_user = friend.user.first()
        if friend_user:
            for item in friend_user.inventory_items.all():
                if item.availability_policy != 'private':
                    yield item


class InventoryItemViewSet(viewsets.ModelViewSet):
    serializer_class = InventoryItemSerializer
    authentication_classes = [SignatureAuthentication]
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        # A KnownIdentity acting purely as a group member (e.g. a remote member on a group
        # hosted on this backend) never has a local ToolshedUser account here - group-owned
        # items must stay reachable for such an identity, only personal ("owner=...") items
        # require .user.exists().
        if type(self.request.user) != KnownIdentity:
            return InventoryItem.objects.none()
        identity = self.request.user
        group_items = InventoryItem.objects.filter(owner_group__in=identity.member_of_groups.all())
        if self.action != 'list':
            # retrieve/update/destroy: anything the caller may act on - their own items, or any
            # group they're currently a member of. The narrower per-group listing below is only
            # for the list action, so the main Inventory page stays scoped to personal items.
            if identity.user.exists():
                return InventoryItem.objects.filter(owner=identity.user.get()) | group_items
            return group_items
        group_id = self.request.query_params.get('group')
        if group_id:
            try:
                group = Group.objects.get(pk=group_id)
            except Group.DoesNotExist:
                return InventoryItem.objects.none()
            if not group.is_member(identity):
                return InventoryItem.objects.none()
            return InventoryItem.objects.filter(owner_group=group)
        if identity.user.exists():
            return InventoryItem.objects.filter(owner=identity.user.get())
        return InventoryItem.objects.none()

    def perform_create(self, serializer):
        group_id = self.request.data.get('owner_group')
        with transaction.atomic():
            if group_id:
                try:
                    group = Group.objects.get(pk=group_id)
                except Group.DoesNotExist:
                    raise NotFound('No such group')
                if not group.is_member(self.request.user):
                    raise PermissionDenied('Not a member of this group')
                serializer.save(owner=None, owner_group=group).clean()
            else:
                serializer.save(owner=self.request.user.user.get()).clean()

    @staticmethod
    def _is_authorized(request, instance):
        if instance.owner_id:
            return request.user.user.filter(pk=instance.owner_id).exists()
        return instance.owner_group.is_member(request.user)

    def perform_update(self, serializer):
        with transaction.atomic():
            if self._is_authorized(self.request, serializer.instance):
                serializer.save().clean()

    def perform_destroy(self, instance):
        if self._is_authorized(self.request, instance):
            instance.delete()


def matches_query(item, query):
    query = query.lower()
    if query in item.name.lower():
        return True
    if item.description and query in item.description.lower():
        return True
    if any(query in tag.name.lower() for tag in item.tags.all()):
        return True
    return False


@api_view(['GET'])
@authentication_classes([SignatureAuthentication])
@permission_classes([IsAuthenticated])
def search_inventory_items(request):
    query = request.query_params.get('query')
    if query:
        matching_items = [item for item in inventory_items(request.user) if matches_query(item, query)]
        return Response(InventoryItemSerializer(matching_items, many=True).data)
    return Response({'error': 'No query provided.'}, status=400)


@api_view(['GET'])
@authentication_classes([SignatureAuthentication])
@permission_classes([IsAuthenticated])
def get_shared_item(request, handle, id):
    """Fetch a single item by its owner's handle (username@domain) and local id, e.g. for the
    /i/<handle>/<id> item URL (see docs/design-in-progress/items-labels.md) or the
    /inventory/shared/<handle>/<id> in-app view. Unlike InventoryItemViewSet, which only ever
    returns the requester's own items, this looks the item up by owner instead of by requester,
    so it's the only endpoint that can serve a friend's item - subject to the same
    friends-or-self and availability_policy checks getUserProfile/_accessible_files already
    use elsewhere."""
    try:
        username, domain = split_userhandle_or_throw(handle)
    except ValueError:
        return Response(status=400)
    try:
        owner = ToolshedUser.objects.get(username=username, domain=domain)
    except ToolshedUser.DoesNotExist:
        return Response(status=404)
    if owner not in request.user.friends_or_self():
        return Response(status=403)
    try:
        item = owner.inventory_items.get(id=id)
    except InventoryItem.DoesNotExist:
        return Response(status=404)
    is_owner = request.user.user.filter(pk=owner.pk).exists()
    if item.availability_policy == 'private' and not is_owner:
        return Response(status=403)
    return Response(InventoryItemSerializer(item).data)


class StorageLocationViewSet(viewsets.ModelViewSet):
    serializer_class = StorageLocationSerializer
    authentication_classes = [SignatureAuthentication]
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        if type(self.request.user) == KnownIdentity and self.request.user.user.exists():
            return StorageLocation.objects.filter(owner=self.request.user.user.get())
        return StorageLocation.objects.none()

    def perform_create(self, serializer):
        with transaction.atomic():
            serializer.save(owner=self.request.user.user.get())

    def perform_update(self, serializer):
        with transaction.atomic():
            if serializer.instance.owner == self.request.user.user.get():
                serializer.save()

    def perform_destroy(self, instance):
        if instance.owner == self.request.user.user.get():
            instance.delete()


class WorkflowInstanceViewSet(viewsets.ModelViewSet):
    serializer_class = WorkflowInstanceSerializer
    authentication_classes = [SignatureAuthentication]
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        if type(self.request.user) == KnownIdentity and self.request.user.user.exists():
            return WorkflowInstance.objects.filter(owner=self.request.user.user.get())
        return WorkflowInstance.objects.none()

    def perform_create(self, serializer):
        with transaction.atomic():
            serializer.save(owner=self.request.user.user.get())

    def perform_update(self, serializer):
        with transaction.atomic():
            if serializer.instance.owner == self.request.user.user.get():
                serializer.save()

    def perform_destroy(self, instance):
        if instance.owner == self.request.user.user.get():
            staged_file_ids = list(instance.staged_files.values_list('id', flat=True))
            instance.delete()
            for file in File.objects.filter(id__in=staged_file_ids):
                if file.connected_items.count() == 0 and file.profile_picture_users.count() == 0 \
                        and file.staged_by_workflows.count() == 0:
                    file.file.delete(save=False)
                    file.delete()


router.register(r'inventory_items', InventoryItemViewSet, basename='inventory_items')
router.register(r'storage_locations', StorageLocationViewSet, basename='storage_locations')
router.register(r'workflows', WorkflowInstanceViewSet, basename='workflows')

urlpatterns = router.urls + [
    path('search/', search_inventory_items, name='search_inventory_items'),
    path('inventory_items/<str:handle>/<int:id>/', get_shared_item, name='shared_inventory_item'),
]
