''' handles all of the activity coming in to the server '''
import json
from urllib.parse import urldefrag

import django.db.utils
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import requests

from bookwyrm import activitypub, models, views
from bookwyrm import status as status_builder
from bookwyrm.tasks import app
from bookwyrm.signatures import Signature


@csrf_exempt
@require_POST
def inbox(request, username):
    ''' incoming activitypub events '''
    try:
        models.User.objects.get(localname=username)
    except models.User.DoesNotExist:
        return HttpResponseNotFound()

    return shared_inbox(request)


@csrf_exempt
@require_POST
def shared_inbox(request):
    ''' incoming activitypub events '''
    try:
        resp = request.body
        activity = json.loads(resp)
        if isinstance(activity, str):
            activity = json.loads(activity)
        activity_object = activity['object']
    except (json.decoder.JSONDecodeError, KeyError):
        return HttpResponseBadRequest()

    if not has_valid_signature(request, activity):
        if activity['type'] == 'Delete':
            # Pretend that unauth'd deletes succeed. Auth may be failing because
            # the resource or owner of the resource might have been deleted.
            return HttpResponse()
        return HttpResponse(status=401)

    handlers = {
        'Follow': handle_follow,
        'Accept': handle_follow_accept,
        'Reject': handle_follow_reject,
        'Block': handle_block,
        'Create': handle_create,
        'Delete': handle_delete_status,
        'Like': handle_favorite,
        'Announce': handle_boost,
        'Add': {
            'Edition': handle_add,
        },
        'Undo': {
            'Follow': handle_unfollow,
            'Like': handle_unfavorite,
            'Announce': handle_unboost,
            'Block': handle_unblock,
        },
        'Update': {
            'Person': handle_update_user,
            'Edition': handle_update_edition,
            'Work': handle_update_work,
        },
    }
    activity_type = activity['type']

    handler = handlers.get(activity_type, None)
    if isinstance(handler, dict):
        handler = handler.get(activity_object['type'], None)

    if not handler:
        return HttpResponseNotFound()

    handler.delay(activity)
    return HttpResponse()


def has_valid_signature(request, activity):
    ''' verify incoming signature '''
    try:
        signature = Signature.parse(request)

        key_actor = urldefrag(signature.key_id).url
        if key_actor != activity.get('actor'):
            raise ValueError("Wrong actor created signature.")

        remote_user = activitypub.resolve_remote_id(models.User, key_actor)
        if not remote_user:
            return False

        try:
            signature.verify(remote_user.key_pair.public_key, request)
        except ValueError:
            old_key = remote_user.key_pair.public_key
            remote_user = activitypub.resolve_remote_id(
                models.User, remote_user.remote_id, refresh=True
            )
            if remote_user.key_pair.public_key == old_key:
                raise # Key unchanged.
            signature.verify(remote_user.key_pair.public_key, request)
    except (ValueError, requests.exceptions.HTTPError):
        return False
    return True


@app.task
def handle_follow(activity):
    ''' someone wants to follow a local user '''
    try:
        relationship = activitypub.Follow(
            **activity
        ).to_model(models.UserFollowRequest)
    except django.db.utils.IntegrityError as err:
        if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
            raise
        relationship = models.UserFollowRequest.objects.get(
            remote_id=activity['id']
        )
        # send the accept normally for a duplicate request

    manually_approves = relationship.user_object.manually_approves_followers

    status_builder.create_notification(
        relationship.user_object,
        'FOLLOW_REQUEST' if manually_approves else 'FOLLOW',
        related_user=relationship.user_subject
    )
    if not manually_approves:
        views.handle_accept(relationship)


@app.task
def handle_unfollow(activity):
    ''' unfollow a local user '''
    obj = activity['object']
    requester = activitypub.resolve_remote_id(models.User, obj['actor'])
    to_unfollow = models.User.objects.get(remote_id=obj['object'])
    # raises models.User.DoesNotExist

    to_unfollow.followers.remove(requester)


@app.task
def handle_follow_accept(activity):
    ''' hurray, someone remote accepted a follow request '''
    # figure out who they want to follow
    requester = models.User.objects.get(remote_id=activity['object']['actor'])
    # figure out who they are
    accepter = activitypub.resolve_remote_id(models.User, activity['actor'])

    try:
        request = models.UserFollowRequest.objects.get(
            user_subject=requester,
            user_object=accepter
        )
        request.delete()
    except models.UserFollowRequest.DoesNotExist:
        pass
    accepter.followers.add(requester)


