"""
Media attachment handling.
"""

from collections.abc import Callable, Coroutine
from logging import Logger
from typing import Any

from fastapi.datastructures import UploadFile

from phederation.federation.resolver import ActivityPubResolver
from phederation.models import APActivity, APDocument, APObject
from phederation.models.objects import dereference
from phederation.models.proofs import DataIntegrityProof
from phederation.storage.base import StorageBackend
from phederation.utils import ActivityType, NodeInfo, ObjectId
from phederation.utils.exceptions import (
    AuthenticationError,
    HandlerError,
    MediaError,
    catch_exceptions,
)
from phederation.utils.logging import configure_logger
from phederation.utils.settings import PhedSettings


class MediaHandler:
    """Handle media attachments."""

    def __init__(self, storage_backend: StorageBackend, resolver: ActivityPubResolver, settings: PhedSettings):
        self.storage: StorageBackend = storage_backend
        self.resolver: ActivityPubResolver = resolver
        self.settings: PhedSettings = settings
        self.logger: Logger = configure_logger(__name__, prefix=self.settings.federation.logging_prefix)

    async def validate(self, activity: APObject, actor_id: ObjectId) -> APActivity:
        if not isinstance(activity, APActivity):
            raise HandlerError("activity send to media endpoint is not an APActivity")

        if activity.type != ActivityType.CREATE.value:
            raise HandlerError("Cannot create media without Create activity")

        if dereference(activity, "actor") != actor_id:
            raise AuthenticationError("Sending actor must be 'actor' in activity")

        document = activity.object
        if document is None:
            raise HandlerError("When sending media files, activity Create must have an object")
        if isinstance(document, APObject):
            # this is necessary to convert to a proper APDocument
            document = document.serialize()
        if isinstance(document, str):
            raise HandlerError("When sending media files, activity Create object must not be an url, but the actual object (as dict)")

        document = self.resolver.resolve_from_dict(document)
        if isinstance(document, DataIntegrityProof):
            raise HandlerError("Document to resolve was a DataIntegrityProof, not an APObject")
        if isinstance(document, NodeInfo):
            raise HandlerError("Document to resolve was a NodeInfo, not an APObject")
        document.attributed_to = actor_id
        activity.object = document

        if self.settings.media.require_content_hash:
            if not document.hash:
                raise HandlerError(
                    f"Document {(document.serialize())} should have a SHA256 hash of the file content in the 'hash' field.",
                    user_facing_message="Security settings demand that the SHA256 hash of the uploaded file is stored in the 'hash' field of the Document",
                )
        return activity

    @catch_exceptions(MediaError, "Failed to process attachment")
    async def process_attachment(
        self,
        attachment: UploadFile,
        description: APObject | dict[str, Any] | str | None,
        proof_string: Callable[..., Coroutine[Any, Any, DataIntegrityProof]] | None = None,
    ) -> APDocument:
        """
        Process media attachment.
        If required by security settings, also checks the file SHA-hash against the one provided in the description.

        Returns:
            Processed attachment object
        """
        if not description:
            description = {}
        if isinstance(description, str):
            description = {"description": description}
        media_info: dict[str, Any] = description.serialize() if isinstance(description, APObject) else description
        media_info["type"] = "Document"
        document = APDocument.deserialize(media_info)
        if not isinstance(document, APDocument):
            raise MediaError(f"Could not deserialize APDocument with description {description}")
        
        # store the document to get an id
        document.id = await self.storage.object.create(data=document, raise_if_exists=True)

        # Validate mime type
        mime_type = document.media_type or attachment.content_type
        if mime_type is None or mime_type not in self.settings.media.allowed_types:
            raise MediaError(f"Unsupported media type: {mime_type}")

        if attachment.size is None:
            raise MediaError("Media does not have set size")
        if attachment.size > self.settings.media.max_size:
            raise MediaError("Media too large")

        # Save file using storage backend
        file_id = await self.storage.save(document_id=document.id, content=attachment)

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

        # TODO: Process media based on type

        document.media_type = mime_type
        document.url = [file_id]
        document.name = file_metadata.filename

        if self.settings.media.require_file_hash:
            if document.hash != file_metadata.hash_sha256:
                raise MediaError(
                    f"SHA256 file hashes do not agree for document '{document.id}' with file '{file_id}'",
                    user_facing_message="SHA256 file hashes do not agree. Security settings demand that they are verified",
                )
        document.hash = file_metadata.hash_sha256

        if file_metadata.hash_sha256 is not None and proof_string is not None and self.settings.media.create_file_hash:
            document.proof = await proof_string(file_metadata.hash_sha256)
            document.proof.description = "Actor signed sha256 hash of the file content."

        # udpate the document with the new file information
        _ = await self.storage.object.upsert(id=document.id, data=document)

        return document
