"""
Context managers and routines that generate and upate actions and pass them to asynchronous callback functions, possibly
repeatedly and especially after an action transitions from one status to another. In general, callbacks should not
expect to receive an updated action reflecting every status transition, but they can expect to receive at least a final
status. All status transitions are guaranteed to be reflected in the action's attributes.

See heaobject.activity for a description of the action lifecycle.
"""
from types import TracebackType
from typing import Type
from heaobject.activity import DesktopObjectAction, Status
from heaobject.user import NONE_USER
from heaobject.root import Share, ShareImpl, Permission
from heaserver.service.oidcclaimhdrs import SUB
from aiohttp.web import Request, Application

from typing import Any
from collections.abc import Iterable, Callable, Awaitable, Coroutine
from contextlib import AbstractAsyncContextManager
from asyncio import Task, create_task, Future
from asyncio.exceptions import CancelledError
import logging


class DesktopObjectActionLifecycle(AbstractAsyncContextManager[DesktopObjectAction]):
    """
    Asynchronous context manager that generates desktop object actions and passes them to a callback multiple times,
    each time with a possible change to the action's status attribute to reflect a transition from one status to
    another.

    Actions generated by this context manager immediately go into IN_PROGRESS status before being passed to the
    callback, and so the callback is not invoked while the action has REQUESTED status. The callback may be
    invoked multiple times while the action is in IN_PROGRESS status, and then it will be called exactly once with a
    final status (SUCCEEDED or FAILED). An unhandled exception that is raised from the context manager block will cause
    the action's final status to be FAILED. Otherwise, the action's final status will be SUCCEEDED unless the context
    manager block sets the status to FAILED.

    A long-running context manager block should frequently yield control to other async tasks to avoid delays in
    sending action status updates to the callback.

    This class is not designed for inheritance.

    See the heaserver.service.messagebroker module for a callback that publishes the action to the HEA message broker.
    """

    def __init__(self, request: Request,
                 code: str,
                 description: str, *,
                 user_id: str | None = None,
                 shares: Iterable[Share] | None = None,
                 activity_cb: Callable[[Application, DesktopObjectAction], Awaitable[None]] | None = None) -> None:
        """
        Creates the context manager. A HTTP request, code, and description are required. Code may be any value that is
        meaningful to a microservice or web client, but HEA provides a set of standard codes that this context manager
        will populate automatically. They are:
            hea-get: Accessing a desktop object was attempted.
            hea-create: Creating a desktop object was attempted.
            hea-update: Updating a desktop object was attempted.
            hea-duplicate: Duplicating a desktop object was attempted.
            hea-move: Moving a desktop object was attempted.
            hea-archive: Archiving a desktop object was attempted.
            hea-unarchive: Unarchiving a desktop object was attempted.
        HEA reserves the hea- prefix for future extension. For custom codes, you must populate the action in the body
        of the context manager block.

        :param request: HTTP request (required).
        :param code: one of the codes above (required).
        :param description: A brief description of the action for display in a user interface (required).
        :param user_id: the id of the user who attempted the action.
        :param shares: the users with whom this action may be shared.
        :param activity_cb: an awaitable that publishes the action on the message queue. The context manager will
        generate IN_PROGRESS, SUCCEEDED, and FAILED statuses. If the awaitable is a Coroutine, it will be wrapped in
        a Task for execution. If the awaitable is a Future, Coroutine, or Task, an attempt will be made to cancel it
        upon exiting the context manager.
        :raises TypeError: if an argument of the wrong type was provided.
        :raises ValueError: if an otherwise invalid value was passed as an argument.
        """
        if code is None:
            raise ValueError('code cannot be None')
        if description is None:
            raise ValueError('description cannot be None')
        if not isinstance(request, Request):
            raise TypeError(f'request must be a Request but was a {type(request)}')
        self.__request = request
        self.__code = str(code)
        self.__description = str(description)
        self.__user_id = str(user_id) if user_id is not None else request.headers.get(SUB, NONE_USER)
        self.__shares = list(shares) if shares is not None else _default_shares(request)
        if any(not isinstance(share, Share) for share in self.__shares):
            raise ValueError(f'shares must all be Share objects but were {", ".join(set(str(type(share)) for share in self.__shares))}')
        self.__activity_cb = activity_cb
        self.__activity_cb_task: Awaitable[None] | None = None

    async def __aenter__(self) -> DesktopObjectAction:
        self.__activity = DesktopObjectAction()
        self.__activity.generate_application_id()
        self.__activity.code = self.__code
        self.__activity.owner = NONE_USER
        self.__activity.shares = self.__shares
        self.__activity.user_id = self.__user_id
        self.__activity.description = self.__description
        self.__activity.status = Status.IN_PROGRESS
        if self.__activity_cb:
            awaitable = self.__activity_cb(self.__request.app, self.__activity)
            match awaitable:
                case Coroutine():
                    self.__activity_cb_task = create_task(awaitable)
                case _:
                    self.__activity_cb_task = awaitable
        return self.__activity

    async def __aexit__(self, exc_type: Type[BaseException] | None,
                        exc_value: BaseException | None,
                        traceback: TracebackType | None) -> Any:
        logger = logging.getLogger(__name__)
        if self.__activity_cb_task:
            if isinstance(self.__activity_cb_task, (Task, Future)) and not self.__activity_cb_task.done():
                self.__activity_cb_task.cancel()
            try:
                await self.__activity_cb_task
            except CancelledError:
                logger.debug('Activity callback %s cancelled', self.__activity_cb)
        if exc_type is not None:
            self.__activity.status = Status.FAILED
        elif self.__activity.status not in (Status.SUCCEEDED, Status.FAILED):
            if exc_type is None:
                self.__activity.status = Status.SUCCEEDED
            else:
                self.__activity.status = Status.FAILED
        if self.__activity_cb:
            await self.__activity_cb(self.__request.app, self.__activity)


def _default_shares(request: Request) -> list[Share]:
    """
    Default shares are NONE_USER and VIEWER permission.
    """
    share = ShareImpl()
    share.user = request.headers.get(SUB, NONE_USER)
    share.permissions = [Permission.VIEWER]
    return [share]