@app.task
def handle_follow_reject(activity):
    ''' someone is rejecting a follow request '''
    requester = models.User.objects.get(remote_id=activity['object']['actor'])
    rejecter = activitypub.resolve_remote_id(models.User, activity['actor'])

    request = models.UserFollowRequest.objects.get(
        user_subject=requester,
        user_object=rejecter
    )
    request.delete()
    #raises models.UserFollowRequest.DoesNotExist

@app.task
def handle_block(activity):
    ''' blocking a user '''
    # create "block" databse entry
    activitypub.Block(**activity).to_model(models.UserBlocks)
    # the removing relationships is handled in post-save hook in model


@app.task
def handle_unblock(activity):
    ''' undoing a block '''
    try:
        block_id = activity['object']['id']
    except KeyError:
        return
    try:
        block = models.UserBlocks.objects.get(remote_id=block_id)
    except models.UserBlocks.DoesNotExist:
        return
    block.delete()


@app.task
def handle_create(activity):
    ''' someone did something, good on them '''
    # deduplicate incoming activities
    activity = activity['object']
    status_id = activity.get('id')
    if models.Status.objects.filter(remote_id=status_id).count():
        return

    try:
        serializer = activitypub.activity_objects[activity['type']]
    except KeyError:
        return

    activity = serializer(**activity)
    try:
        model = models.activity_models[activity.type]
    except KeyError:
        # not a type of status we are prepared to deserialize
        return

    status = activity.to_model(model)
    if not status:
        # it was discarded because it's not a bookwyrm type
        return

    # create a notification if this is a reply
    notified = []
    if status.reply_parent and status.reply_parent.user.local:
        notified.append(status.reply_parent.user)
        status_builder.create_notification(
            status.reply_parent.user,
            'REPLY',
            related_user=status.user,
            related_status=status,
        )
    if status.mention_users.exists():
        for mentioned_user in status.mention_users.all():
            if not mentioned_user.local or mentioned_user in notified:
                continue
            status_builder.create_notification(
                mentioned_user,
                'MENTION',
                related_user=status.user,
                related_status=status,
            )


@app.task
def handle_delete_status(activity):
    ''' remove a status '''
    try:
        status_id = activity['object']['id']
    except TypeError:
        # this isn't a great fix, because you hit this when mastadon
        # is trying to delete a user.
        return
    try:
        status = models.Status.objects.get(
            remote_id=status_id
        )
    except models.Status.DoesNotExist:
        return
    models.Notification.objects.filter(related_status=status).all().delete()
    status_builder.delete_status(status)


@app.task
def handle_favorite(activity):
    ''' approval of your good good post '''
    fav = activitypub.Like(**activity)
    # we dont know this status, we don't care about this status
    if not models.Status.objects.filter(remote_id=fav.object).exists():
        return

    fav = fav.to_model(models.Favorite)
    if fav.user.local:
        return

    status_builder.create_notification(
        fav.status.user,
        'FAVORITE',
        related_user=fav.user,
        related_status=fav.status,
    )


@app.task
def handle_unfavorite(activity):
    ''' approval of your good good post '''
    like = models.Favorite.objects.filter(
        remote_id=activity['object']['id']
    ).first()
    if not like:
        return
    like.delete()


@app.task
def handle_boost(activity):
    ''' someone gave us a boost! '''
    try:
        boost = activitypub.Boost(**activity).to_model(models.Boost)
    except activitypub.ActivitySerializerError:
        # this probably just means we tried to boost an unknown status
        return

    if not boost.user.local:
        status_builder.create_notification(
            boost.boosted_status.user,
            'BOOST',
            related_user=boost.user,
            related_status=boost.boosted_status,
        )


@app.task
def handle_unboost(activity):
    ''' someone gave us a boost! '''
    boost = models.Boost.objects.filter(
        remote_id=activity['object']['id']
    ).first()
    if boost:
        boost.delete()


@app.task
def handle_add(activity):
    ''' putting a book on a shelf '''
    #this is janky as heck but I haven't thought of a better solution
    try:
        activitypub.AddBook(**activity).to_model(models.ShelfBook)
    except activitypub.ActivitySerializerError:
        activitypub.AddBook(**activity).to_model(models.Tag)


@app.task
def handle_update_user(activity):
    ''' receive an updated user Person activity object '''
    try:
        user = models.User.objects.get(remote_id=activity['object']['id'])
    except models.User.DoesNotExist:
        # who is this person? who cares
        return
    activitypub.Person(
        **activity['object']
    ).to_model(models.User, instance=user)
    # model save() happens in the to_model function


@app.task
def handle_update_edition(activity):
    ''' a remote instance changed a book (Document) '''
    activitypub.Edition(**activity['object']).to_model(models.Edition)


@app.task
def handle_update_work(activity):
    ''' a remote instance changed a book (Document) '''
    activitypub.Work(**activity['object']).to_model(models.Work)