from typing import Annotated, override

from fastapi import Security

from phederation.api.routes.base import BaseRoute
from phederation.models.collections import APOrderedCollection
from phederation.models.objects import APObject, dereference
from phederation.security.authentication import UserInfo
from phederation.utils.base import (
    AccessType,
    ObjectType,
    UrlType,
    assemble_id_url,
    collection_id_from_name,
)
from phederation.utils.exceptions import AuthorizationError, ValidationError


class ObjectRoute(BaseRoute):

    @override
    def setup(self):

        # "response_model=None" is important here, because this allows to return fields of classes that inherit from APObject.
        # If it is not set, FastAPI will *only* return fields from APObject, and none from inheriting classes (like APDocument).
        @self.app.get("/objects/{object_primary}", tags=["objects"], response_model_exclude_none=True, response_model=None)
        async def get_object(  # pyright: ignore[reportUnusedFunction]
            object_primary: str, user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])] = None
        ) -> APObject:
            """Return object with given id, if accessible."""
            object_id = assemble_id_url(
                base_url=self.api.settings.domain.base_url,
                primary=object_primary,
                type=UrlType.Objects,
            )
            obj = await self.server.storage.object.read(id=object_id)
            if not obj:
                raise ValidationError("Object does not exist locally")

            # TODO: security: unit test for this visibility check
            actor_id = dereference(obj, "attributed_to")
            actor_for_authorization = user_info.actor_id if user_info else None
            access: AccessType = await self.server.determine_access(actor_id=actor_id, actor_for_authorization=actor_for_authorization)
            if not AccessType.from_visibility(obj.visibility).is_accessible(access):
                raise AuthorizationError(f"Object {object_id} cannot be accessed by actor {actor_for_authorization}, visibility is {obj.visibility}")

            # Add DataIntegrityProof here if required by settings (unless it is a tombstone)
            if self.server.settings.media.proof_object_hash and obj.type != ObjectType.TOMBSTONE.value:
                if obj.hash is None:
                    raise ValidationError(f"Cannot add proof to object {object_id}: object.hash is None")
                actor_id = dereference(obj, "attributed_to")
                if actor_id is None:
                    raise ValidationError(f"Cannot resolve object.attributed_to")
                private_key_id = await self.server.key_manager.get_key_id_from_actor(actor_id)
                obj.proof = await self.server.key_manager.proof_string(string_to_sign=obj.hash, private_key_id=private_key_id)

            # add information about the collections of the object
            obj.shares = f"{object_id}/shares"
            obj.likes = f"{object_id}/likes"
            return obj

        @self.app.get(
            "/objects/{object_id}/shares",
            response_model_exclude_none=True,
            tags=["objects"],
        )
        async def get_object_shares(  # pyright: ignore[reportUnusedFunction]
            object_id: str,
            page: int | None = None,
            user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])] = None,
        ) -> APOrderedCollection:
            """Return object shares list."""
            object_id = assemble_id_url(
                base_url=self.api.settings.domain.base_url,
                primary=object_id,
                type=UrlType.Objects,
            )
            obj = await self.server.storage.object.read(id=object_id)
            if not obj:
                raise ValidationError("Object does not exist locally.")
            actor_id = dereference(obj, "attributed_to")
            collection_id = collection_id_from_name(id=object_id, name="shares", page=None)
            return await self.server.handle_pagination(
                collection_id=collection_id, actor_id=actor_id, actor_for_authorization=user_info.actor_id if user_info else None, page=page
            )

        @self.app.get(
            "/objects/{object_id}/likes",
            response_model_exclude_none=True,
            tags=["objects"],
        )
        async def get_object_likes(  # pyright: ignore[reportUnusedFunction]
            object_id: str,
            page: int | None = None,
            user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])] = None,
        ) -> APOrderedCollection:
            """Return object likes list."""
            object_id = assemble_id_url(
                base_url=self.api.settings.domain.base_url,
                primary=object_id,
                type=UrlType.Objects,
            )
            obj = await self.server.storage.object.read(id=object_id)
            if not obj:
                raise ValidationError("Object does not exist locally.")

            collection_id = collection_id_from_name(id=object_id, name="likes", page=None)
            actor_id = dereference(obj, "attributed_to")
            return await self.server.handle_pagination(
                collection_id=collection_id, actor_id=actor_id, actor_for_authorization=user_info.actor_id if user_info else None, page=page
            )
