"""media.py

The media route, to serve and accept media objects.
"""

from http import HTTPStatus
import json
from typing import Annotated, Any, cast, override

from fastapi import Depends, File, HTTPException
from fastapi.datastructures import UploadFile
from fastapi.encoders import jsonable_encoder
from fastapi.params import Form, Security
from fastapi.responses import JSONResponse, StreamingResponse

from phederation.api.routes.base import BaseRoute
from phederation.models import APActivity, ErrorMessageActivityHandler
from phederation.models import (
    ErrorMessageActivityRestricted,
)
from phederation.models.objects import APDocument
from phederation.models.activities import ErrorMessageActivityValidation
from phederation.models.actors import APActor, ErrorMessageActorUnauthorized
from phederation.security.authentication import UserInfo
from phederation.utils.base import UrlType, assemble_id_url
from phederation.utils.exceptions import (
    ActivityPubException,
    HandlerError,
    SecurityError,
    UserError,
)


class MediaRoute(BaseRoute):

    @override
    def setup(self):

        @self.app.get("/media/{media_primary}", tags=["media"], response_description="A stream to download the media object (file), if it exists")
        async def get_media(  # pyright: ignore[reportUnusedFunction]
            media_primary: str, user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])] = None
        ) -> StreamingResponse:
            """Return media with given id, if accessible."""
            if "media" not in self.api.settings.federation.endpoints:
                raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Instance does not support the media endpoint")

            file_id = assemble_id_url(
                base_url=self.api.settings.domain.base_url,
                primary=media_primary,
                type=UrlType.Media,
            )
            metadata = await self.server.storage.metadata(file_id=file_id)
            document_id = metadata.document_id

            actor_requesting_access = user_info.actor_id if user_info else None

            document = await self.api.server.resolver.resolve_object(object_id=document_id)
            obj_with_access = await self.api.server.resolver.resolve_access_type(object=document, actor_requesting_access=actor_requesting_access)
            if not obj_with_access or not isinstance(obj_with_access, APDocument):
                self.logger.warning(f"Media file visibility restricted, user: {user_info} cannot access it")
                raise HTTPException(
                    status_code=HTTPStatus.BAD_REQUEST,
                    detail=ErrorMessageActivityRestricted().message,
                )

            # load the actual file from storage and stream it to the client
            obj = await self.server.storage.load(file_id=file_id)
            return StreamingResponse(obj)

        @self.router.post(
            "/users/{username}/endpoints/uploadMedia",
            tags=["users"],
            responses={
                HTTPStatus.BAD_REQUEST: {
                    "model": ErrorMessageActivityValidation,
                    "description": ErrorMessageActivityValidation().description,
                },
                HTTPStatus.INTERNAL_SERVER_ERROR: {
                    "model": ErrorMessageActivityHandler,
                    "description": ErrorMessageActivityHandler().description,
                },
                HTTPStatus.UNAUTHORIZED: {
                    "model": ErrorMessageActorUnauthorized,
                    "description": ErrorMessageActorUnauthorized().description,
                },
            },
            dependencies=[Security(self.middleware.authorization, scopes=["user"])],
        )
        async def post_user_endpoints(  # pyright: ignore[reportUnusedFunction]
            actor: Annotated[APActor, Depends(self.middleware.authentication.verify_local_username)],
            activity_form: Annotated[str, Form(..., description="Create activity sent in addition of the file data")],
            file: Annotated[UploadFile, File(..., description="File to upload to the media endpoint")],
        ):
            """Handle user media endpoint requests."""
            if "media" not in self.api.settings.federation.endpoints:
                raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Instance does not support the media endpoint")

            if actor.id is None:
                raise UserError("Actor does not have an id")

            activity_dict = cast(dict[str, Any], json.loads(activity_form))
            activity = self.api.server.resolver.resolve_from_dict(activity_dict)
            if not isinstance(activity, APActivity):
                raise HandlerError("Object sent was not an APActivity")
            activity.attributed_to = actor.id
            activity = await self.server.media_handler.validate(activity=activity, actor_id=actor.id)

            # we can now create the media file and store it in the activity.
            if self.api.settings.media.create_file_hash:
                actor_current_key = await self.server.key_manager.resolve_current_key(actor.public_key_as_ids())
                if not actor_current_key:
                    raise SecurityError("Actor public key not found")

                async def proof_string(string_value: str):
                    return await self.server.key_manager.proof_string(string_to_sign=string_value, private_key_id=actor_current_key.id)

                document = await self.server.media_handler.process_attachment(attachment=file, description=activity.object, proof_string=proof_string)

                document.attributed_to = actor.id
                activity.object = document

            result = "Result unset"
            try:
                result = await self.server.handle_outbox(activity=activity)
                activity_id = result.get_success_id()
                if not activity_id:
                    raise HandlerError(f"Did not create activity id in outbox, result={result}")
            except ActivityPubException as e:
                self.logger.error(f"Exception in post_user_endpoints: {e.message}")
                raise HTTPException(detail=e.user_message, status_code=e.status_code)

            # Servers MUST return a 201 Created HTTP code, and unless the activity is transient, MUST include the new id in the Location header.
            # https://www.w3.org/TR/activitypub/#client-to-server-interactions
            return JSONResponse(
                content=jsonable_encoder({"Status": "OK"}),
                status_code=HTTPStatus.CREATED,
                media_type="application/activity+json",
                headers={"Location": activity_id},
            )
