# Copyright (C) 2022 Cochise Ruhulessin
#
# All rights reserved. No warranty, explicit or implicit, provided. In
# no event shall the author(s) be liable for any claim or damages.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""Declares :class:`AortaEndpoint`."""
import asyncio
import warnings
from typing import Any
from typing import Awaitable

import aorta
import fastapi
from aorta.messagehandler import MessageHandler
from aorta.models import Message
from aorta.types import RetryCommand
from fastapi.dependencies.utils import get_parameterless_sub_dependant
from fastapi.dependencies.utils import solve_dependencies

from cbra import Endpoint
from cbra.exceptions import Unserializable
from cbra.transaction import Transaction
from cbra.transaction import NullTransaction
from .eventarcendpoint import EventarcEndpoint
from .models import PubsubMessage


class AortaEndpoint(EventarcEndpoint):
    __module__: str = 'cbra.ext.google'
    document: bool = False
    summary: str = "Aorta"
    description: str = (
        "Receive a `google.cloud.pubsub.topic.v1.messagePublished` message "
        "containing an Aorta command, event or task, and run all handlers."
        "\n\nThis endpoint may respond with a non-2xx response code if any "
        "of the handlers fails to process the message."
    )
    provider: aorta.MessageHandlersProvider = aorta.get_default_provider()
    transaction: Transaction = fastapi.Depends(NullTransaction)
    mount_path: str = ".well-known/aorta"

    async def on_message(self, message: PubsubMessage) -> None | fastapi.Response:
        """Deserialize the Aorta object and run all matching handlers."""
        envelope = self.parse_aorta_message(message)
        handlers = self.provider.match(envelope) # type: ignore
        futures: list[Awaitable[None]] = []
        for handler_class in handlers:
            futures.append(self.run_handler(handler_class, envelope)) # type: ignore
        try:
            await asyncio.gather(*futures)
        except RetryCommand:
            self.logger.critical(
                "Retrying command per handler request (id: %s, version: %s, kind: %s)",
                envelope.metadata.id, envelope.api_version, envelope.kind
            )
            return fastapi.Response(status_code=503)

    async def run_handler(
        self,
        handler_class: type[MessageHandler],
        envelope: Message
    ) -> None:
        """Invoke `handler_class` with the given message."""
        obj: Any = envelope.get_object()
        dependant = get_parameterless_sub_dependant(
            depends=fastapi.Depends(handler_class),
            path='/'
        )
        values, errors, *_ = await solve_dependencies(
            request=self.request,
            dependant=dependant,
            body=obj,
            dependency_overrides_provider=None
        )
        if errors:
            warnings.warn(
                f"Improperly configured dependant: {handler_class.__name__}"
            )
            return
        handler: MessageHandler = dependant.call(**values) # type: ignore
        async with Transaction(publisher=self.publisher) as tx: # type: ignore
            tx.set_correlation_id(envelope.metadata.correlation_id)
            return await handler.handle(obj) # type: ignore

    def parse_aorta_message(self, message: PubsubMessage) -> Message:
        """Deserialize the message received from Google Pub/Sub and parse it
        into a previously registered Aorta type.
        """
        envelope: Message | None = None
        try:
            if not message.data:
                raise Unserializable
            envelope = self.provider.parse(message.get_data()) # type: ignore
        except Unserializable:
            warnings.warn(
                f"Message {message.message_id} did not contain an object "
                f"that was recognized by Aorta."
            )
            raise
        except aorta.UnknownMessageType:
            warnings.warn(
                f"Message {message.message_id} was succesfully deserialized "
                "but it did not contain an object that could be interpreted "
                "by Aorta."
            )
            raise

        self.logger.info(
            "Received %s/%s (id: %s, correlationId: %s)",
            envelope.api_version,
            envelope.kind,
            envelope.metadata.id,
            envelope.metadata.correlation_id
        )
        return envelope

    @Endpoint.register_exception # type: ignore
    async def on_unserializable(
        self,
        exception: Unserializable
    ) -> fastapi.Response:
        return fastapi.Response(
            status_code=200,
            headers={'X-Error-Code': "MALFORMED_OBJECT"}
        )

    @Endpoint.register_exception # type: ignore
    async def on_unknown_message_type(
        self,
        exception: aorta.exceptions.UnknownMessageType
    ) -> fastapi.Response:
        return fastapi.Response(
            status_code=200,
            headers={'X-Error-Code': "UNKNOWN_MESSAGE_TYPE"}
        )
