"""
Handler for Follow activities.
"""

from datetime import datetime
from typing import override

from phederation.models import APAccept, ValidCollection, dereference
from phederation.models.activities import APActivity, APFollow
from phederation.utils.base import AccessType, collection_id_from_name
from phederation.utils.exceptions import HandlerError, ValidationError

from .base import ActivityHandler


class FollowHandler(ActivityHandler):
    """Handles Follow activities."""

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

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

        # Check that the object of the activity exists.
        if not activity.object:
            raise ValidationError("Follow must have an object")

        # make sure we are not following ourselves
        if dereference(data=activity, key="actor") == dereference(data=activity, key="object"):
            raise ValidationError("An actor cannot follow itself.")

        return activity

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        actor_id = dereference(activity, key="actor")
        target_id = dereference(activity, key="object")

        # Resolve target actor
        target_actor = await self.resolver.resolve_actor(target_id)
        if not target_actor or not target_id or not actor_id:
            raise HandlerError(f"Could not resolve target actor: {target_id}")

        # Check if already following (just if it was registered previously, not if it was actually accepted)
        # TODO: this does not work without proper storage session management; the await of is_following is a problem
        if not await self.storage.is_following(actor_id, target_id, check_accepted=False):
            self.logger.debug(f"FollowHandler: storage does not have the follow activity '{activity.id}' already, storing it now.")
            # Store follow request
            follow_id = await self.storage.follow.create(
                data=APFollow(id=activity.id, accepted=False, actor=actor_id, object=target_id), raise_if_exists=False
            )
            collection_follow = collection_id_from_name(id=actor_id, name=ValidCollection.Following.value)
            access = AccessType.from_visibility(activity.visibility)
            _ = await self.collections.add_to_collection(collection_id=collection_follow, items=follow_id, access=access, add_only_once=True)

        # Inform the target actor. This should trigger an "ACCEPT" from their side (if accepted).
        send_follow_result = await self.delivery.deliver_to_actor_inbox(activity=activity, actor_id=target_id)
        if send_follow_result.failed:
            self.logger.warning(f"Failing to send the Follow activity to the actor to follow: {send_follow_result}.")
        return activity

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        follow_activity_id = activity.id
        follower_id = dereference(activity.actor, "id")
        following_id = dereference(activity, key="object")

        if not follow_activity_id or not following_id or not follower_id:
            raise HandlerError("Could not resolve follow activity id, following_id, or follower_id")

        # Resolve target actor
        # Check that the object of the activity is an actor that can be followed. Actors have at least an inbox and an outbox property.
        target_actor = await self.resolver.resolve_actor(following_id)
        if not target_actor or not following_id:
            raise HandlerError(f"Could not resolve target actor: {following_id}")

        self.logger.debug(f"FollowHandler: starting to have follower={follower_id} to following={following_id}...")

        # Check if already following (just if it was registered previously, not if it was actually accepted)
        # also check that an outstanding Follow request has not already been made. It may be useful to store outstanding Follow requests in an easy-to-find way in your storage.
        if await self.storage.is_following(follower=follower_id, following=following_id, check_accepted=True):
            return None

        # Check that the object of the activity is an object on the local server.
        # Otherwise, the Follow activity can be safely treated like any other activity with no side effects.
        if not target_actor.local:
            return None

        # If these checks work, there are two paths to follow:
        if not target_actor.autoaccept:
            # Path 1: If the recipient does require manually confirming follows, the server should allow the actor to review the follow and either accept or reject. A typical implementation shows new follow requests in a separate section of the UI. Another option is including the Follow activity in the actor's inbox for review.
            self.logger.debug(f"FollowHandler: following actor requires manual accept, waiting...")
            if not await self.storage.is_following(follower=follower_id, following=following_id, check_accepted=False):
                follow_id = await self.storage.follow.create(
                    APFollow(
                        id=activity.id,
                        actor=follower_id,
                        object=following_id,
                        accepted=False,
                    )
                )
                # do not add to the followers collection here, actor has not accepted yet.
                return follow_id
        else:
            # Path 2: If the recipient does not require manually confirming follows, then you can simply create the connection and send an Accept activity in response.
            self.logger.debug(f"FollowHandler: following actor does autoaccept, storing follow for activity '{activity.id}'.")
            if not activity.id:
                raise HandlerError(f"FollowHandler: activity.id is None")
            _ = await self.storage.follow.upsert(
                id=activity.id,
                data=APFollow(
                    id=activity.id, actor=follower_id, object=following_id, accepted=True, accepted_at=datetime.now(), visibility=activity.visibility
                ),
            )
            # send Accept back to follower
            accept = APAccept(
                actor=following_id,
                object=follow_activity_id,
                to=[follower_id],  # this is necessary here, otherwise the follower actor will not be notified
                visibility=activity.visibility,
            )

            # To create the connection, there are two steps:
            #     1. Add the actor of the activity to the object's followers collection (done here, on this side).
            #     2. Add the object of the activity to the actor's following collection (done on the follower side).
            collection_follow = collection_id_from_name(id=following_id, name=ValidCollection.Followers.value)
            access = AccessType.from_visibility(activity.visibility)
            _ = await self.collections.add_to_collection(collection_id=collection_follow, items=follow_activity_id, access=access, add_only_once=True)

            # deliver the accept to the outbox of the actor that is being followed (to register the Accept and then send it off)
            following_dict = await self.resolver.resolve_actor(actor_id=following_id)
            following_outbox = dereference(data=following_dict, key=ValidCollection.Outbox.value)

            if not following_outbox:
                raise HandlerError(f"FollowHandler: outbox of actor is None; actor_dict={following_outbox}")

            # deliver the accept to the outbox of the actor being followed.
            # Note that this is NOT using token based authentication, but directly goes into the outbox of the actor on the local instance.
            result = await self.delivery.deliver_to_local_actor(activity=accept, collection=ValidCollection.Outbox, actor_id=following_id)
            if result.failed:
                self.logger.warning(f"FollowHandler: Accept activity delivery failed: {result.failed}, error message: {result.error_message}")
            return follow_activity_id
