from http import HTTPStatus
from typing import Annotated, override

from fastapi import Depends, HTTPException, Path, Query, Security
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import ValidationError

from phederation.api.routes.base import BaseRoute
from phederation.models import (
    APActivity,
    APActor,
    APOrderedCollection,
    APOrderedCollectionPage,
    APUpdate,
    APUser,
    ErrorMessageActivityHandler,
    ErrorMessageActivityValidation,
    ErrorMessageActorUnauthorized,
    RelativeLink,
    RelativeLinkTemplate,
    ValidCollection,
)
from phederation.security.authentication import UserInfo
from phederation.utils import ObjectId
from phederation.utils.base import actor_id_from_username, collection_id_from_name
from phederation.utils.exceptions import (
    ActivityPubException,
    AuthenticationError,
    CollectionError,
    DeliveryError,
    HandlerError,
    UserError,
)


class ActorRoute(BaseRoute):

    async def _get_users(self, page: int | None = None) -> APOrderedCollectionPage:
        """Get all publicly available users.

        Note that access level to the entire users list does not use authorization,
        which means it is not possible to count the private users.

        Args:
            page (int | None, optional): The page to access. Defaults to None (first page).

        Returns:
            APOrderedCollectionPage: A paginated collection of all public users on the instance.
        """
        collection_id = collection_id_from_name(id=self.server.base_url, name="users")
        collection = await self.server.handle_pagination(collection_id=collection_id, page=page)
        return collection

    async def _get_user(
        self, actor: APActor, actor_for_authorization: ObjectId | None = None  # pyright: ignore[reportUnusedParameter]
    ) -> APUser | None:
        """Return the APActor information for the given username.

        The actor_for_authorization is unused, because even actors with discoverable=False
        must be accessible by their username / actor_id. Otherwise, http signature authentication
        (and more) would not be possible for these "private actors", because their public keys are not accessible.

        Args:
            username (str): username of the actor to return.
            actor_for_authorization (_type_, optional): actor.id of an actor trying to get access. Defaults to None, Unused.

        Returns:
            APUser | None: The APActor object for the given username, if it exists on the instance.
        """
        if actor.id is None:
            return None

        public_key = actor.public_key_as_ids()
        if not public_key:
            self.logger.error(f"Actor for id {actor.id} does not have a public_key.")

        user_dict = actor.serialize()
        user_dict["public_key"] = public_key
        user_dict["links"] = [
            RelativeLink(rel="self", type="application/activity+json", href=actor.id).model_dump(),
            RelativeLinkTemplate(
                rel="http://ostatus.org/schema/1.0/subscribe",
                template=f"{self.api.settings.domain.base_url}/authorize_interaction?uri={{uri}}",
            ).model_dump(),
        ]
        user_dict["preferred_username"] = actor.preferred_username
        try:
            user = APUser.deserialize(user_dict)
        except ValidationError as e:
            self.logger.warning(f"Could not deserialize APUser from actor '{actor.id}' in ActorRoute, error {e}. Data={user_dict}")
            return None

        if not isinstance(user, APUser):
            self.logger.warning(f"Could not deserialize APUser from actor '{actor.id}' in ActorRoute")
            return None

        return user

    async def _get_user_collection(
        self, actor_id: ObjectId | None, collection: str, page: int | None = None, actor_for_authorization: ObjectId | None = None
    ) -> APOrderedCollection:
        if not actor_id:
            raise CollectionError("Actor id is none")
        collection_id = collection_id_from_name(id=actor_id, name=collection)
        collection_obj = await self.server.handle_pagination(
            collection_id=collection_id,
            actor_id=actor_id,
            page=page,
            actor_for_authorization=actor_for_authorization,
        )
        return collection_obj

    @override
    def setup(self):

        @self.router.get(
            "/users",
            tags=["users"],
            response_model_exclude_none=True,
        )
        async def get_user_list(  # pyright: ignore[reportUnusedFunction]
            page: Annotated[int | None, Query(..., description="The page of the collection to load (if any)")] = None,
        ) -> APOrderedCollectionPage:
            """Get paginated OrderedCollection of all local and public users."""
            return await self._get_users(page=page)

        @self.router.put(
            "/users",
            tags=["users"],
            response_model_exclude_none=True,
        )
        async def create_new_user(  # pyright: ignore[reportUnusedFunction]
            user_info: Annotated[UserInfo, Security(self.middleware.authorization, scopes=["user"])],
        ):
            """Create a new user.
            The authorization route to authorize the user will already create them on the instance,
            so this route is only to set specific attributes that do not come from keycloak.
            """
            assert user_info.actor_id, "user_info actor id is not set"

            self.logger.info(f"Received new user account info to register user '{user_info.full_name}'")

            try:
                # update the account information
                account = await self.server.storage.account.read(user_info.actor_id)
                if not account:
                    raise UserError(f"Could not find account for user {user_info.actor_id}")
                account.email = user_info.email

                update_account = APUpdate(
                    object=account,
                    actor=user_info.actor_id,
                )
                _ = await self.server.update_account(activity=update_account)
            except ActivityPubException as e:
                self.logger.error(f"ActorRoute handler error in create_new_user: {e.message}")
                raise e

            return JSONResponse(
                content=jsonable_encoder({"Status": "User created successfully"}),
                status_code=HTTPStatus.OK,
                media_type="application/activity+json",
            )

        @self.router.delete("/users/{username}", tags=["users"])
        async def delete_user(  # pyright: ignore[reportUnusedFunction]
            username: str, user_info: Annotated[UserInfo, Security(self.middleware.authorization, scopes=["user"])]
        ):
            """Delete the given user (if authenticated as them)."""
            if username != user_info.preferred_username:
                raise UserError("User is not authenticated with the username of the actor to delete")

            actor_id = actor_id_from_username(username=username, base_url=self.api.settings.domain.base_url)

            try:
                await self.server.actor_manager.delete_actor(actor_id)
                return JSONResponse(
                    content={"Status": "OK", "detail": "Actor deleted."},
                    status_code=HTTPStatus.OK,
                    media_type="application/activity+json",
                )
            except ActivityPubException as e:
                self.logger.error(f"ActorRoute: could not delete actor, exception {e.message}.")
                return JSONResponse(
                    content={"Status": "Error", "detail": "Actor could not be deleted. " + (e.user_message or "")},
                    status_code=HTTPStatus.BAD_REQUEST,
                    media_type="application/activity+json",
                )

        @self.router.get(
            "/users/{username}",
            tags=["users"],
            response_model=APUser,
            response_model_exclude_none=True,
        )
        async def get_user_route(  # pyright: ignore[reportUnusedFunction]
            actor: Annotated[APActor, Depends(self.middleware.authentication.verify_local_username)],
            user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])],
        ) -> APUser:
            """Handle user data requests."""
            if user := await self._get_user(actor, actor_for_authorization=user_info.actor_id if user_info else None):
                return user
            raise HTTPException(status_code=HTTPStatus.GONE)

        @self.router.post(
            "/users/{username}/inbox",
            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,
                },
            },
        )
        async def post_user_inbox(  # pyright: ignore[reportUnusedFunction]
            actor: Annotated[APActor, Depends(self.middleware.authentication.verify_local_username)],
            activity: Annotated[APActivity, Security(self.middleware.activity_from_http, scopes=["user"])],
        ):
            """Handle actor inbox requests."""
            result = "Result unset"
            try:
                result = await self.server.handle_inbox(activity=activity, inbox_owner_id=actor.id)
                if result.success and actor.id:
                    activity_id = str(result.success[0])
                else:
                    raise HandlerError(f"Could not handle inbox of actor {actor.id}")
            except ActivityPubException as e:
                self.logger.debug(f"Handle inbox failed with {type(e).__name__} {e.message}. Failing (HttpStatus.INTERNAL_SERVER_ERROR).")
                return JSONResponse(
                    content=e.user_message,
                    status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
                    media_type="application/activity+json",
                )

            # as we have verified the remote url through HTTP signature, we can also tell the remote instance its current rate limit here
            response_headers = {"Location": activity_id}
            response_headers = self.middleware.rate_limiter.update_headers(domain=activity_id, headers=response_headers)
            self.logger.debug(f"Updated response headers for rate limit: {response_headers}")

            # 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={"Status": "OK"},
                status_code=HTTPStatus.CREATED,
                media_type="application/activity+json",
                headers=response_headers,
            )

        @self.router.post("/users/{username}/outbox", tags=["users"])
        async def post_user_outbox(  # pyright: ignore[reportUnusedFunction]
            activity: APActivity,
            user_info: Annotated[UserInfo, Security(self.middleware.authorization, scopes=["user"])],
            actor: Annotated[APActor, Depends(self.middleware.authentication.verify_local_username)],
        ):
            """
            Handle user outbox requests (client-to-server).

            The POST is secured by authentication, the request is not verified.
            This is necessary because then clients do not need to sign the headers (they cannot, because they do not have the private keys),
            but clients need to authenticate using Bearer token (Oauth2).
            """
            if actor.id is None:
                raise AuthenticationError(f"Actor does not have an id.")

            if user_info.preferred_username != actor.preferred_username:
                raise AuthenticationError(f"User with username '{actor.preferred_username}' is not the authenticated user.")

            # set actor who this was posted to as the owner of the activity.
            activity.actor = actor.id

            try:
                result = await self.server.handle_outbox(activity=activity)
                success_id = result.get_success_id()
                if not success_id:
                    raise DeliveryError(f"Delivery to outbox failed: result={result}, actor.id={actor.id}, activity={activity.type}")
            except ActivityPubException as e:
                self.logger.debug(f"Handle outbox failed with {type(e).__name__} {e.message}. Failing silently (HttpStatus.CONTINUE).")
                e.status_code = HTTPStatus.CONTINUE
                return JSONResponse(
                    content={"Status": "Error", "description": e.user_message},
                    status_code=HTTPStatus.CONTINUE,
                    media_type="application/activity+json",
                )

            # 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={"Status": "OK"},
                status_code=HTTPStatus.CREATED,
                media_type="application/activity+json",
                headers={"Location": success_id},
            )

        @self.router.get(
            "/users/{username}/blocks",
            response_model_exclude_none=True,
            tags=["users"],
            dependencies=[Security(self.middleware.authorization, scopes=["user"])],  # make blocks only available after authentication
        )
        async def get_user_blocks(  # pyright: ignore[reportUnusedFunction]
            actor: Annotated[APActor, Depends(self.middleware.authentication.verify_local_username)],
            user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])],
            page: Annotated[int | None, Query(..., description="The page of the collection to load (if any)")] = None,
        ) -> APOrderedCollection:
            """Return list of blocked objects (actors) by the authenticated actor.
            This will return all objects in the given users "blocks" collection, if available.
            """
            return await self._get_user_collection(
                actor_id=actor.id,
                collection=ValidCollection.Blocks.value,
                page=page,
                actor_for_authorization=user_info.actor_id if user_info else None,
            )

        @self.router.get(f"/users/{{username}}/{ValidCollection.Collections.value}/{{collection}}", response_model_exclude_none=True, tags=["users"])
        async def get_user_defined_collection(  # pyright: ignore[reportUnusedFunction]
            actor: Annotated[APActor, Depends(self.middleware.authentication.verify_local_username)],
            collection: Annotated[str, Path(description="Name of collection created by actor")],
            user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])],
            page: Annotated[int | None, Query(..., description="The page of the collection to load (if any)")] = None,
        ) -> APOrderedCollection | APOrderedCollectionPage:
            """Handle collection (GET) requests for user-defined collections.
            This will return public objects in the given users "collection", if available.
            If there are too many objects, it returns an OrderedCollectionPage of the collection.
            """
            # TODO: refactor so that "get_user_collection" is used instead (combined)
            collection_path = f"{ValidCollection.Collections.value}/{collection}"
            self.logger.debug(f"Accessing actor collection {collection_path}, page {page if page else "not set"}")
            collection_obj = await self._get_user_collection(
                actor_id=actor.id, collection=collection_path, page=page, actor_for_authorization=user_info.actor_id if user_info else None
            )
            return collection_obj

        @self.router.get("/users/{username}/{collection}", response_model_exclude_none=True, tags=["users"])
        async def get_user_collection(  # pyright: ignore[reportUnusedFunction]
            actor: Annotated[APActor, Depends(self.middleware.authentication.verify_local_username)],
            collection: Annotated[ValidCollection, Path(description="Collection name. Can be one of `ValidCollection`")],
            user_info: Annotated[UserInfo | None, Security(self.middleware.authentication, scopes=["user"])],
            page: Annotated[int | None, Query(..., description="The page of the collection to load (if any)")] = None,
        ) -> APOrderedCollection | APOrderedCollectionPage:
            """Handle certain collection (GET) requests.
            This will return public objects in the given users "collection", if available.
            If there are too many objects, it returns an OrderedCollectionPage of the collection.
            """
            self.logger.debug(f"Accessing actor collection '{collection.value}', page {page if page else "not set"}")
            collection_obj = await self._get_user_collection(
                actor_id=actor.id, collection=collection.value, page=page, actor_for_authorization=user_info.actor_id if user_info else None
            )
            return collection_obj

        @self.router.get(
            "/actor",
            tags=["instance"],
            response_model=APUser,
            response_model_exclude_none=True,
        )
        async def get_instance_actor() -> APUser:  # pyright: ignore[reportUnusedFunction]
            """Get instance actor."""
            actor = await self.middleware.authentication.verify_local_username(self.api.settings.federation.instance_name)
            if user := await self._get_user(actor):
                return user
            raise HTTPException(status_code=HTTPStatus.GONE)

        @self.router.get(
            "/inbox",
            tags=["instance"],
            response_model_exclude_none=True,
        )
        async def get_shared_inbox(page: int | None = None) -> APOrderedCollection:  # pyright: ignore[reportUnusedFunction]
            """Get activities from the shared inbox."""
            if "sharedInbox" not in self.api.settings.federation.endpoints:
                raise HTTPException(status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="Instance does not support the sharedInbox endpoint")

            actor = await self.middleware.authentication.verify_local_username(self.api.settings.federation.instance_name)
            inbox = await self._get_user_collection(
                page=page,
                actor_id=actor.id,
                collection="inbox",
                actor_for_authorization=actor.id,  # this makes the inbox as accessible as the instance actor
            )
            self.logger.debug(f"InstanceRoute: accessing shared inbox of instance {self.api.settings.federation.instance_name}.")
            return inbox
