"""
Announce activity handler implementation.
"""

from typing import override

from phederation.models import APPublicKey, dereference, object_from_type
from phederation.models.activities import APActivity
from phederation.models.objects import APObject, dereference_or_raise
from phederation.utils import ObjectId
from phederation.utils.base import AccessType, collection_id_from_name
from phederation.utils.exceptions import HandlerError, ValidationError

from .base import ActivityHandler


class AnnounceHandler(ActivityHandler):
    """Handle Announce (Boost/Reblog) activities."""

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

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

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

        # Validate object exists
        object_id = activity.object.id if isinstance(activity.object, APObject) else activity.object
        obj = await self.resolver.resolve_object(object_id)
        if not obj:
            raise ValidationError(f"Object not found: {object_id}")

        # Validate actor can announce
        actor = await self.resolver.resolve_actor(activity.actor)
        if not actor:
            raise ValidationError(f"Actor not found: {activity.actor}")

        return activity

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """Process Announce activity."""

        # Update object/actor shares collection
        object_id = dereference(activity, key="object")
        actor_id = dereference(activity, key="actor")
        if not object_id or not actor_id:
            raise HandlerError(f"AnnounceHandler: object_id or actor_id is None; object_id={object_id}, actor_id={actor_id}")

        access = AccessType.from_visibility(activity.visibility)
        await self._update_shares_collection(object_id=object_id, actor_id=actor_id, access=access)

        # Send notifications
        await self._notify_object_owner(object_id, activity)

        # Deliver to followers
        await self._deliver_to_followers(activity)

        return activity

    async def _update_shares_collection(self, object_id: ObjectId, actor_id: ObjectId, access: AccessType) -> None:
        """Update object's shares collection."""
        collection_shares = collection_id_from_name(id=object_id, name="shares")
        self.logger.debug(f"Adding actor {actor_id} to shares collection: {collection_shares}")
        # TODO: allow multiple shares if set in settings
        # TODO: it is not possible to "add only if not exists" here because of the storage session issue.
        _ = await self.collections.add_to_collection(collection_id=collection_shares, items=actor_id, access=access)

    async def _notify_object_owner(self, object_id: ObjectId, activity: APActivity) -> None:
        """Notify object owner about announce."""
        obj = await self.storage.object.read(id=object_id)
        if obj and obj.attributed_to:
            target_id = obj.attributed_to
            if isinstance(target_id, APObject):
                target_id = target_id.id
            if not target_id:
                self.logger.error(
                    f"AnnounceHandler: failed (error: target_id is Nont when trying to deliver Announce to actor '{target_id}. Activity: {activity}"
                )
                return

            # Inform the actor owning the object. This should create an addition to the "shares" collection of their object.
            # Only do this if the actor is not local, otherwise we will update twice.
            if not await self.delivery.check_is_local(actor_id=target_id):
                result = await self.delivery.deliver_to_actor_inbox(activity=activity, actor_id=target_id)
                if result.failed:
                    self.logger.error(
                        f"AnnounceHandler: failed (error: {result.error_message}) to deliver Announce to actor '{target_id}. Activity: {activity}"
                    )

    async def _deliver_to_followers(self, activity: APActivity) -> None:
        """Deliver announce to followers."""
        actor_id = dereference(activity, "actor")
        if not actor_id:
            raise HandlerError("AnnounceHandler, _deliver_to_followers: activity.actor is None")
        actor = await self.storage.actor.read(id=actor_id)
        if actor and actor.followers:
            _ = await self.delivery.deliver_activity(activity=activity, recipients=[actor.followers])

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        self.logger.debug(f"Processing Announce activity in inbox")

        # Update object/actor shares collection, if required. The object will not be a local one, but we may want to update local copies of it
        object_id = dereference_or_raise(activity, key="object")
        actor_id = dereference_or_raise(activity, key="actor")
        access = AccessType.from_visibility(activity.visibility)
        await self._update_shares_collection(object_id=object_id, actor_id=actor_id, access=access)

        if not isinstance(activity.object, APObject) and not isinstance(activity.object, dict):
            return
        obj = object_from_type(activity.object)
        if isinstance(obj, APPublicKey):
            self.logger.debug(f"Received new key in inbox, treating as key rotation", extra={"activity_id": activity.id})
            # at least check if the target is local to the instance.
            # Note that the *target* is not the actor with the new key here!
            # Key rotation always targets the instance actor (with "activity.bto"), but the "actor" is the one
            # on the remote instance that is rotating their keys.
            activity_target = dereference(activity.target, "id")
            activity_actor = dereference(activity.actor, "id")
            new_public_key = obj

            if not activity_target:
                self.logger.warning(f"Received a Create activity with key rotation data, but the activity.target is not set")
                return None
            if self.resolver.is_local(activity_target):
                self.logger.warning(
                    f"Received an Announce activity with key rotation data, but the activity.target actor {activity_target} is local to the instance (rotation should be federated). Discarding the new key"
                )
                return None
            if not activity_actor:
                self.logger.warning(f"Received a Create activity with key rotation data, but the activity.actor is not set")
                return None

            # store the new key in the actor list on the local instance
            actor_to_rotate = await self.resolver.resolve_actor(actor_id=activity_actor)
            if not actor_to_rotate.id:
                self.logger.warning(f"Received a Create activity with key rotation data, but the actor.id for {activity_actor} is None")
                return None

            new_key_id = dereference_or_raise(new_public_key, "id")
            actor_key_ids = actor_to_rotate.public_key_as_ids()
            if (
                (isinstance(actor_key_ids, list) and not (new_key_id in actor_key_ids))
                or (isinstance(actor_key_ids, str) and actor_key_ids != new_key_id)
                or actor_key_ids is None
            ):
                actor_to_rotate.add_public_key(new_public_key)
            _ = await self.storage.actor.upsert(id=actor_to_rotate.id, data=actor_to_rotate)

            # also store the key locally
            self.logger.debug(f"Storing new key locally, key_id={new_public_key.id}")
            _ = await self.key_manager.store_key(new_public_key)
