"""Accept activity handler."""

from typing import override

from phederation.models import dereference
from phederation.models.activities import APActivity
from phederation.models.collections import ValidCollection
from phederation.models.objects import dereference_or_raise
from phederation.models.proofs import DataIntegrityProof
from phederation.utils import NodeInfo, UrlType
from phederation.utils.base import (
    AccessType,
    ActivityPubBaseWithId,
    ActivityType,
    collection_id_from_name,
    urljoin,
)
from phederation.utils.exceptions import AuthorizationError, HandlerError, ValidationError

from .base import ActivityHandler


class AddRemoveHandler(ActivityHandler):
    """Handles Add and Remove activities."""

    @override
    async def validate(self, activity: APActivity) -> APActivity:
        """Validate Add/Remove activity."""
        if not (activity.type in [ActivityType.ADD.value, ActivityType.REMOVE.value]):
            raise ValidationError("Invalid activity type")

        actor_id = dereference(activity, "actor")
        if not actor_id:
            raise ValidationError("Add/Remove activity must have an actor set")

        object_id = dereference_or_raise(activity, "object")

        # TODO: also make it possible to add remote objects to lists
        object_url_type = UrlType.from_local_url(object_id)
        object_to_handle = await self.resolver.try_resolve_from_storage(object_id, url_type=object_url_type)

        if not object_to_handle:
            raise ValidationError("Add/Remove activity must have an existing object to add/remove")

        # validate access
        obj_access = AccessType.from_visibility(object_to_handle.visibility)
        obj_attribution = dereference(object_to_handle, "attributedTo")
        if not obj_access.is_accessible(AccessType.PUBLIC) and (not obj_attribution or obj_attribution != actor_id):
            raise AuthorizationError(f"Not authorized to Add/Remove this object (obj_access={obj_access}, attributed_to={obj_attribution})")
        
        if isinstance(object_to_handle, DataIntegrityProof):
            raise ValidationError("Cannot add/remove DataIntegrityProof to a list")
        if isinstance(object_to_handle, NodeInfo):
            raise ValidationError("Cannot add/remove NodeInfo to a list")
        
        activity.object = object_to_handle

        if activity.type == ActivityType.REMOVE.value:
            collection_id = dereference(activity, "origin")
            if not collection_id:
                raise ValidationError("Remove activity must have an origin set where to remove the object")
        elif activity.type == ActivityType.ADD.value:
            collection_id = dereference(activity, "target")
            if not collection_id:
                raise ValidationError("Add activity must have a target set where to add the object")
        else:
            collection_id = None
        if not collection_id:
            raise ValidationError("Add/Remove activity does not have target/origin id set")

        # validate that the collection id is part of the actor collections
        if not collection_id.startswith(urljoin(actor_id, ValidCollection.Collections.value)):
            raise ValidationError(f"Collection id {collection_id} is not a collection of actor {actor_id}, cannot Add/Remove")

        return activity

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """Handle Add and Remove activities."""
        actor_id = dereference_or_raise(activity, "actor")
        actor_collections = collection_id_from_name(id=actor_id, name=ValidCollection.Collections.value)
        object_to_handle = activity.object
        if not isinstance(object_to_handle, ActivityPubBaseWithId):
            raise HandlerError(f"Activity.object to Add/Remove has wrong type: {type(object_to_handle).__name__}")
        
        object_id = dereference_or_raise(object_to_handle, "id")

        # set the access level to the maximum security between activity and object
        access_of_activity = AccessType.from_visibility(activity.visibility)
        access_of_object = AccessType.from_visibility(object_to_handle.visibility)
        access = AccessType.maximum_security(access_of_activity, access_of_object)

        if activity.type == ActivityType.ADD.value:
            collection_id = dereference_or_raise(activity, "target")
            _ = await self.collections.add_to_collection(collection_id=collection_id, items=object_id, access=access, add_only_once=False)
            _ = await self.collections.add_to_collection(collection_id=actor_collections, items=collection_id, access=access, add_only_once=True)
            self.logger.info(f"Added {object_id} to actor collection {collection_id}")
        elif activity.type == ActivityType.REMOVE.value:
            collection_id = dereference_or_raise(activity, "origin")
            _ = await self.collections.remove_from_collection(collection_id=collection_id, items=object_id, access=access)
            self.logger.info(f"Removed {object_id} from actor collection {collection_id} once")
            if not (await self.collections.collection_exists(collection_id=collection_id)):
                await self.collections.remove_from_collection(collection_id=actor_collections, items=collection_id, access=access)
                self.logger.info(f"Collection is now empty, also removing {collection_id} from actor collections")

        # TODO: notify followers?
        return activity

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        """Process inbox for APAdd and APRemove."""
        # Nothing to do here? This would arise if followers are notified.
        object_id = dereference_or_raise(activity, "object")
        self.logger.info(f"Received {activity.type} activity for object {object_id} in inbox, nothing to do here")

        return activity.id
