"""
Enhanced Create activity handler.
"""

from typing import cast, override

from phederation.models import dereference
from phederation.models.activities import APActivity, APCreate
from phederation.models.keys import APPublicKey
from phederation.models.objects import (
    APArticle,
    APDocument,
    APImage,
    APNote,
    APObject,
    APUserData,
    DocumentType,
)
from phederation.models.proofs import DataIntegrityProof
from phederation.utils import ObjectId
from phederation.utils.base import (
    AccessType,
    ActivityType,
    ObjectType,
    collection_id_from_name,
)
from phederation.utils.exceptions import HandlerError, SecurityError, ValidationError

from .base import ActivityHandler


class CreateHandler(ActivityHandler):
    """Enhanced Create activity handler."""

    SUPPORTED_TYPES: dict[str, type[APNote] | type[APArticle] | type[APImage] | type[APDocument] | type[APUserData] | type[APPublicKey]] = {
        DocumentType.NOTE.value: APNote,
        DocumentType.ARTICLE.value: APArticle,
        DocumentType.IMAGE.value: APImage,
        DocumentType.DOCUMENT.value: APDocument,
        ObjectType.KEY.value: APPublicKey,
    }

    @classmethod
    def wrap(cls, object: APObject, actor_id: ObjectId) -> APCreate:
        """Wraps a given object into a Create activity.
        Only sets the "id" (to None), "actor", "object", and "type" fields.
        Does NOT set any of the "target", "to" etc. fields.

        Args:
            object (APObject): Object to be wrapped.
            actor_id (str): actor id that is performing this activity.

        Returns:
            APCreate: the create activity.
        """
        activity = APCreate(actor=actor_id, object=object)

        # copy recipients & visibility from the object to the activity
        for key in ["to", "cc", "bto", "bcc", "audience", "visibility"]:
            value = getattr(object, key, None)
            if value:
                setattr(activity, key, value)

        return activity

    @staticmethod
    async def store_object_locally(handler: ActivityHandler, obj: APObject, actor_id: ObjectId) -> APObject:
        # drop the id from the object (we create our own)
        obj.id = None

        # Attributed to creating user
        obj.attributed_to = actor_id

        # Clear the actor property
        if hasattr(obj, "actor"):
            setattr(obj, "actor", None)

        # For non-transient objects, the server MUST attach an id to both the wrapping Create and its wrapped Object.
        # https://www.w3.org/TR/activitypub/#client-to-server-interactions
        object_id = await handler.storage.object.create(data=obj)

        # add the id to the object so that it can be dereferenced when being stored in the activity
        obj.id = object_id

        # Set the blocks and shares collection ids
        obj.likes = collection_id_from_name(id=object_id, name="likes")
        _ = await handler.collections.create_collection(obj.likes, attributed_to=obj.id)
        obj.shares = collection_id_from_name(id=object_id, name="shares")
        _ = await handler.collections.create_collection(obj.shares, attributed_to=obj.id)
        _ = await handler.storage.object.upsert(id=object_id, data=obj)

        # if the instance supports objects lists for actors, add the object to the objects collection
        actor = await handler.resolver.resolve_actor(actor_id=actor_id)
        if actor.objects:
            await handler.collections.add_to_collection(
                collection_id=actor.objects, items=object_id, access=AccessType.from_visibility(obj.visibility)
            )

        # coll_shares = await self.collections.get_collection_page(collection_id=obj.shares)
        return obj

    @override
    async def validate(self, activity: APActivity) -> APActivity:
        """
        Enhanced Create validation.

        Validates:
        - Activity structure
        - Object type support
        - Content rules
        - Media attachments
        - Rate limits
        """
        # Validate activity.object
        if activity.type != ActivityType.CREATE.value:
            raise ValidationError(f"Create: APActivity is not APCreate (is type {activity.type})")

        obj_dict = activity.object
        if isinstance(obj_dict, APObject):
            obj_dict = obj_dict.serialize()
        if not isinstance(obj_dict, dict):
            self.logger.warning(f"CreateHandler: object in activity was not an APObject (instead: {type(obj_dict)}): activity {activity.serialize()}")
            raise ValidationError(f"Object in CREATE did not have proper information")
        obj_type = cast(str, obj_dict.get("type", ""))

        if not obj_type or obj_type not in self.SUPPORTED_TYPES:
            raise ValidationError(f"Unsupported object type: {obj_type}")

        # Validate object model
        model_class = self.SUPPORTED_TYPES[obj_type]
        obj = model_class.model_validate(obj_dict)

        # Check actor permissions
        actor_id = dereference(activity.actor, key="id")
        if not actor_id:
            raise ValidationError("Create: APCreate.actor.id is None")

        actor = await self.resolver.resolve_actor(actor_id=actor_id)
        if not actor:
            raise ValidationError("Actor not found")

        # Attribute the object to the actor of Create
        if obj.attributed_to and obj.attributed_to != actor_id:
            raise ValidationError(f"Object in Create is attributed to the wrong actor: attributed_to={obj.attributed_to}, actor_id={actor.id}")
        obj.attributed_to = actor_id

        # Validate file hash
        await CreateHandler.validate_content_hash(self, obj)

        # Validate content
        obj = await self._validate_content(obj)

        # Visibility of the object cannot be less secure than activity
        object_access = AccessType.from_visibility(obj.visibility)
        activity_access = AccessType.from_visibility(activity.visibility)
        maximim_security_visibility = AccessType.maximum_security(object_access, activity_access).value

        obj.visibility = maximim_security_visibility

        # Important for "store_in_collection" of server to create the object at this point, and not too late in the handle_outbox method
        activity_object = await CreateHandler.store_object_locally(handler=self, obj=obj, actor_id=actor_id)
        activity.object = activity_object

        # Set the visibility of the activity to be at most the one from the object
        activity.visibility = maximim_security_visibility

        return activity

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """
        Enhanced Create processing.

        Handles:
        - Object storage
        - Side effects
        - Notifications
        - Federation
        """
        # TODO: copy all receipients from activity to object, then back
        # A mismatch between addressing of the Create activity and its object is likely to lead to confusion. As such, a server SHOULD copy any recipients of the Create activity to its object upon initial distribution, and likewise with copying recipients from the object to the wrapping Create activity.
        # https://www.w3.org/TR/activitypub/#create-activity-outbox

        # create object locally
        actor_id = dereference(activity, key="actor")
        if not actor_id:
            raise HandlerError("Create activity does not have actor")

        obj = await self.resolver.resolve_object(activity.object)
        if not obj.id:
            raise HandlerError("obj.id is None")

        # Process mentions
        # TODO: implement mentions
        # if mentions := self._extract_mentions(obj):
        #    self._handle_mentions(mentions, activity)

        # Process attachments
        # TODO: implement attachments
        if attachment := dereference(obj, "attachment"):
            await self._process_attachments(attachment, obj.id)

        # Handle notifications
        # TODO: implement notifications
        # self._send_notifications(activity)

        return activity

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        self.logger.info(f"CreateHandler: Received CREATE in inbox (activity id: {activity.id})", extra={"activity_id": activity.id})

        # Validate content proof if required in settings.
        if self.storage.settings.media.require_file_proof_in_inbox:
            self.logger.debug(f"Validate file proof in inbox", extra={"activity_id": activity.id})
            obj = await self.resolver.resolve_object(activity.object)
            await CreateHandler.validate_proof(self, obj)

        return None

    async def _process_attachments(self, attachment: ObjectId, object_id: ObjectId):
        # TODO: actually process attachments
        attachment = attachment
        object_id = object_id

    async def _validate_content(self, object: APDocument | APPublicKey) -> APDocument | APPublicKey:
        # TODO: implement more content validation
        return object

    @staticmethod
    async def validate_content_hash(handler: ActivityHandler, object: APObject):
        """If required in settings, check if the object has a "hash" field attached to it (without checking its validity).

        Args:
            object (APObject): Object that should have a 'hash' field set.

        Raises:
            SecurityError: If required in settings and there is no hash field set to a string value.
        """
        if handler.storage.settings.media.require_content_hash:
            if not object.hash:
                raise SecurityError(f"Could not find a 'hash' field attached to the {object.type} with id '{object.id}'")

    @staticmethod
    async def validate_proof(handler: ActivityHandler, object: APObject):
        """Verify the object has a DataIntegrityProof attached in a 'proof' field and the object.hash is properly signed with the given proof.

        Args:
            object (APObject): Object with 'hash' and 'proof' fields.

        Raises:
            SecurityError: If the proof verification fails.
        """
        if handler.storage.settings.media.require_file_proof_in_inbox and isinstance(object, APDocument):
            proof = object.proof
            # TODO: refactoring: move this check to DataIntegrityProof class
            if isinstance(proof, DataIntegrityProof) and proof.verification_method:
                if proof.cryptosuite == "RSA":
                    public_key = await handler.key_manager.get_public_key(key_id=str(proof.verification_method))
                    if public_key and object.hash:
                        if proof.verify(object.hash, key=public_key):
                            handler.logger.debug(f"Successfully verified object hash for object '{object.id}'")
                            return
                else:
                    handler.logger.warning(f"Could not verify the proof with its cryptosuite set to '{proof.cryptosuite}' - not implemented")
                    pass

            raise SecurityError(f"Could not find a proper DataIntegrityProof attached to the {object.type} with id '{object.id}'")
