"""
The HEA Server Activity Microservice manages activity events generated by other microservices. It uses the HEA
message broker and subscribes to all Activity types found in the heaobject.activity module.
"""

from heaserver.service import response, requestproperty, client
from heaserver.service.runner import routes, web, start, init_cmd_line
from heaserver.service.db import mongo, mongoservicelib
from heaserver.service.messagebroker import subscriber_cleanup_context_factory
from heaserver.service.wstl import action, builder_factory
from heaserver.service.heaobjectsupport import RESTPermissionGroup
from heaserver.service.oidcclaimhdrs import SUB
from heaserver.service.representor.factory import formats_from_request
from heaserver.service import appproperty
from heaobject.activity import DesktopObjectAction, DesktopObjectSummaryStatus
from heaobject.root import DesktopObjectDict, PermissionContext, DesktopObject
from heaobject.user import NONE_USER
from heaobject.organization import Organization
from heaobject.volume import Volume
from aiohttp import WSMsgType, hdrs
from asyncio import gather, TimeoutError as AsyncioTimeoutError
from typing import Any, Literal
from yarl import URL
import logging

MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION = 'desktopobjectactions'

_wsresponses: dict[web.WebSocketResponse, tuple[web.Request, list[str]]] = {}

@routes.get('/recentlyaccessedviewsping')
async def ping(request: web.Request) -> web.Response:
    """
    Checks if this service is running.

    :param request: the HTTP request.
    :return: the HTTP response.
    """
    return await mongoservicelib.ping(request)

