from django.contrib import auth
from django.db import IntegrityError
from django.urls import path, include
from rest_framework import routers, serializers, viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response

from authentication.models import ToolshedUser, AccountPreference
from authentication.signature_auth import SignatureAuthenticationLocal, SignatureAuthentication, \
    split_userhandle_or_throw
from files.models import File
from files.serializers import FileSerializer
from hostadmin.models import Domain

router = routers.SimpleRouter()

# Schema for the account-level preferences a client may store on the server (see
# AccountPreference). Device-level preferences are never sent here - they stay in the
# browser's local storage since they describe the device, not the account.
PREFERENCE_DEFINITIONS = [
    {
        'key': 'ui.compact_mode',
        'type': 'boolean',
        'default': False,
        'label': 'Compact mode',
        'description': 'Show denser item rows and reduce spacing in lists.',
    },
    {
        'key': 'ui.default_search_scope',
        'type': 'enum',
        'options': ['inventory', 'friends', 'all'],
        'default': 'inventory',
        'label': 'Default search scope',
        'description': 'Choose where the global search starts.',
    },
    {
        'key': 'notifications.desktop_enabled',
        'type': 'boolean',
        'default': True,
        'label': 'Desktop notifications',
        'description': 'Enable in-browser notifications for important updates.',
    },
    {
        'key': 'files.max_upload_mb',
        'type': 'integer',
        'default': 25,
        'label': 'Default upload size limit (MB)',
        'description': 'Used as a prefill hint in upload dialogs.',
    },
    {
        'key': 'ui.experimental_flags',
        'type': 'json',
        'default': {},
        'label': 'Experimental flags',
        'description': 'Optional JSON toggles for feature previews.',
    },
]


class UserAuthToken(ObtainAuthToken):

    def post(self, request, *args, **kwargs):
        try:
            fullname = request.data.get('username')
            username = fullname.split('@')[0]
            domain = fullname.split('@')[1]
            password = request.data.get('password')
            user = auth.authenticate(username=username, password=password, domain=domain)
            token, created = Token.objects.get_or_create(user=user)
            return Response({
                'token': token.key,
                'key': user.private_key
            })
        except IndexError:
            return Response({
                'error': 'Invalid Credentials'
            }, status=400)
        except IntegrityError:
            return Response({
                'error': 'Invalid Credentials'
            }, status=400)


class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = ToolshedUser
        fields = ['username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'date_joined']


class UserViewSet(viewsets.ModelViewSet):
    queryset = ToolshedUser.objects.all()
    serializer_class = UserSerializer
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated, IsAdminUser]


@api_view(['GET', 'PATCH'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthenticationLocal])
def getUserInfo(request):
    """Get or update the authenticated local user's own account info. Only usable by the
    account owner on their own home server - see getUserProfile for viewing another (friend)
    user's public profile."""
    user = request.user
    if request.method == 'PATCH':
        old_file = user.profile_picture
        if 'profile_picture' in request.data:
            profile_picture = request.data.get('profile_picture')
            if profile_picture is None:
                user.profile_picture = None
            elif type(profile_picture) == dict:
                serializer = FileSerializer(data=profile_picture)
                if not serializer.is_valid():
                    return Response(serializer.errors, status=400)
                user.profile_picture = serializer.save()
            else:
                return Response({'profile_picture': 'Must be null or an object with data and mime_type.'}, status=400)
        elif 'profile_picture_id' in request.data:
            profile_picture_id = request.data.get('profile_picture_id')
            if profile_picture_id is None:
                user.profile_picture = None
            else:
                try:
                    user.profile_picture = File.objects.get(id=profile_picture_id)
                except File.DoesNotExist:
                    return Response({'profile_picture_id': 'File does not exist.'}, status=400)
        user.save()

        if old_file and old_file != user.profile_picture and old_file.connected_items.count() == 0 \
                and old_file.profile_picture_users.count() == 0 and old_file.staged_by_workflows.count() == 0:
            old_file.file.delete(save=False)
            old_file.delete()

    return Response({
        'username': user.username,
        'domain': user.domain,
        'email': user.email,
        'profile_picture': FileSerializer(user.profile_picture).data if user.profile_picture else None,
    })


@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def getUserProfile(request, handle):
    """Get another local user's public profile by handle (username@domain), e.g. so a friend
    can look up someone's avatar. The caller must be a friend of that user (or the user
    itself, signing with their own known identity rather than their local credentials)."""
    try:
        username, domain = split_userhandle_or_throw(handle)
    except ValueError:
        return Response(status=400)
    try:
        target = ToolshedUser.objects.get(username=username, domain=domain)
    except ToolshedUser.DoesNotExist:
        return Response(status=404)
    if target not in request.user.friends_or_self():
        return Response(status=403)
    return Response({
        'username': target.username,
        'domain': target.domain,
        'profile_picture': FileSerializer(target.profile_picture).data if target.profile_picture else None,
    })


@api_view(['POST'])
@permission_classes([])
@authentication_classes([])
def registerUser(request):
    try:
        username = request.data.get('username')
        domain = request.data.get('domain')
        password = request.data.get('password')
        email = request.data.get('email')

        errors = {}
        if not username:
            errors['username'] = 'Username is required'
        if not domain:
            errors['domain'] = 'Domain is required'
        if not password:
            errors['password'] = 'Password is required'
        if not email:
            errors['email'] = 'Email is required'
        if ToolshedUser.objects.filter(email=email).exists():
            errors['email'] = 'Email already exists'
        if ToolshedUser.objects.filter(username=username, domain=domain).exists():
            errors['username'] = 'Username already exists'
        if errors:
            return Response({'errors': errors}, status=400)

        Domain.objects.get(name=domain, open_registration=True)

        user = ToolshedUser.objects.create_user(username, email, password, domain=domain)
        return Response({'username': user.username, 'domain': user.domain})
    except Domain.DoesNotExist:
        return Response({'errors': {'domain': 'Domain does not exist or is not open for registration'}}, status=400)


@api_view(['GET'])
@permission_classes([])
@authentication_classes([])
def preference_definitions(request):
    """Return the schema (types, defaults, labels) for the account preferences clients may set."""
    return Response(PREFERENCE_DEFINITIONS)


@api_view(['GET', 'PUT'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthenticationLocal])
def account_preferences(request):
    """Get or bulk-upsert the authenticated user's account-level preferences.

    GET returns the current preferences as a {key: value} dict. PUT accepts a {key: value}
    dict of one or more preferences to set/overwrite; unspecified keys are left untouched.
    """
    if request.method == 'PUT':
        if not isinstance(request.data, dict):
            return Response({'detail': 'Expected an object of key/value pairs.'}, status=400)
        for key, value in request.data.items():
            AccountPreference.objects.update_or_create(user=request.user, key=key, defaults={'value': value})

    preferences = {pref.key: pref.value for pref in request.user.preferences.all()}
    return Response(preferences)


@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthenticationLocal])
def account_preference_detail(request, key):
    """Reset a single account-level preference back to its default by deleting it."""
    AccountPreference.objects.filter(user=request.user, key=key).delete()
    return Response(status=204)


router.register(r'users', UserViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('user/', getUserInfo),
    path('user/<str:handle>/', getUserProfile),
    path('register/', registerUser),
    path('token/', UserAuthToken.as_view()),
    path('preferences/', preference_definitions),
    path('self/preferences/', account_preferences),
    path('self/preferences/<str:key>/', account_preference_detail),
]
