"""
Enhanced Create activity handler.
"""

import io
import json
from typing import Any, cast, override

from fastapi.datastructures import UploadFile

from phederation.handlers.create import CreateHandler
from phederation.models import dereference, object_from_type
from phederation.models.activities import APActivity, APMigrate
from phederation.models.actors import APActor
from phederation.models.objects import (
    APDocument,
    APObject,
    DocumentType,
    dereference_or_raise,
)
from phederation.utils import UrlType
from phederation.utils.base import AccessType
from phederation.utils.exceptions import HandlerError, ValidationError
from phederation.utils.serialization import ActivityPubBase, to_json_string

from .base import ActivityHandler


class MigrationHandler(ActivityHandler):
    """Migration activity handler."""

    SUPPORTED_TYPES: dict[str, type[APDocument]] = {
        DocumentType.DOCUMENT.value: APDocument,
    }

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

        Validates:
        - Activity structure
        - Object type support
        - Rate limits
        """
        activity_resolved = object_from_type(activity)
        if not isinstance(activity_resolved, APMigrate):
            raise ValidationError(f"APActivity is not APMigrate (is type {activity.type})")
        activity = activity_resolved

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

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

        migration_document = activity.object

        # if there is a migration document attached, validate it and store it locally
        if migration_document:
            if isinstance(migration_document, APObject):
                migration_document = migration_document.serialize()
            if not isinstance(migration_document, dict):
                self.logger.warning(f"Object in activity was not an APObject (instead: {type(migration_document)}): activity {activity.serialize()}")
                raise ValidationError(f"Object in MIGRATE did not have proper information")
            obj_type = cast(str, migration_document.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(migration_document)

            if obj.attributed_to and obj.attributed_to != actor_id:
                raise ValidationError(f"Object in Migrate 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)

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

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

            # Important to do this not too late in the handle_outbox method
            activity.object = await CreateHandler.store_object_locally(handler=self, obj=obj, actor_id=actor_id)

        # Set the accepted field to None, so that it must be set explicitly by an APAccept
        activity.accepted_at = None

        return activity

    @override
    async def process_outbox(self, activity: APActivity) -> APActivity:
        """
        Migration activity processing in the instance the actor is *leaving*.

        Handles:
        - Assembling the data of the migrating actor
        - Informing the new instance about the user data being ready for migration
        """
        if not isinstance(activity, APMigrate):
            raise HandlerError(f"Activity is not of type Migrate, is {activity.type}")

        activity = await self._assemble_user_data(activity)
        activity = await self._inform_new_instance(activity)

        return activity

    async def _assemble_user_data(self, activity: APMigrate):
        actor_id = dereference_or_raise(activity, "actor")
        activity_id = dereference_or_raise(activity, "id")

        actor = await self.resolver.try_resolve_from_storage(url=actor_id, url_type=UrlType.Users)
        if not isinstance(actor, APActor):
            raise ValidationError("APMigrate.actor could not be resolved from storage")
        document = await self.create_user_data(actor)
        document.visibility = activity.visibility

        activity.object = document
        activity.id = await self.storage.activity.upsert(id=activity_id, data=activity)
        return activity

    async def _inform_new_instance(self, activity: APMigrate):
        """Inform the instance the actor is migrating to about the migration data being ready.

        Args:
            activity (APMigrate): The migration activity.

        Raises:
            HandlerError: If the activity is not properly set up.

        Returns:
            APMigrate: The activity, without any recipients.
        """
        object_id = dereference(activity, "object")
        if not object_id:
            raise HandlerError(f"APMigrate activity {activity.id} does not have a proper object with id, cannot migrate")
        if not activity.actor_to:
            self.logger.info(
                "Id of the actor on the new instance is not set in the APMigrate activity, will not inform any new instance about the migration data"
            )
            return activity

        # do not send to anybody except the new actor
        activity.clear_recipients()
        result = await self.delivery.deliver_activity(activity=activity, recipients=[activity.actor_to])
        if result.failed:
            self.logger.info(f"Failed to deliver message to the new actor {activity.actor_to}. Message: {result.error_message}")
        return activity

    @override
    async def process_inbox(self, activity: APActivity) -> None | str:
        """
        Migration activity processing in the instance the actor is *migrating to*.

        This only validates the document proof created by the old instance.
        The actor on this instance must send an APAccept to this instance with the object
        begin the APMigrate activity id. This then triggers the migration.
        """
        self.logger.info(f"Received MIGRATE in inbox (activity id: {activity.id})", extra={"activity_id": activity.id})

        # Always validate content proof
        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 create_user_data(self, actor: APActor) -> APDocument:
        self.logger.info("Received Migrate activity. Creating user data")
        actor_id = actor.id
        if not actor_id:
            raise HandlerError("APMigrate.actor is None, cannot create user data file")
        actor_serialized = to_json_string(actor.serialize())

        document = APDocument()

        document.media_type = "application/json"
        document.name = actor_id

        # create object in storage so that it gets and id
        document = await CreateHandler.store_object_locally(handler=self, obj=document, actor_id=actor_id)
        if not document.id:
            raise HandlerError("Descriptor document for Actor does not have a proper id, cannot store actor as file")
        if not isinstance(document, APDocument):
            raise HandlerError("Descriptor document for Actor is not a proper APDocument, cannot store actor as file")

        filename = actor_id
        inmemory_file = UploadFile(filename=filename, file=io.BytesIO(actor_serialized.encode()))
        file_id = await self.storage.save(document_id=document.id, content=inmemory_file)
        document.url = [file_id]

        # get metadata that was saved with the file
        file_metadata = await self.storage.metadata(file_id=file_id)
        document.hash = file_metadata.hash_sha256

        if not document.hash:
            raise HandlerError("Not able to create SHA256 hash for file content")

        # create a proof of the file content, sign it with the actor key
        actor_current_key = await self.key_manager.get_key_id_from_actor(actor_id)
        document.proof = await self.key_manager.proof_string(string_to_sign=document.hash, private_key_id=actor_current_key)
        document.proof.description = f"SHA256 hash of the file content, signed by actor {actor_id}"

        # update the object with the file info
        _ = await self.storage.object.upsert(document.id, data=document)

        return document

    @staticmethod
    async def accept_migrate_on_new_instance(handler: ActivityHandler, activity: APMigrate):
        """Accept the migration event on the new instance.
        This will create an actor if they are not already created, and sets their memorial status to False.

        Args:
            activity (APMigrate): The APMigrate activity.
        """
        if not activity.actor_to:
            raise HandlerError(f"APMigrate.actor_to must not be None (activity {activity.id})")

        file_document = await handler.resolver.resolve_object(activity.object)
        file_urls = file_document.url
        if not file_urls:
            raise HandlerError("file_document.url should contain the url to the migration file (activity {activity.id})")

        # download the data from the old instance
        migration_objects: list[ActivityPubBase] = []

        # TODO: what is a good way to pass a user token for the old instance? Right now the user data must be publicly accessible...
        token = None
        for file_url in file_urls:
            resolved_file = b"".join([line async for line in await handler.resolver.resolve_media(file_id=file_url, access_token=token)])

            try:
                object_dict = cast(dict[str, Any], json.loads(resolved_file.decode()))
                migration_objects.append(object_from_type(object_dict))
            except json.JSONDecodeError:
                raise HandlerError(f"Unable to download user data from old instance during APMigrate (JSONDecodeError, activity {activity.id})")

        actor_object = None
        # account_object = None

        for obj in migration_objects:
            if isinstance(obj, APActor):
                actor_object = obj
            # if isinstance(obj, APAccount):
            #    account_object = obj

        if not actor_object:
            raise HandlerError(f"Files in migration data did not contain APActor data ({activity.id})")
    
        # TODO: instead of create actor update the actor_to actor here!
        _ = await handler.actor_manager.update_actor(update=actor_object, actor_id=activity.actor_to)
        # _ = self.actor_manager.create_account(activity=APCreate(object=account_object, actor=activity.actor_to))

    @staticmethod
    async def accept_migrate_on_previous_instance(handler: ActivityHandler, activity: APMigrate):
        """Handle migration on the previous instance of the migrating actor.
        This will disable the actor on the instance by setting their memorial=True and discoverable=False.

        Args:
            activity (APMigrate): The migration activity.
        """
        actor_id = dereference_or_raise(activity, "actor")
        if not (await handler.actor_manager.deactivate_actor(actor_id)):
            handler.logger.info(f"Unable to deactivate actor {actor_id} on the previous instance during migration")
        else:
            handler.logger.info(f"Deactivated actor {actor_id} on the previous instance during migration")
