"""Accept activity handler."""

from datetime import datetime
from typing import override

from phederation.handlers.migrate import MigrationHandler
from phederation.models import dereference
from phederation.models.activities import APActivity, APFollow, APMigrate
from phederation.models.collections import ValidCollection
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 AcceptHandler(ActivityHandler):
    """Handles Accept activities."""

    ACCEPTABLE_TYPES: list[str] = [ActivityType.FOLLOW.value, ActivityType.MIGRATE.value]

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

        activity_to_accept = await self.resolver.resolve_activity(activity.object)
        if not activity_to_accept:
            raise ValidationError("Accept must have an object, set to the activity to accept")

        # Check if object is a dict with type
        if activity_to_accept.type not in self.ACCEPTABLE_TYPES:
            raise ValidationError(f"Cannot accept activity type: {activity_to_accept.type}")

        return activity

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """Handle Accept activity."""
        actor_id = dereference(data=activity, key="actor")
        activity_to_accept = await self.resolver.resolve_activity(dereference(activity, "object"))

        if not actor_id:
            raise HandlerError(f"Actor not found: actor_id is None for activity {activity}.")

        activity_actor_id = dereference_or_raise(data=activity_to_accept, key="actor")

        # Handle acceptance
        access = AccessType.from_visibility(activity.visibility)
        if activity_to_accept.type == ActivityType.FOLLOW.value:
            follow_activity_id = dereference_or_raise(data=activity_to_accept, key="id")
            following_id = dereference_or_raise(data=activity_to_accept, key="object")
            # Verify authorization for APFollow
            if following_id != actor_id:
                raise HandlerError(f"Unauthorized: can only accept APFollow activities targeting self. {following_id} is not {actor_id}")
            await self._handle_accept_follow(follower=activity_actor_id, following=following_id, follow_activity_id=follow_activity_id, access=access)
        elif activity_to_accept.type == ActivityType.MIGRATE.value and isinstance(activity_to_accept, APMigrate):
            target_actor_id = dereference(data=activity_to_accept, key="actor_to")
            # Verify authorization for APFollow
            if target_actor_id != actor_id:
                raise HandlerError(
                    f"Unauthorized: can only accept APMigrate activities targeting self. {target_actor_id} is not {actor_id}; activity={activity_to_accept.serialize()}"
                )
            await MigrationHandler.accept_migrate_on_new_instance(handler=self, activity=activity_to_accept)

        # Notify original actor: not necessary, because it should be in the "to" field of the activity already
        return activity

    async def _handle_accept_follow(self, follower: ObjectId, following: ObjectId, follow_activity_id: ObjectId, access: AccessType) -> None:
        """Handle accepting a Follow."""
        _ = await self.storage.follow.upsert(
            id=follow_activity_id, data=APFollow(id=follow_activity_id, actor=follower, object=following, accepted=True, accepted_at=datetime.now())
        )
        collection_follow = collection_id_from_name(id=following, name=ValidCollection.Followers.value)
        _ = await self.collections.add_to_collection(collection_id=collection_follow, items=follow_activity_id, access=access, add_only_once=True)

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        """Process inbox for APAccept.

        Returns:
            Optional[str]: Follow id stored with the accept processed, if it was created anew.
        """
        # Resolve the activity to accept
        accepted_activity_id = dereference_or_raise(activity, key="object")
        accepted_activity = await self.resolver.resolve_activity(accepted_activity_id)

        actor_id = dereference_or_raise(accepted_activity, "actor")
        target_id = dereference_or_raise(data=accepted_activity, key="object")

        if accepted_activity.type == ActivityType.FOLLOW.value:
            self.logger.info(f"Accepting APFollow in inbox of actor {actor_id}")
            # If the object is an activity of type Follow, the server should also check:
            #     That the actor of the Accept activity is not already in the recipient's following collection.
            if await self.storage.is_following(follower=actor_id, following=target_id, check_accepted=True):
                self.logger.info("Follow already in status ACCEPT locally.")
                return None
            # TODO: Check That the Follow activity has not been undone by an Undo activity. See ActivityPub/Primer/Undo activity for more information.

            # If these checks pass, then the server should:
            #     Add the actor of the Accept activity to the actor of the Follow activity's following.
            _ = await self.storage.follow.upsert(
                id=accepted_activity_id,
                data=APFollow(id=accepted_activity_id, actor=actor_id, object=target_id, accepted=True, accepted_at=datetime.now()),
            )
            collection_follow = collection_id_from_name(id=actor_id, name=ValidCollection.Following.value)
            if not await self.collections.contains(collection_id=collection_follow, item=accepted_activity_id):
                access = AccessType.from_visibility(accepted_activity.visibility)
                _ = await self.collections.add_to_collection(collection_id=collection_follow, items=accepted_activity_id, access=access)
        elif accepted_activity.type == ActivityType.MIGRATE.value and isinstance(accepted_activity, APMigrate):
            self.logger.info(f"Accepting APMigrate in inbox of actor {actor_id}")
            await MigrationHandler.accept_migrate_on_previous_instance(handler=self, activity=accepted_activity)
        else:
            self.logger.warning(f"Could not process activity {accepted_activity.type} in inbox (incorrect type)")

        # TODO: If the server is keeping caches of remote followers collects, add the actor of the Follow activity to the actor of the Accept activity's following.
        return activity.id