@routes.get('/recentlyaccessedviews')
@routes.get('/recentlyaccessedviews/')
@routes.get('/recentlyaccessedviews/bytype/{type}')
@routes.get('/recentlyaccessedviews/bytype/{type}/')
@action('heaserver-activity-recentlyaccessed-get-actual', rel='hea-actual', path='{+actual_object_uri}')
async def get_recently_accessed(request: web.Request) -> web.Response:
    """
    Gets RecentlyAccessedView objects. The objects are sorted by the status_updated field in descending order.

    :param request: the HTTP request.
    :return: the requested RecentlyAccessedView objects.
    ---
    summary: A list of recently accessed objects, optionally by type, and with a variety of filtering options.
    tags:
        - heaserver-activity
    parameters:
        - name: type
          in: path
          required: true
          description: The view type
          schema:
            type: string
          examples:
            example:
              summary: a desktop object type name
              value: heaobject.project.AWSS3Project
        - $ref: '#/components/parameters/OIDC_CLAIM_sub'
        - name: type
          in: query
          required: true
          description: The view type
          schema:
            type: string
          examples:
            example:
              summary: a desktop object type name
              value: heaobject.project.AWSS3Project
        - name: begin
          in: query
          description: The index of the first item to return (zero-based).
          schema:
            type: integer
            minimum: 0
          examples:
            example:
              summary: the index of the first item to return
              value: 0
        - name: end
          in: query
          description: The index of the last item to return (exclusive, zero-based).
          schema:
            type: integer
            minimum: 0
          examples:
            example:
              summary: the index of the last item to return
              value: 10
        - name: volume_id
          in: query
          description: The volume ids of the items to return. Omitting this parameter will return items in all volumes.
          schema:
            type: array
            items:
              type: string
          examples:
            example:
              summary: the volume ids of the items to return
              value: 'volume_id1'
        - name: object_uri
          in: query
          description: The object URIs of the items to return. Omitting this parameter will return items with any object URI.
          schema:
            type: array
            items:
              type: string
          examples:
            example:
              summary: the object URIs of the items to return
              value: ['volumes/volume_id1/bucket_ids/bucket_id1/awss3folders/object_id1']
        - name: organization_id
          in: query
          description: The organization ids of the items to return. Omitting this parameter will return items with any organization ID.
          schema:
            type: array
            items:
              type: string
          examples:
            example:
              summary: the organization ids of the items to return
              value: ['1234567890abcdef']
    responses:
      '200':
        $ref: '#/components/responses/200'
    """
    logger = logging.getLogger(__name__)
    begin = int(request.query.get('begin', 0))
    end_str = request.query.get('end', None)
    end = int(end_str) if end_str else None
    empty_list_of_string: list[str] = []
    sub = request.headers.get(SUB, NONE_USER)

    if 'type' in request.query:
        type_name = request.query['type']
    elif 'type' in request.match_info:
        type_name = request.match_info['type']
    else:
        return response.status_bad_request('No type specified')

    volume_ids = request.query.getall('volume_id', empty_list_of_string)

    if object_uris := request.query.getall('object_uri', empty_list_of_string):
        object_uri_query: dict[str, list[dict[str, dict[str, list[str] | None]]]] = {'$or': [{'new_object_uri': {'$in': object_uris}}, {'old_object_uri': {'$in': object_uris}}]}  # The property exists and its value is in the list.
    else:
        object_uri_query = {'$or': [{'new_object_uri': {'$ne': None}}, {'old_object_uri': {'$ne': None}}]}  # The property exists and is not null.
    if organization_ids := request.query.getall('organization_id', empty_list_of_string):
        if organization_ids:
            org_volumes_url = await client.get_resource_url(request.app, Organization)
            assert org_volumes_url is not None, 'No organization resource URL found'
            for org_id in organization_ids:
                async for volume in client.get_all(request.app, URL(org_volumes_url) / org_id / 'volumes', Volume):
                    assert volume.id is not None, f'Volume {volume} has no id'
                    volume_ids.append(volume.id)
    if volume_ids:
        volume_id_query = {'$or': [{'old_volume_id': {'$in': volume_ids}}, {'new_volume_id': {'$in': volume_ids}}]}  # Why do we allow null or non-existent volume ids to sneak in?
    else:
        volume_id_query = {}

    logger.debug('Requested recent accesses')

    pipeline: list[dict[str, Any]] = [{ "$match": { "$and": [
                        {'$or': [{'owner': sub}, {'$and': [{"shares.user": sub}, {'shares.permissions': {'$in': [perm.name for perm in RESTPermissionGroup.GETTER_PERMS.perms]}}]}]},
                        {'$or': [{'new_object_type_name': type_name}, {'old_object_type_name': type_name}]},
                        {'status': "SUCCEEDED"},
                        volume_id_query,
                        object_uri_query,
                        ]}},
        {'$sort': {'status_updated': -1}},
        {'$facet': {
        'newOrUpdated': [{'$match': {'new_object_uri': {'$ne': None}}}, {'$group': {'_id': '$new_object_uri', 'obj': {'$first': '$$ROOT'}}}, {'$addFields': {'status': 'newOrUpdated'}}],
        'deleted': [{'$match': {'$and': [{'old_object_uri': {'$ne': None}}, {'new_object_uri': {'$ne': '$old_object_uri'}}]}}, {'$group': {'_id': '$old_object_uri', 'obj': {'$first': '$$ROOT'}}}, {'$addFields': {'status': 'deleted'}}]}
        },
        {'$project': {'mergeArray': {'$concatArrays': ['$newOrUpdated', '$deleted']}}},
        {'$unwind': '$mergeArray'},
        {'$sort': {'mergeArray.obj.status_updated': -1}},
        {'$group': {'_id': '$mergeArray._id', 'obj': {'$first': '$$ROOT'}}},
        {'$project': {'obj': '$obj.mergeArray.obj', 'status': '$obj.mergeArray.status', 'new_object_display_name': '$obj.mergeArray.obj.new_object_display_name'}},
        {'$match': {'status': 'newOrUpdated', 'new_object_display_name': {'$ne': None}}},
        {'$project': {
            'id': '$obj.new_object_id',
            'type': 'heaobject.activity.RecentlyAccessedView',
            'actual_object_id': '$obj.new_object_id',
            'actual_object_type_name': '$obj.new_object_type_name',
            'actual_object_uri': '$obj.new_object_uri',
            'accessed': '$obj.status_updated',
            'created': '$obj.created',
            'modified': '$obj.modified',
            'owner': '$obj.user_id',
            'context': '$obj.context',
            'context_dependent_object_path': '$obj.new_context_dependent_object_path',
            'display_name': '$obj.new_object_display_name',
            'description': '$obj.new_object_description'
        }},
        {'$sort': {'accessed': -1}}]
    if begin:
        pipeline.append({'$skip': begin})
    if end:
        pipeline.append({'$limit': end - begin})

    return await mongoservicelib.aggregate(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION, pipeline)


