
try:
    from django.utils.encoding import force_bytes
except ImportError:
    from .utils import force_bytes

try:
    from six.moves import cPickle as pickle
except ImportError:
    import pickle

from . import TIEMPO_REGISTRY
from . import REDIS_GROUP_NAMESPACE as group_ns
from .conn import REDIS

from logging import getLogger

import inspect
import uuid
import base64
import importlib
import functools
import datetime
import traceback
import json


logger = getLogger(__name__)


def resolve_group_namespace(group_name):
    return '%s:%s'%(group_ns, group_name)

class TaskBase(object):

    @property
    def html(self):
        return '<task>%s</task>' % self.uid

    @classmethod
    def _encode(cls, dictionary):
        "Returns the given session dictionary pickled and encoded as a string."
        pickled = pickle.dumps(dictionary, pickle.HIGHEST_PROTOCOL)
        return base64.b64encode(pickled).decode('ascii')

    @staticmethod
    def _decode(data):

        encoded_data = base64.b64decode(force_bytes(data))
        try:
            # could produce ValueError if there is no ':'
            pickled = encoded_data
            dec = pickle.loads(pickled)
            # print 'pickle:', dec
            return dec

        except Exception:
            raise
            # ValueError, SuspiciousOperation, unpickling exceptions. If any of
            # these happen, just return an empty dictionary (an empty session).
            return {}

    @staticmethod
    def rehydrate(base_64):

        d = Task._decode(base_64)

        module = importlib.import_module(d['function_module_path'])
        T = getattr(module, d['function_name'])
        T.data = d
        T._thaw()
        return T


class Task(TaskBase):

    def __repr__(self):
        return self.key

    def __init__(self, *args, **kwargs):

        self.day = None
        self.hour = None
        self.minute = None
        self.periodic = False

        self.uid = str(uuid.uuid1())

        self.key = self.uid
        self.function_name = None
        self.group = unicode(kwargs.get('priority', kwargs.get('group','1')))

        self.frozen = False
        # group and other attrs may be overridden here.
        for key, val in kwargs.items():
            setattr(self, key, val)

        if args and hasattr(args[0], '__call__'):
            self.__setup(args)

    def __call__(self, *args, **kwargs):

        # we only want this to happen if this is being called
        # as a decorator, otherwise all "calls" are performed as
        # special functions ie. "now" or "soon" etc.
        if args and hasattr(args[0], '__call__'):
            self.__setup(args)

        return self

    def __setup(self, args):
        self.func = args[0]
        self.cache = {}
        functools.update_wrapper(self, self.func)
        self.key = '%s.%s' % (
            inspect.getmodule(self.func).__name__, self.func.__name__
        )
        TIEMPO_REGISTRY[self.key] = self
        return self

    @property
    def group_key(self):
        return resolve_group_namespace(self.group)

    @property
    def waitfor_key(self):
        return resolve_group_namespace(self.uid)

    def _freeze(self, *args, **kwargs):
    
        # make a fresh uid specific to this instance that will persist
        # through getting saved to the queue        
        self.uid = str(uuid.uuid1())

        self.data = {
            'function_module_path': inspect.getmodule(self._get_function()).__name__,
            'function_name': self.func.__name__,
            'args_to_function': args,
            'kwargs_to_function': kwargs,
            'schedule': self.get_schedule(),
            'uid': self.uid,
        }
        self.frozen = True

        return self.data

    def _thaw(self, data=None):
        if not data and hasattr(self, 'data'):
            data = self.data

        if data:
            for key, val in data.items():
                setattr(self, key, val)

    def _get_function(self):
        if hasattr(self, 'func'):
            return self.func

        if not self.function_name:
            self._thaw()

        if hasattr(self, 'function_module_path'):

            module = importlib.import_module(self.function_module_path)
            obj = getattr(module, self.function_name)

            if hasattr(obj, 'func'):
                return obj.func
            return obj
        else:
            print "could not find function", self.func

    def _enqueue(self, queue_name):
        if not self.frozen:
            raise ValueError(
                'need to freeze this task before enqueuing'
            )

        print "enqueueing", self
        d = self._encode(self.data)

        REDIS.rpush(queue_name, d)
        return self.data['uid']

    def _enqueue_dependents(self):

        obj = REDIS.lpop(self.waitfor_key)
        if obj:
            awaiting_task = Task.rehydrate(obj)
            #queue it
            print 'enqueing:', awaiting_task,'uid',awaiting_task.uid, 'with waitfor:', self.waitfor_key

            awaiting_task.soon(
                *getattr(awaiting_task, 'args_to_function', ()),
                **getattr(awaiting_task, 'kwargs_to_function', {})
            )

            self._enqueue_dependents()

    def get_schedule(self):
        if self.periodic:
            sched = [
                '*',
                '*',
                '*'
            ]
            for i, inc in enumerate(['day', 'hour', 'minute']):
                attr = getattr(self, inc, None)
                if attr is not None:
                    if attr != '*':
                        attr = '%02d' % int(attr)
                    sched[i] = attr

            return '.'.join(sched)

    def run(self):
        """
            runs this task right now
        """
        self.start()

        try:
            func = self._get_function()
            result = func(
                *getattr(self, 'args_to_function', ()),
                **getattr(self, 'kwargs_to_function', {})
            )
            self.finish()

            return result

        except Exception as e:
            errtext = traceback.format_exc()
            self.finish(error=errtext)

    def soon(self, *args, **kwargs):
        """
            schedules this task to be run with the args and kwargs
            whenever a worker participating in this task's groups
            comes up as available

        """
        logger.debug('scheduling task %r for next available execution', self)

        queue_name = kwargs.pop('tiempo_wait_for', None)

        if queue_name:
            queue_name = resolve_group_namespace(queue_name)
        else:
            queue_name = self.group_key

        self._freeze(*args, **kwargs)
        self._enqueue(queue_name)
        return self

    def now(self, *args, **kwargs):
        """
            runs this task NOW with the args and kwargs
        """
        self._freeze(*args, **kwargs)
        self._thaw()
        return self.run()

    def finish(self, error=None):
        print 'finished:', self.uid
        self._enqueue_dependents()

        now = datetime.datetime.now()
        data = {
            'key': self.key,
            'uid':self.uid,
            'start': self.start_time.strftime('%y/%m/%d %I:%M%p'),
            'finished': now.strftime('%y/%m/%d %I:%M%p'),
            'elapsed':(now-self.start_time).seconds,
            'errors': 'with errors: %s'%error if error else ''
        }

        data['text'] =  """
%(key)s:
started at %(start)s
finished in %(elapsed)s seconds
%(errors)s"""%data

        REDIS.set('tiempo_last_finished_%s'%self.group_key, json.dumps(data))

    def start(self, error=None):

        self.start_time = datetime.datetime.now()
        now = datetime.datetime.now()

        data = {
            'key': self.key,
            'uid':self.uid,
            'start': self.start_time.strftime('%y/%m/%d %I:%M%p'),
        }

        data['text'] =  """
%(key)s:
starting at %(start)s"""%data

        REDIS.set('tiempo_last_started_%s'%self.group_key, json.dumps(data))

task = Task
