"""
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
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
from heaobject.root import DesktopObject, DesktopObjectDict
from heaobject.user import NONE_USER
from aiohttp import WSMsgType
from asyncio import gather, TimeoutError as AsyncioTimeoutError
from datetime import datetime
import logging

MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION = 'desktopobjectactions'

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

@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/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 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:
        - 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
    responses:
      '200':
        $ref: '#/components/responses/200'
      '404':
        $ref: '#/components/responses/404'
    """
    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
    volume_ids = request.query.getall('volume_id', [])
    sub = request.headers.get(SUB, NONE_USER)
    type_name = request.match_info['type']
    logger.debug('Requested recent accesses')
    if volume_ids:
        volume_id_query = {'$or': [{'new_volume_id': {'$exists': False}}, {'new_volume_id': {'$in': volume_ids + [None]}}]}
    else:
        volume_id_query = {}

    pipeline = [{ "$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,
                        {'$or': [{'new_object_uri': {'$ne': None}}, {'old_object_uri': {'$ne': None}}]},
                        {'new_object_id': {'$ne': None}}
                        ]}},
        { '$sort': {'status_updated': -1}},
        { '$facet': {
            "first": [
                { '$group': {'_id': {'$cond': [{'$ne': ["$new_object_uri", None]}, "$new_object_uri", None]}, 'obj': {'$first': "$$ROOT"}}},
            ],
            "second": [
                { '$group': {'_id': {'$cond': [{'$ne': ["$old_object_uri", None]}, "$old_object_uri", None]}, 'obj': {'$first': "$$ROOT"}}},
            ]
        }},
        { '$project': {
            'mergeArray': {
                '$concatArrays': ["$first", "$second"]
            }
        }},
        { '$unwind': "$mergeArray"},
        { '$match': {"mergeArray._id": {'$ne': None}}},
        { '$sort': {"mergeArray.obj.status_updated": -1}},
        { '$group': {'_id': "$mergeArray._id", 'first': {'$first': "$$ROOT"}}},
        { '$project': {
            '_id': 1,
            'obj': "$first.mergeArray.obj"
        }},
        { '$match': {'$expr': {'$eq': ["$obj.new_object_uri", "$_id"]}}},
        { '$project': {
            '_id': 0,
            '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",
        }},
        { '$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('/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'
    """
    sort = request.query.get('sort', None)
    if sort is None:
        sort_int = None
    else:
        if sort != 'asc' and sort != 'desc':
            return await response.status_bad_request(f'sort may be asc or desc but was {sort}')
        sort_int = 1 if sort == 'asc' else -1

    return await mongoservicelib.get_all(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION, sort={'status_updated': sort_int} if sort_int is not None else None)


@routes.get('/desktopobjectactionsupdatelistener')
async def websocket_handler(request: web.Request) -> web.Response:
    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_}')
    else:
        count_ = None

    wsresponse = web.WebSocketResponse()
    await wsresponse.prepare(request)
    try:
        # if count_ is None or count_:
        #     async for index, data in enumerate(mongoservicelib.get_all_gen(request, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION)):
        #         logger.debug('Sending %s', data)
        #         msg, mime_type = await _to_format(request, data)
        #         logger.debug('Converted to %s: %s', mime_type, msg)
        #         await wsresponse.send_str(msg)
        #         if count_ is not None and index >= count_ - 1:
        #             break
        _wsresponses[wsresponse] = request
        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]


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)
    saved_object = await app[appproperty.HEA_DB].get_admin(MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION, mongoattributes={
        'application_id': desktop_object.application_id
    })
    if saved_object is not None:
        desktop_object.id = saved_object['id']
        if desktop_object.modified is None:
            desktop_object.modified = datetime.now()
        if await app[appproperty.HEA_DB].update_admin(desktop_object, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION) is None:
            raise ValueError(f'Failed to update desktop object action {desktop_object}')
    else:
        if desktop_object.created is None:
            desktop_object.created = datetime.now()
        if (result := await app[appproperty.HEA_DB].insert_admin(desktop_object, MONGODB_DESKTOP_OBJECT_ACTION_COLLECTION)) is None:
            raise ValueError(f'Failed to insert desktop object action {desktop_object}')
        desktop_object.id = result
    logger.debug('Desktop object saved: %s', desktop_object)

    desktop_object_dict = desktop_object.to_dict()
    if _wsresponses:
        logger.debug('Sending update %s', desktop_object_dict)
        gather_responses = []
        for wsresponse, request in _wsresponses.items():
            sub = wsresponse.headers.get(SUB, NONE_USER)
            if desktop_object.has_permissions(sub, RESTPermissionGroup.GETTER_PERMS):
                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