@routes.get('/desktopobjectsummaryviews')
@routes.get('/desktopobjectsummaryviews/')
@routes.get('/desktopobjectsummaryviews/bytype/{type}')
@routes.get('/desktopobjectsummaryviews/bytype/{type}/')
@action('heaserver-activity-desktopobjectsummaries-get-actual', rel='hea-actual', path='{+actual_object_uri}')
async def get_desktop_object_summary(request: web.Request) -> web.Response:
    """
    Gets DesktopObjectSummaryView objects. This endpoint differs from /recentlyaccessedviews in that it returns
    information about deleted objects as well as new or updated objects. If a sort query parameter is provided, the
    status_updated field is used for sorting.

    :param request: the HTTP request.
    :return: the requested activity summary objects.
    ---
    summary: A list of desktop object summary views, optionally by type, and with a variety of filtering and sorting
    options.
    tags:
        - heaserver-activity
    parameters:
        - name: type
          in: path
          required: true
          description: The view type
          schema:
            type: string
          examples:
            example:
              summary: a desktop object type name
              value: heaobject.project.AWSS3Project
        - name: type
          in: query
          required: true
          description: The view type
          schema:
            type: string
          examples:
            example:
              summary: a desktop object type name
              value: heaobject.project.AWSS3Project
        - name: begin
          in: query
          description: The index of the first item to return.
          schema:
            type: integer
            minimum: 0
          examples:
            example:
              summary: the index of the first item to return
              value: 0
        - name: end
          in: query
          description: The index of the last item to return.
          schema:
            type: integer
            minimum: 0
          examples:
            example:
              summary: the index of the last item to return
              value: 10
        - name: volume_id
          in: query
          description: The volume ids of the items to return. Omitting this parameter will return items in all volumes.
          schema:
            type: array
            items:
              type: string
            examples:
              example:
                summary: the volume ids of the items to return
                value: ['volume_id1', 'volume_id2']
        - name: object_uri
          in: query
          description: The object URIs of the items to return. Omitting this parameter will return items with any object URI.
          schema:
            type: array
            items:
              type: string
          examples:
            example:
              summary: the object URIs of the items to return
              value: ['volumes/volume_id1/bucket_ids/bucket_id1/awss3folders/object_id1']
        - name: sort
          in: query
          description: The sort order. May be 'asc' or 'desc'. If omitted, the data is returned unsorted.
          schema:
            type: string
            enum:
                - 'asc'
                - 'desc'
            examples:
              example:
                summary: the sort order
                value: 'asc'
        - $ref: '#/components/parameters/OIDC_CLAIM_sub'
    responses:
      '200':
        $ref: '#/components/responses/200'
    """
    logger = logging.getLogger(__name__)
    begin = int(request.query.get('begin', 0))
    end_str = request.query.get('end', None)
    end = int(end_str) if end_str else None
    empty_list_of_string: list[str] = []
    sub = request.headers.get(SUB, NONE_USER)

    if 'type' in request.query:
        type_name = request.query['type']
        type_name_query = {'$or': [{'new_object_type_name': type_name}, {'old_object_type_name': type_name}]}
    elif 'type' in request.match_info:
        type_name = request.match_info['type']
        type_name_query = {'$or': [{'new_object_type_name': type_name}, {'old_object_type_name': type_name}]}
    else:
        type_name_query = {}

    if volume_ids := request.query.getall('volume_id', empty_list_of_string):
        volume_id_query = {'$or': [{'new_volume_id': {'$exists': False}}, {'new_volume_id': {'$in': volume_ids + [None]}}]}  # Why do we allow null or non-existent volume ids to sneak in?
    else:
        volume_id_query = {}

    if object_uris := request.query.getall('object_uri', empty_list_of_string):
        object_uri_query: dict[str, list[dict[str, dict[str, list[str] | None]]]] = {'$or': [{'new_object_uri': {'$in': object_uris}}, {'old_object_uri': {'$in': object_uris}}]}  # The property exists and its value is in the list.
    else:
        object_uri_query = {'$or': [{'new_object_uri': {'$ne': None}}, {'old_object_uri': {'$ne': None}}]}  # The property exists and is not null.

    sort_order_dict = mongoservicelib.MongoSortOrder.from_request_dict_raises_http_error(request, 'status_updated')

    logger.debug('Requested desktop object summaries')

    pipeline: list[dict[str, Any]] = [{ "$match": { "$and": [
                        {'$or': [{'owner': sub}, {'$and': [{"shares.user": sub}, {'shares.permissions': {'$in': [perm.name for perm in RESTPermissionGroup.GETTER_PERMS.perms]}}]}]},
                        type_name_query,
                        {'status': "SUCCEEDED"},
                        volume_id_query,
                        object_uri_query,
                        ]}},
        {'$sort': {'status_updated': -1}},
        {'$facet': {
        'newOrUpdated': [{'$match': {'new_object_uri': {'$ne': None}}}, {'$group': {'_id': '$new_object_uri', 'obj': {'$first': '$$ROOT'}}}, {'$addFields': {'status': 'newOrUpdated'}}],
        'deleted': [{'$match': {'$and': [{'old_object_uri': {'$ne': None}}, {'new_object_uri': {'$ne': '$old_object_uri'}}]}}, {'$group': {'_id': '$old_object_uri', 'obj': {'$first': '$$ROOT'}}}, {'$addFields': {'status': 'deleted'}}]}
        },
        {'$project': {'mergeArray': {'$concatArrays': ['$newOrUpdated', '$deleted']}}},
        {'$unwind': '$mergeArray'},
        {'$sort': {'mergeArray.obj.status_updated': -1}},
        {'$group': {'_id': '$mergeArray._id', 'obj': {'$first': '$$ROOT'}}},
        {'$project': {'obj': '$obj.mergeArray.obj', 'status': '$obj.mergeArray.status'}},
        {'$project': {
            'id': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': '$obj.new_object_id', 'else': '$obj.old_object_id'}},
            'type': 'heaobject.activity.DesktopObjectSummaryView',
            'actual_object_id': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': '$obj.new_object_id', 'else': '$obj.old_object_id'}},
            'actual_object_type_name': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': '$obj.new_object_type_name', 'else': '$obj.old_object_type_name'}},
            'actual_object_uri': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': '$obj.new_object_uri', 'else': '$obj.old_object_uri'}},
            'accessed': '$obj.status_updated',
            'created': '$obj.created',
            'modified': '$obj.modified',
            'owner': '$obj.user_id',
            'context': '$obj.context',
            'context_dependent_object_path': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': '$obj.new_context_dependent_object_path', 'else': '$obj.old_context_dependent_object_path'}},
            'display_name': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': '$obj.new_object_display_name', 'else': '$obj.old_object_display_name'}},
            'description': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': '$obj.new_object_description', 'else': '$obj.old_object_description'}},
            'status': {'$cond': {'if': {'$eq': ['$status', 'newOrUpdated']}, 'then': DesktopObjectSummaryStatus.PRESENT.name, 'else': DesktopObjectSummaryStatus.DELETED.name}}
        }}]
    if begin:
        pipeline.append({'$skip': begin})
    if end:
        pipeline.append({'$limit': end - begin})
    if sort_order_dict:
        pipeline.append({'$sort': sort_order_dict})

    return await mongoservicelib.aggregate(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION, pipeline)


