'''
Model for the client's jobs.
There are functions to manipulate them and to send them.
'''

from __future__ import unicode_literals
import db
import json
from db import Base
from urllib import urlencode
from sqlalchemy import Integer, Unicode, Column, DateTime, ForeignKey
from sqlalchemy.orm import relation, backref
from sqlalchemy.util import classproperty
from utils import import_module, adquire_thread_orm
import datetime
from threading import Thread, current_thread
import config
import time
from main.model import Computer
from user.model import User
from secure_connection import SecureConnection
from socket import error as SocketError
from ssl import SSLError
from decorators import i18n
import web

metadata = Base.metadata

'''
Specifies how much time a job can be in 'WAITING' status before it's set to 'ERROR'
timed out.
'''
JOB_TIMEOUT = 60 # seconds

class ComputerNotFoundError(Exception):
    '''
    Raise when you want to send a job to a computer that it's not
    in the data base
    '''
    pass

class JobMixin(object):
  @classproperty
  def __mapper_args__(cls):
      if cls.__name__ == "Job":
          return {"polymorphic_on":cls.class_name, "polymorphic_identity":"Job", "with_polymorphic":"*"}
      else:
          return {"polymorphic_identity":unicode(cls.__name__)}

class Job(JobMixin, Base):
    '''
    An instance of this class is created when a plugin (or the core) sends a
    job to a client. The client will reply to the job with a new status and
    with a message. Developers can create custom jobs inheriting from
    "BaseJob", and for example receive a callback in the BaseJob.update()
    reimplemented function everytime their jobs get an update from the client.

    * 'id' is the primary key.

    * 'class_name' stores the name of the Job class. This is used for making
      Job polymorphic.

    * 'request' is the command sent to the client.

    * 'status' enumeration:
        'CREATED'
            Default status when a Job is inserted in the database.
        'WAITING'
            Set when the client is not ready to receive the job because it
            is not online and/or it's not running the client side daemon.
        'SENT'
            Set once the job has been successfully sent to the client.

            If a Job stays for more than JOB_TIMEOUT time in this status, then
            it's status is changed to 'ERROR' timeout.
        'ERROR',
            Set whenever an error happens. The message field contains information
            about the exact error. For example, if there is any type
            of SSL negotiation error, message is set to 'SSLError'. Or if the
            requested operation causes an error in the client, the client sends
            an update request to the server daemon and the server daemon sets
            the status to 'ERROR' and message to 'ClientError:<error info sent
            by theclient>'.
        'INPROGRESS'
            Set when the client updates the job status (sending an update
            request to the server daemon) and it has not finished the task yet.
            This way the client can continously ping us back sending
            information (for example a percentage indicating how much of the
            task has been done) when an operation takes a long time.
        'CANCELLED',
            Set when the user cancels an operation. NOTE that this is not an
            error, because the user requested it.
        'FINISHED'
            The job finished successfully.

    * 'client_message' is usually the string returned by the client. however, in the
    case of some errors, in order to be able to store more information about it, we
    use a json object. this json object will always contain an element called "response",
    which will be a string with the string returned by the client.

    * 'data' is a custom data field that can be used by custom jobs to serialize data.

    * 'creation_date' is the date when the job was sent.

    * 'last_modified_date' is the date of the last response of the client.

    * 'computer' which has assigned the job.

    * 'user' is who orders the job. It could be None. For example if the
        plugins do jobs automatically.
    '''
    __tablename__ = 'job'
    __table_args__ = {'useexisting': True}
    class_name = Column(Unicode(128), nullable=False)

    id = Column(Integer, primary_key=True)
    request = Column(Unicode())
    status = Column(Unicode(128))
    client_message = Column(Unicode())
    creation_date = Column(DateTime())
    last_modified_date = Column(DateTime())
    progress = Column(Integer)

    computer_id = Column(Integer, ForeignKey('computer.id'))
    computer = relation(Computer, backref=backref('jobs', order_by=id))

    user_id = Column(Integer, ForeignKey('user.id'))
    user = relation(User, backref=backref('jobs', order_by=id))

    '''
    Jobs can form a hierarchy. This is convenient for example if we want to execute a job
    in an heterogeneous group of computers, or maybe in all the computers inside an
    organizative unit.
    '''
    parent_id = Column(Integer, ForeignKey('job.id'))
    children = relation("Job",  backref=backref('parent', remote_side=id))

    def __init__(self, request="", args=None, files=None,
                 type='command', sequential=False):
        '''
        Creates a new job.

        * request: command to execute
        * args: python list of command arguments, arguments that
                starts with $ refers to files, $0 is the first file.
        * files: python list with filenames, "files are sended to the
                 client"
        * type: org|command
        * sequential: True|False, if this jobs should be in sequential
                      stack or not
        '''

        filelist = []
        files = files if files else []
        for f in files:
            fo = open(f)
            filelist.append(fo.read())
            fo.close()

        self.request = {'command': request,
                        'args': args if args else [],
                        'files': filelist,
                        'sequential': sequential,
                        'type': type}
        self.request = unicode(json.dumps(self.request))
        self.user = None
        self.status = 'CREATED'

    def send(self, computer=None):
        '''
        Adds the job to the data base. The job will be sent when it
        would be possible. The computer's id is necessary.
        This is because it's usually to send the same command to many
        computers.

        If the computer isn't in the db ComputerNotFoundError exception
        will be raised.
        '''
        self.creation_date = datetime.datetime.now()
        self.last_modified_date = datetime.datetime.now()
        if computer:
            self.computer = computer

        if not self.computer:
            if not self.children:
                # Nope, it's not a group job, it's just a job with no associated computer,
                # so we cannot send it (this can happen with reserved jobs for example)
                raise ComputerNotFoundError()
            else:
                # send all subjobs
                for subjob in self.children:
                    subjob.send()
                return self.id

        if self.computer.online:
            # If the computer is turned on
            # we will try to send the command
            self.status = 'WAITING'
            adquire_thread_orm().commit()
            thread = SendRequestThread(self.id)
            thread.start()
        else:
            self.status = 'WAITING'
            adquire_thread_orm().commit()

        return self.id

    def update_response(self, new_status, new_client_message, new_progress=None):
        '''
        This function is called when status needs an update.
        The message, the status and the modified date are updated, for
        both this job and its parent job if any.

        For convenience, if new_status == None then this function does
        nothing.
        '''
        if not new_status:
            return

        self.last_modified_date = datetime.datetime.now()
        print new_client_message
        self.client_message = new_client_message
        self.status = new_status
        if new_progress:
            self.progress = new_progress

        # The computer which sent this is obviosly online now
        if self.computer:
            self.computer.online = True

        def update_parent(job):
            '''
            Updates parent job (if any) status and call to update() accordingly.

            Possible scenearios:
            * This subjobs status is 'ERROR': then the parent status will be 'ERROR' too.
            * All subjobs have are finished or inprogress: then parent is in progress.
            * All subjobs have are finished or waiting: then parent is waiting.
            * Subjobs mix waiting and inprogress status: then parent is inprogress.
            '''
            if not job.parent:
                return False
            list_type = []
            parent = job.parent
            orm = adquire_thread_orm()

            parent.last_modified_date = datetime.datetime.now()
            prev_status = parent.status
            for subjob in parent.children:
                status = subjob.status
                if status == 'ERROR':
                    parent.status = 'ERROR'

                    if prev_status != parent.status:
                        orm.commit()
                        parent.update()
                        return
                elif not subjob.status in list_type:
                    list_type += [subjob.status]
            count = len(list_type)
            if count == 1:
                parent.status = list_type[0]
            else:
                parent.status = "INPROGRESS"

            if prev_status != parent.status:
                orm.commit()
                parent.update()
                return

        orm = adquire_thread_orm()

        orm.commit()
        ret = self.update()
        if new_progress:
            self.update_progress()

        update_parent(self)
        return ret

    def json_client_message(self):
        try:
            return json.loads(self.client_message)
        except Exception, e:
            return None

    def update_progress(self):
        '''
        Called when the job receives a progress response from the client.
        self.client_message will contain the messsage that came along the 
        client response.
        '''
        pass

    def update(self):
        '''
        Called when a response or an updates comes from the client.
        '''
        pass

    @i18n
    def get_user_message(self):
        '''
        Returns an user friendly string giving information about the job. Reimplement if
        needed.
        '''
        json_client_message = self.json_client_message()
        if not json_client_message:
            # then it's a string
            return self.client_message
        elif json_client_message['type'] == 'HttpError':
            return _('Error connecting with the client, invalid http response')
        elif json_client_message['type'] == 'ConnectionRefusedError':
            return _('Client was not reacheable, probably offline or shutdown')
        elif json_client_message['type'] == 'SSLError':
            return _('Error connecting with the client, connection security problem')
        else:
            return None

    def command_str(self):
        c = json.loads(self.request)
        return "%s %s" % (c['command'], ' '.join(c['args']))


