"""
Middleware between FastAPI and the activitypub implementation.
"""

from collections.abc import Awaitable, Callable
from datetime import datetime, timezone
from http import HTTPStatus
from logging import Logger
from typing import Annotated, Any

from fastapi import Depends, FastAPI
from fastapi import Security
from fastapi.requests import Request
from fastapi.responses import JSONResponse
from fastapi.responses import Response

from phederation.cache import BaseCache, WithCache
from phederation.cache.base import with_cache
from phederation.federation.ratelimit import RateLimiter
from phederation.federation.server import ActivityPubServer
from phederation.models import dereference
from phederation.models.activities import APActivity
from phederation.models.maintenance import APMaintenanceMode
from phederation.models.objects import dereference_or_raise
from phederation.security import Authentication
from phederation.security.authentication import UserInfo
from phederation.security.authorization import Authorization
from phederation.security.http_signatures import HTTPSignatureVerifier
from phederation.statistics.statistics import Statistics
from phederation.utils import ObjectId
from phederation.utils.base import AccessType, UrlType, serialize_datetime
from phederation.utils.exceptions import (
    AuthenticationError,
    MiddlewareError,
    SecurityError,
)
from phederation.utils.logging import configure_logger
from phederation.utils.settings import PhedSettings


class ActivityPubMiddleware(WithCache):
    """ActivityPub middleware."""

    def __init__(
        self,
        app: FastAPI,
        server: ActivityPubServer,
        statistics: Statistics,
        settings: PhedSettings,
        signature_verifier: HTTPSignatureVerifier | None,
        rate_limiter: RateLimiter,
        cache: BaseCache | None = None,
    ):
        self.settings: PhedSettings = settings
        self.server: ActivityPubServer = server
        self.statistics: Statistics = statistics
        self.signature_verifier: HTTPSignatureVerifier | None = signature_verifier
        self.rate_limiter: RateLimiter = rate_limiter
        self.app: FastAPI = app
        self.authentication: Authentication = Authentication(settings=settings, actor_manager=server.actor_manager)
        self.authorization: Authorization = Authorization(settings=settings, authentication=self.authentication)
        self.logger: Logger = configure_logger(__name__, prefix=self.settings.federation.logging_prefix)
        self.cache: BaseCache | None = cache

        self.activity_from_token: Callable[[APActivity, UserInfo | None], Awaitable[APActivity]] | None = None
        self.activity_from_http: Callable[[APActivity, ObjectId | None], Awaitable[APActivity]] | None = None

    async def initialize(self):
        """Setup of all middleware routes."""
        self.logger.debug(f"Initializing middleware")

        @self.app.middleware("http")
        async def activitypub_middleware(request: Request, call_next: Callable[..., Awaitable[Response]]):  # pyright: ignore[reportUnusedFunction]
            """Processes all requests before they are handled internally."""
            request_url = str(request.url)
            self.logger.debug(f"Middleware processing request {request.method} at {request_url}")

            client_ip: str | None = request.headers.get("x-forwarded-for", None)
            if not client_ip and request.client:
                client_ip = f"{request.client.host}:{request.client.port}"
            else:
                client_ip = RateLimiter.EVERYBODY

            if self.settings.security.verify_ssl and request.url.scheme != "https":
                raise SecurityError(f"SSL verification failed in middleware for request url {request_url}")

            response_headers: dict[str, str] = {}
            if command := await self.check_maintenance_mode():
                if not self.settings.federation.admin_mode:
                    now = datetime.now(tz=timezone.utc)
                    if command.start_time:
                        response_headers["x-maintenance-start-time"] = serialize_datetime(command.start_time)
                    if command.end_time:
                        response_headers["x-maintenance-end-time"] = serialize_datetime(command.end_time)
                    if command.id:
                        response_headers["x-maintenance-id"] = command.id
                    if (not command.start_time or command.start_time <= now) and (not command.end_time or now <= command.end_time):
                        response_dict = {"status": "maintenance", "detail": "In maintenance mode."}
                        for key, value in response_headers.items():
                            response_dict[key] = value
                        return JSONResponse(status_code=HTTPStatus.METHOD_NOT_ALLOWED, content=response_dict, headers=response_headers)

            # test if the request includes a valid HTTP signature; which then can enable higher rate limits
            try:
                rate_limit_domain = await _verify_http_signature(request=request, verified_actor_key_id=None)
            except AuthenticationError:
                # if not authenticated, rate limit on ip
                rate_limit_domain = client_ip

            if self.rate_limiter.check_rate_limit(domain=rate_limit_domain):
                response_headers = self.rate_limiter.update_headers(domain=rate_limit_domain, headers=response_headers)

                response = await call_next(request)
                response = await self.process_response(response, response_headers=response_headers)
            else:
                response = self.rate_limiter.ratelimit_response(rate_limit_domain)

            response = await self.statistics.log(request=request, response=response)
            return response

        async def _verify_http_signature(
            request: Request, verified_actor_key_id: Annotated[ObjectId | None, Depends(self.get_key_from_activity)]
        ) -> ObjectId:
            """
            Verify signature of incoming HTTP request.

            Args:
                request: Incoming Request to verify HTTP signature

            Returns:
                ObjectID: key id used to sign the request.

            Raises:
                MiddlewareError: If the middleware is not set up properly.
                AuthenticationError: If the HTTP request is signed improperly (or not signed at all).
            """
            # Verify HTTP signature, including body
            if self.signature_verifier:
                # TODO: does this pull an entire file if a media file is sent? This should not be the case,
                # file hashes are computed in media.py and iteratively.
                body_serialized = (await request.body()).decode(encoding="utf-8")
                key_id = await self.signature_verifier.verify_signature(
                    headers=dict(request.headers),
                    body_serialized=body_serialized,
                    verified_key_id=verified_actor_key_id,
                    method=request.method,
                    path=request.url.path,
                )
                if not key_id:
                    raise AuthenticationError(f"Signature could not be verified. Method={request.method}, path={request.url.path}")
            else:
                raise MiddlewareError(
                    f"Signature could not be verified: Middleware does not have a signature verifier object, will just stop all requests."
                )

            return key_id

        async def _activity_from_token(
            activity: APActivity,
            user_info: Annotated[UserInfo | None, Security(self.authentication, scopes=["user"])],
        ) -> APActivity:
            if user_info and user_info.actor_id and user_info.actor_id == dereference(activity, "actor"):
                return activity
            raise AuthenticationError(f"Activity {activity.type} actor could not be authenticated from token")

        async def _activity_from_http(
            activity: APActivity,
            key_id: Annotated[ObjectId | None, Security(_verify_http_signature, scopes=["user"])],
        ) -> APActivity:
            actor_id = dereference_or_raise(activity, "actor")
            if key_id:
                # dereference public key from activity actor, check if they are the same as the http key id
                actor_key_id = await self.server.key_manager.get_key_id_from_actor(actor_id)
                if actor_key_id and actor_key_id == key_id:
                    return activity
                else:
                    raise AuthenticationError(f"Activity {activity.type} actor could not be authenticated from HTTP signature, keys do not match")
            raise AuthenticationError(f"Activity {activity.type} actor could not be authenticated from HTTP signature, key_id is None")

        self.activity_from_token = _activity_from_token
        self.activity_from_http = _activity_from_http

    @with_cache(prefix=__name__)
    async def check_maintenance_mode(self) -> APMaintenanceMode | None:
        """Check if the maintenance mode has been scheduled by an admin.

        Returns:
            APMaintenanceMode | None: Either APMaintenanceMode command if maintenance mode was scheduled, or None.
        """
        maintenance_commands_id = self.server.instance.maintenance_commands
        command_ids = await self.server.collections.get_collection_items(collection_id=maintenance_commands_id, access=AccessType.PRIVATE)
        for id in command_ids:
            self.logger.critical(f"Processing maintenance id in commands: {id}")
            maintenance_command = await self.server.resolver.try_resolve_from_storage(url=id, url_type=UrlType.Objects)
            self.logger.critical(f"Resolved command: {maintenance_command}")
            if isinstance(maintenance_command, APMaintenanceMode):
                now = datetime.now(timezone.utc)
                if not maintenance_command.end_time or maintenance_command.end_time > now:
                    return maintenance_command
        else:
            return None

    async def get_key_from_activity(self, activity: APActivity) -> ObjectId | None:
        """Get the id of the public key of the actor of the given activity.

        The activity.actor does not need to be local to the instance.

        Args:
            activity (APActivity): An activity with an actor.

        Returns:
            ObjectId | None: The id (an url) of the public key, or None.
        """
        actor_id = dereference(activity, "actor")
        if not actor_id:
            return None
        key_id = await self.server.key_manager.get_key_id_from_actor(actor_id=actor_id)
        return key_id

    async def process_response(self, response: Response, response_headers: dict[str, Any] | None = None) -> Response:
        """
        Process outgoing response.

        Args:
            response: Response from the call.

        Returns:
           Response with processed headers.
        """

        # Add standard headers for Json responses
        headers_lower = {key.lower(): response.headers.get(key) for key in response.headers.keys()}
        content_type = response.headers["content-type"] if "content-type" in headers_lower else None
        content_type = response.headers["accept"] if "accept" in headers_lower else content_type
        if content_type and "json" in content_type:
            response.headers.update({"Vary": "Accept, Accept-Encoding", "Cache-Control": "max-age=0, private, must-revalidate"})
            if response_headers:
                response.headers.update(response_headers)

        return response