@routes.get('/desktopobjectactions/{id}')
@action('heaserver-activity-desktopobjectaction-get-properties', rel='hea-properties')
@action('heaserver-activity-desktopobjectaction-get-old-object-uri', rel='hea-desktop-object', itemif='old_object_uri is not None and new_object_uri is None', path='{+old_object_uri}')
@action('heaserver-activity-desktopobjectaction-get-new-object-uri', rel='hea-desktop-object', itemif='new_object_uri is not None', path='{+new_object_uri}')
@action('heaserver-activity-desktopobjectaction-get-self', rel='self', path='desktopobjectactions/{id}')
async def get_desktop_object_action(request: web.Request) -> web.Response:
    """
    Gets the desktop object action with the specified id.
    :param request: the HTTP request.
    :return: the requested desktop object action or Not Found.
    ---
    summary: A specific desktop object action.
    tags:
        - heaserver-activity
    parameters:
        - $ref: '#/components/parameters/id'
        - $ref: '#/components/parameters/OIDC_CLAIM_sub'
    responses:
      '200':
        $ref: '#/components/responses/200'
      '404':
        $ref: '#/components/responses/404'
    """
    return await mongoservicelib.get(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION)


@routes.get('/desktopobjectactions/byname/{name}')
@action('heaserver-activity-desktopobjectaction-get-self', rel='self', path='desktopobjectactions/{id}')
async def get_desktop_object_action_by_name(request: web.Request) -> web.Response:
    """
    Gets the desktop object action with the specified name.
    :param request: the HTTP request.
    :return: the requested desktop object action or Not Found.
    ---
    summary: A specific desktop object action, by name.
    tags:
        - heaserver-activity
    parameters:
        - $ref: '#/components/parameters/name'
        - $ref: '#/components/parameters/OIDC_CLAIM_sub'
    responses:
      '200':
        $ref: '#/components/responses/200'
      '404':
        $ref: '#/components/responses/404'
    """
    return await mongoservicelib.get_by_name(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION)


