"""Like activity handler."""

from typing import override

from phederation.models import APObject, dereference
from phederation.models.activities import APActivity
from phederation.utils import ActivityType, ObjectId
from phederation.utils.base import AccessType, collection_id_from_name
from phederation.utils.exceptions import HandlerError, ValidationError

from .base import ActivityHandler


class LikeHandler(ActivityHandler):
    """Handles Like activities."""

    @override
    async def validate(self, activity: APActivity) -> APActivity:
        """Validate Like activity."""
        if not ActivityType.__contains__(activity.type) or ActivityType(activity.type) != ActivityType.LIKE:
            raise ValidationError("Invalid activity type")

        if not activity.actor:
            raise ValidationError("Like must have an actor")

        # TODO: check if actor is local to this instance

        if not activity.object:
            raise ValidationError("Like must have an object")

        return activity

    async def has_liked(self, actor_id: ObjectId, object_id: ObjectId):
        collection_liked = collection_id_from_name(id=actor_id, name="liked")
        return await self.collections.contains(collection_id=collection_liked, item=object_id)

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """Handle Like activity."""
        actor_id = dereference(data=activity, key="actor")
        object_id = dereference(data=activity, key="object")

        if not actor_id or not object_id:
            raise HandlerError("Could not dereference actor or object id from activity")

        # Resolve target object
        obj = await self.resolver.resolve_object(object_id)

        # Check for duplicate like
        if await self.has_liked(actor_id, object_id):
            raise HandlerError("Already liked this object")

        # the access level is the maximum (more private) between activity and object visibility
        access1 = AccessType.from_visibility(activity.visibility)
        access2 = AccessType.from_visibility(obj.visibility)
        access = AccessType.maximum_security(access1, access2)

        # Store like
        collection_liked = collection_id_from_name(id=actor_id, name="liked")
        await self.collections.add_to_collection(collection_id=collection_liked, items=object_id, access=access)
        collection_likes = collection_id_from_name(id=object_id, name="likes")
        await self.collections.add_to_collection(collection_id=collection_likes, items=actor_id, access=access)

        # Notify object creator if it is not a local object, otherwise increase likes
        if getattr(obj, "local", False) == False:
            await self._notify_object_owner(obj, activity)

        self.logger.info(f"Handled Like activity: {actor_id} -> {object_id}")
        return activity

    async def _notify_object_owner(self, obj: APObject, activity: APActivity) -> None:
        """Notify object owner about the like."""
        if obj.attributed_to:
            target_actor = await self.resolver.resolve_actor(obj.attributed_to)
            if target_actor and target_actor.id and dereference(data=target_actor, key="inbox"):
                _ = await self.delivery.deliver_to_actor_inbox(activity=activity, actor_id=target_actor.id)
            else:
                self.logger.warning(f"LikeHandler: _notify_object_owner could not deliver to target actor: target_actor={target_actor.serialize()}")
        else:
            raise HandlerError("Object does not have an owner, cannot notify")

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        # Actor and object are validated to be part of the activity.
        actor_id = dereference(data=activity, key="actor")
        object_id = dereference(data=activity, key="object")

        if not actor_id or not object_id:
            raise HandlerError("Could not dereference actor or object id from activity")

        #     Check that the object of the activity is an object on the local server.
        obj = await self.storage.object.read(id=object_id)
        if not obj:
            #  Otherwise, the Like activity can be safely treated like any other activity with no side effects.
            # https://www.w3.org/wiki/ActivityPub/Primer/Like_activity

            # Regardless, we will throw an exception here to inform the stream the object does not exist here.
            raise HandlerError(f"Object {object_id} does not exist on this instance.")

        #     Check that the actor of the activity is not the actor of an existing Like activity in the likes collection of the object.
        if await self.has_liked(actor_id=actor_id, object_id=object_id):
            return object_id

        #     Check that the responsible actor for the object has not blocked the actor of the Like.
        owner_id = dereference(obj, "attributedTo")
        if owner_id:
            collection_blocks = collection_id_from_name(id=owner_id, name="blocks")
            if await self.collections.contains(collection_id=collection_blocks, item=actor_id):
                return None

        #     Check that the actor has permission to view the object, such as being one of the addressees of the object or its Create activity.
        # TODO: implement visibility layers
        if obj.visibility != "public":
            raise HandlerError(f"Object {object_id} is not public.")

        # If these checks work:
        self.logger.warning(f"Storing like in likes collection of {object_id} in inbox")
        collection_liked = collection_id_from_name(id=actor_id, name="liked")
        await self.collections.add_to_collection(collection_id=collection_liked, items=object_id)
        collection_likes = collection_id_from_name(id=object_id, name="likes")
        await self.collections.add_to_collection(collection_id=collection_likes, items=actor_id)
