"""
Update activity handler.
"""

from datetime import datetime, timezone
from typing import override
from urllib.parse import urlparse, urlunparse

from phederation.handlers.delete import DeleteHandler
from phederation.models import APLink, APObject, DataIntegrityProof
from phederation.models.activities import APActivity
from phederation.models.actors import APActor
from phederation.models.collections import APCollection
from phederation.models.keys import APPrivateKey
from phederation.models.objects import dereference, dereference_or_raise
from phederation.utils import ActivityType, NodeInfo, ObjectId, UrlType
from phederation.utils.base import AccessType, CollectionType, assemble_id_url
from phederation.utils.exceptions import (
    AuthorizationError,
    HandlerError,
    ResolverError,
    ValidationError,
)

from .base import ActivityHandler


class UpdateHandler(ActivityHandler):
    """Handle Update activities."""

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

        object_id = dereference(activity, "object")
        if not object_id:
            raise ValidationError("Missing object")

        actor_id = dereference(activity, "actor")
        if not actor_id:
            raise ValidationError("Missing actor")

        # check if the object is a collection instead of an object
        if await DeleteHandler.check_is_collection(self, actor_id=actor_id, object_id=object_id):
            # validate that the new collection name still is a collection of the actor
            collections_of_actor = assemble_id_url(type=UrlType.Collections, base_url=actor_id)
            if not object_id.startswith(collections_of_actor):
                raise ValidationError(f"New collection id {object_id} is not in the collections of actor {actor_id}")
            try:
                obj = await self.resolver.resolve_object(object_id)
                if not isinstance(obj, APCollection):
                    raise ValidationError("Object of APUpdate for collection is not of type APCollection")
            except ResolverError as e:
                raise ValidationError(e.message)
        else:
            # Validate object exists
            obj = activity.object
            if isinstance(obj, dict):
                obj = APObject.deserialize(obj)
            if not isinstance(obj, APObject):
                raise ValidationError("Invalid object format")
            object_id = dereference_or_raise(obj, "id")
            object_type = dereference_or_raise(obj, "type")

            url_type = UrlType.from_object_type(object_type)
            existing = await self.resolver.try_resolve_from_storage(url=object_id, url_type=url_type)
            if not existing:
                raise ValidationError(f"Object not found: {object_id}")
            if isinstance(existing, NodeInfo):
                raise ValidationError(f"Object type of object in storage is wrong: {type(existing).__name__}")

            if isinstance(existing, DataIntegrityProof):
                raise ValidationError(f"Object of type DataIntegrityProof cannot be updated")

            # Validate actor has permission
            if existing.attributed_to != activity.actor:
                raise AuthorizationError("Not authorized to update object")

        # set to the deserialized and validated object
        activity.object = obj

        return activity

    async def update_collection(self, actor_id: ObjectId, collection_id: ObjectId, collection_id_new: ObjectId):
        """Update the name of a collection."""
        self.logger.debug(f"Updating collection name from {collection_id} to {collection_id_new}")
        # TODO: use storage to rename the collection, dont remove/add
        collection_items_public = await self.collections.get_collection_items(collection_id=collection_id, access=AccessType.PUBLIC)
        collection_items_follower = await self.collections.get_collection_items(collection_id=collection_id, access=AccessType.FOLLOWER)
        collection_items_private = await self.collections.get_collection_items(collection_id=collection_id, access=AccessType.PRIVATE)
        for item in collection_items_public:
            collection_items_private.remove(item)
            collection_items_follower.remove(item)
        for item in collection_items_follower:
            collection_items_private.remove(item)
        await self.collections.delete_collection(collection_id=collection_id)

        # also remove the old collection name from the actor collections
        actor_collections = assemble_id_url(type=UrlType.Collections, base_url=actor_id, primary=None)
        await self.collections.remove_from_collection(collection_id=actor_collections, items=collection_id, access=AccessType.PRIVATE)

        # re-add the items to the new collection
        await self.collections.add_to_collection(collection_id=collection_id_new, items=collection_items_public, access=AccessType.PUBLIC)
        await self.collections.add_to_collection(collection_id=collection_id_new, items=collection_items_follower, access=AccessType.FOLLOWER)
        await self.collections.add_to_collection(collection_id=collection_id_new, items=collection_items_private, access=AccessType.PRIVATE)

        # add the new collection to the actor collections
        await self.collections.add_to_collection(collection_id=actor_collections, items=collection_id_new, add_only_once=True)

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """Process Update activity."""
        # Update object
        obj = activity.object
        if not isinstance(obj, APObject):
            raise ValidationError("UpdateHandler: actor.object is not of type APObject")
        obj.updated = datetime.now(timezone.utc)
        object_id = dereference_or_raise(obj, "id")
        actor_id = dereference_or_raise(activity, "actor")
        object_type = dereference_or_raise(obj, "type")
        self.logger.info(f"Updating object with type {object_type}")

        if CollectionType.__contains__(object_type):
            object_id_parsed = urlparse(object_id)
            object_id = urlunparse([object_id_parsed.scheme, object_id_parsed.netloc, object_id_parsed.path, None, None, None])
            target_id = dereference_or_raise(activity, "target")
            await self.update_collection(
                actor_id=actor_id,
                collection_id=object_id,
                collection_id_new=target_id,
            )
        else:
            # update object
            # Preserve immutable fields
            url_type = UrlType.from_object_type(object_type)
            existing = await self.resolver.try_resolve_from_storage(url=object_id, url_type=url_type)
            if not existing:
                raise ValidationError(f"Object with id {obj.id} does not exist locally.")
            existing = existing.serialize()
            for field in ["id", "type", "attributedTo", "published"]:
                if field in existing:
                    setattr(obj, field, existing[field])

            # Store updated object
            await self._store_updated_object(obj)

        # Deliver update to recipients
        await self._deliver_update(activity)

        return activity

    async def _store_updated_object(self, obj: APObject):
        assert obj.id, "Object does not have a proper id"
        if isinstance(obj, APPrivateKey):
            _ = await self.storage.key.upsert(id=obj.id, data=obj)
        elif isinstance(obj, APActor):
            _ = await self.storage.actor.upsert(id=obj.id, data=obj)
        else:
            _ = await self.storage.object.upsert(id=obj.id, data=obj)

    async def _deliver_update(self, activity: APActivity) -> None:
        """Deliver update to recipients."""
        obj = await self.resolver.resolve_object(activity.object)

        # Collect recipients
        recipients: list[ObjectId] = []

        # Add mentioned users
        if obj.tag:
            for tag in obj.tag:
                if isinstance(tag, APLink):
                    tag_href = tag.href
                else:
                    tag = await self.resolver.resolve_object(tag)
                    tag_href = getattr(tag, "href", None)
                if not tag_href:
                    self.logger.warning(f"Tag href is not a proper URL for activity {activity.serialize()}")
                    return
                if tag.type == "Mention":
                    recipients.append(tag_href)

        # Add followers if public
        actor_id = dereference(activity, key="actor")
        if actor_id:
            actor = await self.storage.actor.read(id=actor_id)
            if actor and actor.followers:
                if obj.visibility == "public":
                    recipients.append(actor.followers)

        # Deliver activity
        if recipients:
            _ = await self.delivery.deliver_activity(activity=activity, recipients=recipients)

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        raise HandlerError("Update in inbox is not implemented.")