@routes.get('/desktopobjectactions')
@routes.get('/desktopobjectactions/')
@action('heaserver-activity-desktopobjectaction-get-properties', rel='hea-properties')
@action('heaserver-activity-desktopobjectaction-get-old-object-uri', rel='hea-desktop-object', itemif='old_object_uri is not None and new_object_uri is None', path='{+old_object_uri}')
@action('heaserver-activity-desktopobjectaction-get-new-object-uri', rel='hea-desktop-object', itemif='new_object_uri is not None', path='{+new_object_uri}')
@action('heaserver-activity-desktopobjectaction-get-self', rel='self', path='desktopobjectactions/{id}')
async def get_all_desktop_object_actions(request: web.Request) -> web.Response:
    """
    Gets all desktop object actions. The Authorization header is required if you pass in organization_id query parameters.

    :param request: the HTTP request.
    :return: all desktop object actions.
    ---
    summary: All desktop object actions.
    tags:
        - heaserver-activity
    parameters:
        - $ref: '#/components/parameters/OIDC_CLAIM_sub'
        - $ref: '#/components/parameters/Authorization'
        - name: sort
          in: query
          description: Sort order for components, in the same order as the sort_attr parameter.
          schema:
            type: array
            items:
              type: string
              enum: [asc, desc]
          examples:
            example:
              summary: Sort in ascending order.
              value: asc
        - name: sort_attr
          in: query
          description: Attributes to sort by, in the same order as as the sort parameter. If not specified, defaults to sorting by display_name.
          schema:
            type: array
            items:
              type: string
          examples:
            example:
              summary: Sort by display name.
              value: display_name
        - name: organization_id
          in: query
          description: The organization IDs of the items to return. Omitting this parameter will return items with any organization ID.
          schema:
            type: array
            items:
              type: string
          examples:
            example:
              summary: the organization ids of the items to return
              value: ['1234567890abcdef']
    responses:
      '200':
        $ref: '#/components/responses/200'
    """
    sub = request.headers.get(SUB, NONE_USER)
    headers = {SUB: sub}
    if hdrs.AUTHORIZATION in request.headers:
        headers[hdrs.AUTHORIZATION] = request.headers[hdrs.AUTHORIZATION]
    sort = mongoservicelib.MongoSortOrder.from_request_dict_raises_http_error(request, 'status_updated')
    exclude_codes = request.query.getall('excludecode', None)
    organization_ids = request.query.getall('organization_id', None)
    volume_ids: list[str] = []
    if organization_ids:
        org_volumes_url = await client.get_resource_url(request.app, Organization)
        assert org_volumes_url is not None, 'No organization resource URL found'
        for org_id in organization_ids:
            async for volume in client.get_all(request.app, URL(org_volumes_url) / org_id / 'volumes', Volume, headers=headers):
                assert volume.id is not None, f'Volume {volume} has no id'
                volume_ids.append(volume.id)
    mongoattrs = {}
    if exclude_codes:
        mongoattrs.update({'code': {'$nin': exclude_codes}})
    if volume_ids:
        mongoattrs.update({'new_volume_id': {'$in': volume_ids}})

    return await mongoservicelib.get_all(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION, mongoattributes=mongoattrs,
                                         sort=sort if sort else None)


