from flask import session
from flask_login import current_user
from flask_socketio import ConnectionRefusedError
from threading import Lock
import cqi
from app import db, hashids, socketio
from app.decorators import socketio_login_required
from app.models import Corpus, CorpusStatus


'''
This package tunnels the Corpus Query interface (CQi) protocol through
Socket.IO (SIO) by wrapping each CQi function in a seperate SIO event.

This module only handles the SIO connect/disconnect, which handles the setup
and teardown of necessary ressources for later use. Each CQi function has a
corresponding SIO event. The event handlers are spread across the different
modules within this package.

Basic concept:
1. A client connects to the SIO namespace and provides the id of a corpus to be
   analysed.
     1.1 The analysis session counter of the corpus is incremented.
     1.2 A CQiClient and a (Mutex) Lock belonging to it is created.
     1.3 Wait until the CQP server is running.
     1.4 Connect the CQiClient to the server.
     1.5 Save the CQiClient and the Lock in the session for subsequential use.
2. A client emits an event and may provide a single json object with necessary
   arguments for the targeted CQi function.
3. A SIO event handler (decorated with cqi_over_socketio) gets executed.
     - The event handler function defines all arguments. Hence the client
       is sent as a single json object, the decorator decomposes it to fit
       the functions signature. This also includes type checking and proper
       use of the lock (acquire/release) mechanism.
4. Wait for more events
5. The client disconnects from the SIO namespace
     1.1 The analysis session counter of the corpus is decremented.
     1.2 The CQiClient and (Mutex) Lock belonging to it are teared down.
'''


NAMESPACE = '/corpora/corpus/corpus_analysis'


# Import all CQi over Socket.IO event handlers
from .cqi_corpora_corpus_subcorpora import *  # noqa
from .cqi_corpora_corpus_structural_attributes import *  # noqa
from .cqi_corpora_corpus_positional_attributes import *  # noqa
from .cqi_corpora_corpus_alignment_attributes import *  # noqa
from .cqi_corpora_corpus import *  # noqa
from .cqi_corpora import *  # noqa
from .cqi import *  # noqa


@socketio.on('connect', namespace=NAMESPACE)
@socketio_login_required
def connect(auth):
    # the auth variable is used in a hacky way. It contains the corpus id for
    # which a corpus analysis session should be started.
    corpus_id = hashids.decode(auth['corpus_id'])
    corpus = Corpus.query.get(corpus_id)
    if corpus is None:
        # return {'code': 404, 'msg': 'Not Found'}
        raise ConnectionRefusedError('Not Found')
    if not (corpus.user == current_user or current_user.is_administrator()):
        # return {'code': 403, 'msg': 'Forbidden'}
        raise ConnectionRefusedError('Forbidden')
    if corpus.status not in [
        CorpusStatus.BUILT,
        CorpusStatus.STARTING_ANALYSIS_SESSION,
        CorpusStatus.RUNNING_ANALYSIS_SESSION,
        CorpusStatus.CANCELING_ANALYSIS_SESSION
    ]:
        # return {'code': 424, 'msg': 'Failed Dependency'}
        raise ConnectionRefusedError('Failed Dependency')
    if corpus.num_analysis_sessions is None:
        corpus.num_analysis_sessions = 0
        db.session.commit()
    corpus.num_analysis_sessions = Corpus.num_analysis_sessions + 1
    db.session.commit()
    retry_counter = 20
    while corpus.status != CorpusStatus.RUNNING_ANALYSIS_SESSION:
        if retry_counter == 0:
            corpus.num_analysis_sessions = Corpus.num_analysis_sessions - 1
            db.session.commit()
            return {'code': 408, 'msg': 'Request Timeout'}
        socketio.sleep(3)
        retry_counter -= 1
        db.session.refresh(corpus)
    cqi_client = cqi.CQiClient(f'cqpserver_{corpus_id}')
    session['d'] = {
        'corpus_id': corpus_id,
        'cqi_client': cqi_client,
        'cqi_client_lock': Lock(),
    }
    # return {'code': 200, 'msg': 'OK'}


@socketio.on('disconnect', namespace=NAMESPACE)
def disconnect():
    if 'd' not in session:
        return
    session['d']['cqi_client_lock'].acquire()
    try:
        session['d']['cqi_client'].disconnect()
    except (BrokenPipeError, cqi.errors.CQiException):
        pass
    session['d']['cqi_client_lock'].release()
    corpus = Corpus.query.get(session['d']['corpus_id'])
    corpus.num_analysis_sessions = Corpus.num_analysis_sessions - 1
    db.session.commit()
    session.pop('d')
    # return {'code': 200, 'msg': 'OK'}
