#!/usr/bin/env python
from __future__ import unicode_literals
import web
import db
import datetime
from web.wsgiserver import CherryPyWSGIServer
from web.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
import config
from clientjob.model import Job, JOB_TIMEOUT
import json
from utils import RepeatTimer, IPConnectivityCheck, adquire_thread_orm, set_thread_orm
from threading import current_thread
from pluginmanager.pluginmanager import PluginManager

plugin_manager = None

class UpdateStatusView:
    '''
    Updates the status of a given job
    '''
    def POST(self, id, status):
        message = web.input().message
        print "Automatic Update Status for job id =", id
        job = web.ctx.orm.query(Job).filter(Job.id == id).first()
        if not job:
            print "referred to an unexistant job id: ", id
            return
        valid_statuses = ('ERROR', 'INPROGRESS', 'FINISHED')

        new_status = None
        new_client_message = None
        if not status in valid_statuses or\
            job.status == 'FINISHED' and status in ('ERROR', 'INPROGRESS'):

            new_status = 'ERROR'
            exception = {
                'type'                      : 'InvalidStatusUpdate',
                'response'                  : message,
                'status'                    : status,
                'previous_status'           : job.status,
                'previous_client_message'   : job.client_message
            }
            new_client_message = unicode(json.dumps(exception))
        elif job.status == 'ERROR' and status in ('INPROGRESS', 'FINISHED'):
            new_status = 'ERROR'
            exception = {
                'type'                      : 'InvalidStatusUpdate',
                'response'                  : message,
                'status'                    : status,
                'previous_status'           : job.status,
                'previous_client_message'   : job.client_message
            }
            new_client_message = unicode(json.dumps(exception))
        elif status == 'ERROR':
            # check if previous error was a client side error already, we'll only record
            # first clientside error
            if job.client_message and job.status:
                client_message = json.loads(job.client_message)
                if client_message.type == 'ClientSideError':
                    print "Tried to set twice a ClientSideError for a job"
                    return
            new_status = 'ERROR'
            exception = {
                'type'                      : 'ClientSideError',
                'response'                  : message,
                'previous_status'           : job.status,
                'previous_client_message'   : job.client_message
            }
            new_client_message = unicode(json.dumps(exception))
        else:
            new_status = unicode(status)
            new_client_message = unicode(message)


        return job.update_response(new_status, new_client_message)

class UpdateProgressView:
    '''
    Updates the status of a given job, setting a progress and a message.
    '''
    def POST(self, id, progress):
        message = web.input().message
        print "UpdateProgress for job id =", id
        job = web.ctx.orm.query(Job).filter(Job.id == id).first()
        if not job:
            print "referred to an unexistant job id: ", id
            return
        valid_statuses = ('SENT', 'INPROGRESS')
        if not status in valid_statuses:
            exception = {
                'type'                      : 'InvalidProgressUpdate',
                'response'                  : message,
                'progress'                  : progress,
                'previous_status'           : job.status,
                'previous_client_message'   : job.client_message
            }
            job.update_response(new_status='ERROR',
                new_client_message=unicode(json.dumps(exception)))
        else:
            job.update_response(new_status='INPROGRESS',
                new_client_message=unicode(message), new_progress=progress)
            job.last_modified_date = datetime.datetime.now()

class UpdateJobsTimer(RepeatTimer):
    def __init__(self, interval):
        RepeatTimer.__init__(self, interval, self.update_jobs)

    def update_jobs(self):
        '''
        Called every JOB_TIMEOUT/5 seconds in order to update jobs
        '''
        global plugin_manager
        plugin_manager = PluginManager()
        self.update_timedout_jobs()
        self.update_waiting_jobs()

    def update_timedout_jobs(self):
        '''
        Marks expired jobs as ERROR TimeOut. ONLY called by update_jobs()
        '''
        orm = adquire_thread_orm()
        current_time = datetime.datetime.now()
        valid_statuses = ['SENT', 'INPROGRESS']
        timedout_jobs = orm.query(Job)\
            .filter(Job.status.in_(valid_statuses))\
            .filter(Job.last_modified_date < current_time -\
                datetime.timedelta(seconds=JOB_TIMEOUT))\
            .all()
        for job in timedout_jobs:
            if job.children:
                # Group jobs do not timeout. Only if all subjobs timeout then a
                # group job status is changed to TIMEOUT (done by update_parent_status())
                continue
            print "timing out job.id = ", job.id
            # mark those jobs as timed out
            exception = {
                'type'              : 'Timeout',
                'previous_response' : job.client_message,
                'prevous_status'    : job.status
            }
            job.update_response(new_status='ERROR',
                new_client_message=unicode(json.dumps(exception)))
            # Checking in other thread if the computer is online
            IPConnectivityCheck(job.computer.ip).start()
        orm.commit()

    def update_waiting_jobs(self):
        '''
        Starts jobs waiting for a computer now online to be started.
        ONLY called by update_jobs().
        '''
        orm = adquire_thread_orm()
        current_time = datetime.datetime.now()
        waiting_jobs = orm.query(Job).filter(Job.status == 'WAITING').all()
        for job in waiting_jobs:
            if job.children:
                # Group jobs do not have a computer and thus even if they're in waiting
                # status because all of their children are, they do not need an update
                continue
            IPConnectivityCheck(job.computer.ip).start()
            if not job.computer.online:
                print "job %d related computer (ip=%s) is still not online" % (job.id,
                    job.computer.ip)
                continue
            print "job %d related computer (ip=%s) is now online=%d, sending the job" % (job.id,
                    job.computer.ip, job.computer.online)
            job.send()
        orm.commit()

def launch_server():
    '''
    Launches the server daemon which receives updates from the clients
    '''
    CherryPyWSGIServer.ssl_adapter = pyOpenSSLAdapter(config.cert_file,
        config.key_file, None, config.ca_cert)

    urls = ("/updatejob/normal/(\d+)/([A-Z]+)", "UpdateStatusView",
    "/updatejob/progress/(\d+)/(\d+)", "UpdateProgressView")
    # the following is the only way to tell web.py which port to start the server..
    app = web.application(urls, globals())
    app.add_processor(load_sqlalchemy)
    app.add_processor(load_plugins)
    server = CherryPyWSGIServer((config.server_daemon_ip, config.server_daemon_port),
        app.wsgifunc(), server_name=config.server_daemon_name)

    print "calling update_timedout_jobs every %d seconds" % (JOB_TIMEOUT/5)
    timer = UpdateJobsTimer(3)
    timer.start()

    # Last thing to do, as it is a blocking operation
    print "launching server at %s:%d" % (config.server_daemon_ip, config.server_daemon_port)
    try:
        server.start()
    except KeyboardInterrupt:
        timer.cancel()
        server.stop()

def load_sqlalchemy(handler):
    web.ctx.orm = adquire_thread_orm()
    try:
        to_ret = handler()
    except web.HTTPError:
        web.ctx.orm.commit()
        raise
    except:
        web.ctx.orm.rollback()
        raise
    web.ctx.orm.commit()
    return to_ret

def load_plugins(handler):
    global plugin_manager
    plugin_manager = PluginManager()
    return handler()

if __name__ == "__main__":
    launch_server()