@routes.get('/desktopobjectactionsupdatelistener')
async def websocket_handler(request: web.Request):
    logger = logging.getLogger(__name__)
    count = request.query.get('count', None)
    if count is not None:
        try:
            count_ = int(count)
        except TypeError as e:
            return response.status_bad_request(str(e))
        if count_ < 0:
            return response.status_bad_request(f'count must be non-negative but was {count_}')

    exclude_codes = request.query.getall('excludecode', [])

    wsresponse = web.WebSocketResponse()
    await wsresponse.prepare(request)
    try:
        _wsresponses[wsresponse] = (request, exclude_codes)
        while True:
            if wsresponse.closed:
                logger.debug('Web socket response is closed!')
                break
            try:
                msg = await wsresponse.receive(timeout=5)
                if msg.type == WSMsgType.TEXT and msg.data == '__ping__':
                    await wsresponse.send_str('__pong__')
            except AsyncioTimeoutError:  # TODO: change to built-in TimeoutError in Python 3.11.
                pass
    finally:
        logger.debug('Cleaning up')
        try:
            if not wsresponse.closed:
                await wsresponse.close()
        finally:
            if wsresponse in _wsresponses:
                del _wsresponses[wsresponse]
    return wsresponse


async def desktop_object_action_cb(app: web.Application, desktop_object: DesktopObject):
    """
    Callback for desktop object actions received from the message broker. Actions may be received by this callback
    repeatedly, possibly with an updated status. This callback cannot assume that it will receive an updated action
    reflecting REQUESTED and IN_PROGRESS status changes, though all status transitions will be reflected in the
    action's attributes. It can assume that it will receive an updated action reflecting the action's final status,
    SUCCEEDED or FAILED.

    :param app: the aiohttp Application (required).
    :param desktop_object: the DesktopObject (required). The desktop object must be a
    heaobject.activity.DesktopObjectAction and have a non-None application_id attribute.
    :raises ValueError: if the desktop object action could not be inserted into MongoDB.
    """
    logger = logging.getLogger(__name__)
    if not isinstance(desktop_object, DesktopObjectAction):
        raise TypeError(f'Unexpected desktop object type {type(desktop_object)}')
    if desktop_object.application_id is None:
        raise ValueError(f'No application id in desktop object action {desktop_object}')
    logger.debug('Saving desktop object action %r', desktop_object)
    id_ = await app[appproperty.HEA_DB].upsert_admin(desktop_object, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION,
                                                     mongoattributes={'application_id': desktop_object.application_id} \
                                                        if desktop_object.id is None else None)
    if id_ is None:
        raise ValueError(f'Failed to upsert desktop object action {desktop_object}')
    desktop_object.id = id_
    logger.debug('Desktop object saved: %r', desktop_object)

    desktop_object_dict = desktop_object.to_dict()
    if _wsresponses:
        logger.debug('Sending update %s', desktop_object_dict)
        gather_responses = []
        for wsresponse, (request, excludecodes) in _wsresponses.items():
            if desktop_object.code in excludecodes:
                continue
            sub = wsresponse.headers.get(SUB, NONE_USER)
            context = PermissionContext(sub)
            if await desktop_object.has_permissions(RESTPermissionGroup.GETTER_PERMS, context):
                logger.debug('Sending update to %s', wsresponse)
                if not wsresponse.closed:
                    logger.debug('%s is not closed', wsresponse)
                    async def wrapper():
                        try:
                            logger.debug('Sending desktop object %s', desktop_object_dict)
                            msg, mime_type = await _to_format(request, desktop_object_dict)
                            logger.debug('Converted to %s: %s', mime_type, msg)
                            await wsresponse.send_str(msg)
                        except ConnectionResetError as e:
                            # There is not much we can do if the connection is reset, and this exception might be raised because the
                            # connection is closing, for example, because the client tried closing the connection.
                            logger.exception(f'Ignoring {desktop_object_dict}')
                    gather_responses.append(wrapper())
        logger.debug('Awaiting sending to finish')
        await gather(*gather_responses)
        logger.debug('Sending all done')



def main() -> None:
    config = init_cmd_line(description='A service for tracking activity in hea',
                           default_port=8080)
    start(db=mongo.MongoManager, wstl_builder_factory=builder_factory(__package__), config=config,
          cleanup_ctx=[subscriber_cleanup_context_factory(message_body_cb=desktop_object_action_cb, config=config,
                                                          topics=[DesktopObjectAction.get_type_name()])])

async def _to_format(request: web.Request, data: DesktopObjectDict) -> tuple[str, str]:
    wstl_builder = request[requestproperty.HEA_WSTL_BUILDER]
    wstl_builder.href = str(request.url)
    msg, mime_type = await formats_from_request(request, wstl_builder(data))
    return msg.decode('utf-8'), mime_type
