"""
Undo activity handler implementation.
"""

from typing import override

from phederation.models import ValidCollection, dereference
from phederation.models.activities import APActivity, APUndo
from phederation.models.objects import dereference_or_raise
from phederation.utils import ObjectId
from phederation.utils.base import AccessType, ActivityType, collection_id_from_name
from phederation.utils.exceptions import HandlerError, ValidationError

from .base import ActivityHandler


class UndoHandler(ActivityHandler):
    """Handle Undo activities."""

    @override
    async def validate(self, activity: APActivity) -> APActivity:
        """Validate Undo activity."""
        if activity.type != "Undo":
            raise ValidationError("Invalid activity type")

        if not activity.object:
            raise ValidationError("Missing object")

        if not activity.actor:
            raise ValidationError("Missing actor")

        # Validate object exists
        obj_dict_or_id = activity.object
        obj = await self.resolver.resolve_activity(obj_dict_or_id)

        # Validate actor has permission
        obj_actor = dereference(obj, key="actor")
        activity_actor = dereference(activity, key="actor")
        if not obj_actor or obj_actor != activity_actor:
            self.logger.warning(
                f"Actor {obj_actor} is not actor {activity_actor}, so not authorized to undo activity {obj.type}. Activity: {activity.id}, object={obj.id}"
            )
            raise ValidationError("Not authorized to undo activity")

        # Validate activity can be undone
        if obj.type not in ["Like", "Announce", "Follow", "Block"]:
            raise ValidationError(f"Cannot undo activity type: {obj.type}")

        return activity

    async def process_undo(self, activity: APActivity) -> APActivity:
        # Get original activity
        activity_to_undo_id = dereference(data=activity, key="object")
        if activity_to_undo_id:
            activity_to_undo_obj = await self.storage.activity.read(id=activity_to_undo_id)

            if not activity_to_undo_obj:
                self.logger.error(f"UndoHandler: Object with id {activity.object} does not exist locally. Failing silently.")
                return activity
        else:
            self.logger.warning(f"UndoHandler: activity {activity.type} does not have object")
            raise HandlerError(f"Activity {activity.type} does not have object")

        if not activity_to_undo_obj:
            self.logger.error(f"UndoHandler: Object with id {activity.object} does not exist locally. Failing silently.")
            return activity
        if not activity_to_undo_obj.id:
            self.logger.error(f"UndoHandler: Object of activity {activity.type} does not have proper ID. Failing silently.")
            return activity

        # Process based on activity type
        activity_type = ActivityType(activity_to_undo_obj.type)

        object_id = dereference(activity_to_undo_obj, key="object")
        actor_id = dereference(activity_to_undo_obj, key="actor")
        if not object_id or not actor_id:
            self.logger.error(f"UndoHandler: object_id={object_id} and actor_id={actor_id}. Both should be non-None. Failing silently.")
            return activity

        # the access type of all collections here is always "PRIVATE", since the actor itself is undoing something they did.
        if activity_type == ActivityType.LIKE:
            await self._undo_like(object_id=object_id, actor_id=actor_id)
        elif activity_type == ActivityType.ANNOUNCE:
            await self._undo_announce(object_id=object_id, actor_id=actor_id)
        elif activity_type == ActivityType.FOLLOW:
            await self._undo_follow(object_id=object_id, actor_id=actor_id, activity_id=activity_to_undo_obj.id)
        elif activity_type == ActivityType.BLOCK:
            await self._undo_block(object_id=object_id, actor_id=actor_id)
        return activity

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """Process Undo activity."""
        activity = await self.process_undo(activity=activity)

        # Inform the recipients of the original activity about the undo
        await self._deliver_undo(activity)

        return activity

    async def _undo_like(self, object_id: ObjectId, actor_id: ObjectId) -> None:
        """Undo a Like activity."""
        collection_likes = collection_id_from_name(id=object_id, name=ValidCollection.Likes.value)
        _ = await self.collections.remove_from_collection(collection_id=collection_likes, items=[actor_id], access=AccessType.PRIVATE)
        collection_liked = collection_id_from_name(id=object_id, name=ValidCollection.Liked.value)
        _ = await self.collections.remove_from_collection(collection_id=collection_liked, items=[object_id], access=AccessType.PRIVATE)

    async def _undo_announce(self, object_id: ObjectId, actor_id: ObjectId) -> None:
        """Undo an Announce activity."""
        collection_shares_actor = collection_id_from_name(id=actor_id, name=ValidCollection.Shares.value)
        _ = await self.collections.remove_from_collection(collection_id=collection_shares_actor, items=[object_id], access=AccessType.PRIVATE)
        collection_shares_object = collection_id_from_name(id=object_id, name=ValidCollection.Shares.value)
        _ = await self.collections.remove_from_collection(collection_id=collection_shares_object, items=[actor_id], access=AccessType.PRIVATE)

    async def _undo_follow(self, object_id: ObjectId, actor_id: ObjectId, activity_id: ObjectId) -> None:
        """Undo a Follow activity."""
        # Remove from following collection
        if await self.storage.is_following(follower=actor_id, following=object_id):
            _ = await self.storage.follow.delete(id=activity_id)
            collection_following = collection_id_from_name(id=actor_id, name=ValidCollection.Following.value)
            _ = await self.collections.remove_from_collection(collection_id=collection_following, items=[activity_id], access=AccessType.PRIVATE)
            collection_followers = collection_id_from_name(id=object_id, name=ValidCollection.Followers.value)
            _ = await self.collections.remove_from_collection(collection_id=collection_followers, items=[activity_id], access=AccessType.PRIVATE)

        # TODO: make sure we actually remove the correct activity's follow.

    async def _undo_block(self, object_id: ObjectId, actor_id: ObjectId) -> None:
        """Undo a Block activity."""
        collection_blocks = collection_id_from_name(id=actor_id, name=ValidCollection.Blocks.value)
        _ = await self.collections.remove_from_collection(collection_id=collection_blocks, items=[object_id], access=AccessType.PRIVATE)

    async def _deliver_undo(self, activity: APActivity) -> None:
        """Deliver undo to recipients."""
        activity_to_undo_id = dereference_or_raise(activity, "object")
        # load the object from storage, instead of taking the one stored in the activity (for safety)
        activity_to_undo = await self.storage.activity.read(id=activity_to_undo_id)

        if not activity_to_undo:
            raise HandlerError(f"UndoHandler: Could not deserialize the activity to undo. Activity could not be found in storage: {activity_to_undo_id}")

        # Deliver to original recipients
        if activity_to_undo and activity_to_undo.to and len(activity_to_undo.to) > 0:
            result = await self.delivery.deliver_activity(
                activity=APUndo(id=activity.id, object=activity_to_undo.id, actor=activity.actor), recipients=activity_to_undo.to
            )
            self.logger.info(f"UndoHandler: Delivering object {activity_to_undo.id} to {activity.actor}. Status: {result.status_code}")

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        """Process Undo activity."""
        activity_processed = await self.process_undo(activity=activity)

        # Deliver undo to recipients (not done in inbox, only outbox)
        # self._deliver_undo(activity)

        return activity_processed.id
