"""
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.
"""

import asyncio
from heaserver.service import response, requestproperty
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 import appproperty
from heaobject.activity import DesktopObjectAction
from heaobject.root import DesktopObject
from aiohttp import WSMsgType
from asyncio import gather
import logging

MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION = 'desktopobjectactions'

_wsresponses: set[web.WebSocketResponse] = set()


@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'
    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'
    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.
    :param request: the HTTP request.
    :return: all desktop object actions.
    ---
    summary: All desktop object actions.
    tags:
        - heaserver-activity
    responses:
      '200':
        $ref: '#/components/responses/200'
    """
    return await mongoservicelib.get_all(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION)


@routes.get('/desktopobjectactionsupdatelistener')
async def websocket_handler(request: web.Request) -> web.Response:
    logger = logging.getLogger(__name__)
    wsresponse = web.WebSocketResponse()
    await wsresponse.prepare(request)
    try:
        async for data in mongoservicelib.get_all_gen(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION, sort = -1):
            logger.debug('Sending %s', data)
            wstl_builder = request[requestproperty.HEA_WSTL_BUILDER]
            wstl_builder.data = [data]
            wstl_builder.href = str(request.url)
            msg, mime_type = await response.to_representor_format(request, wstl_builder())
            logger.debug('Converted to %s: %s', mime_type, msg)
            await wsresponse.send_bytes(msg)
        _wsresponses.add(wsresponse)
        while True:
            if wsresponse.closed:
                logger.debug('Web socket response is closed!')
                break
            await asyncio.sleep(1)
    finally:
        logger.debug('Cleaning up')
        try:
            if not wsresponse.closed:
                await wsresponse.close()
        finally:
            if wsresponse in _wsresponses:
                _wsresponses.remove(wsresponse)


async def desktop_object_action_cb(app: web.Application, desktop_object: DesktopObject):
    """
    Message body callback for activities received from the message broker.

    :param app: the aiohttp Application (required).
    :param desktop_object: the DesktopObject (required).
    :raises ValueError: if the activity could not be inserted into MongoDB.
    """
    logger = logging.getLogger(__name__)
    logger.debug('Saving desktop object action %s', desktop_object)
    if await app[appproperty.HEA_DB].insert(desktop_object, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION) is None:
        raise ValueError(f'Failed to insert desktop object action on {desktop_object}')
    logger.debug('Desktop object saved: %s', desktop_object)
    desktop_object_json = desktop_object.to_json()
    if _wsresponses:
        logger.debug('Sending update %s', desktop_object_json)
        gather_responses = []
        for wsresponse in _wsresponses:
            if not wsresponse.closed:
                async def wrapper():
                    try:
                        await wsresponse.send_json(desktop_object_json)
                    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_json}')
                gather_responses.append(wrapper())
        await gather(*gather_responses)



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()])])