class SendRequestThread(Thread):
    '''
    Sends a request
    '''
    job_id = None
    def __init__(self, job_id):
        Thread.__init__(self)
        self.job_id = job_id

    def run(self):
        '''
        It will be called by job.send() and by the watched daemon.
        Given job's id sends the request to the client and manages the
        database in a convenient way.
        '''
        # we're in a different, thread, so we need our own orm (sqlalchemy
        # can only be used inside the thread they were created)
        orm = adquire_thread_orm()
        job = orm.query(Job).filter(Job.id == self.job_id).first()
        print  "send request", job.computer.ip, job.computer.get_port(),\
            job.id, job.request, job.status

        connection = SecureConnection(job.computer.ip, job.computer.get_port())
        response = None
        response_data = None
        try:
            url = '/jobrequest/%d' % job.id
            post_data = urlencode({'request': job.request})
            headers = {"Content-type": "application/x-www-form-urlencoded",
                "Accept": "text/plain"}
            connection.request('POST', url, post_data, headers)
            response = connection.getresponse()
            response_data = response.read()
            print "requested", url, response.status, response_data
            if response.status == 200:
                status, separator, message = response_data.partition(';')
                job.update_response(status, message)
            else:
                exception = {
                    'type'      : 'HttpError',
                    'response'  : response_data,
                    'status'    : response.status
                }
                job.update_response(new_status='ERROR',
                    new_client_message=unicode(json.dumps(exception)))
        except SocketError, e:
            new_status = 'WAITING'
            if response:
                new_status = response.status
            exception = {
                'type'      : 'ConnectionRefusedError',
                'response'  : response_data,
                'status'    : job.status
            }
            job.update_response(new_status=new_status,
                    new_client_message=unicode(json.dumps(exception)))
        except SSLError, e:
            exception = {
                'type'              : 'SSLError',
                'response'          : None,
                'errno'             : e.errno,
                'strerror'          : e.strerror,
                'filename'          : e.filename,
                'previous_status'   : job.status
            }
            job.update_response(new_status='ERROR',
                    new_client_message=unicode(json.dumps(exception)))

        connection.close()
