import threading
import json
from datetime import timedelta, datetime
from typing import Tuple, TypeVar

from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models import SET_NULL
from django.utils.timezone import now as django_now
from django.utils.translation import ugettext_lazy
from django.db.models.deletion import PROTECT

# import a definition from a module at runtime
from django_workflow.utils import import_from, import_from_path

T = TypeVar('T', bound=models.Model)


def clone(old_object: T, **defaults) -> (T, T):

    id = old_object.id
    new_object = old_object
    new_object.id = None
    for key, value in defaults.items():
        setattr(new_object, key, value)
    new_object.save()
    old_object = old_object._meta.model.objects.get(id=id)
    return new_object, old_object


class WorkflowManager(models.Manager):
    def get_by_natural_key(self, name):
        return self.get(name=name)


class Workflow(models.Model):
    objects = WorkflowManager()

    name = models.CharField(max_length=200, unique=True, verbose_name=ugettext_lazy("Name"))
    object_type = models.CharField(max_length=200, verbose_name=ugettext_lazy("Object_Type"))
    initial_prefetch = models.CharField(max_length=4000, null=True, blank=True,
        verbose_name=ugettext_lazy("Object_Type"))

    @property
    def initial_prefetch_dict(self):
        dict = json.loads(self.initial_prefetch) if self.initial_prefetch else None
        for k, v in dict.items():
            if "today" in v:
                sum_operands = [x.strip() for x in v.split("+")]
                res = None
                for sum_op in sum_operands:
                    sub_operands = [x.strip() for x in sum_op.split("-")]
                    sub_res = None
                    for sub_op in sub_operands:
                        mul_operands = [x.strip() for x in sub_op.split("*")]
                        mul_res = None
                        for mul_op in mul_operands:
                            div_res = None
                            div_operands = [x.strip() for x in mul_op.split("/")]
                            for op in div_operands:
                                if div_res is None:
                                    div_res = datetime.now().date() if op == "today" else float(op)
                                else:
                                    div_res = div_res / float(op)  # you cannot divide by today
                            if mul_res is None:
                                mul_res = div_res
                            else:
                                mul_res = mul_res * div_res
                        # perform multiplications and divisions in float, then convert to timedelta for the rest
                        if isinstance(mul_res, float):
                            mul_res = timedelta(seconds=mul_res)
                        if sub_res is None:
                            sub_res = mul_res
                        else:
                            sub_res = sub_res - mul_res
                    if res is None:
                        res = sub_res
                    else:
                        res = res + sub_res
                res = res.strftime("%Y-%m-%dT%H:%M:%SZ")
                dict.update({k: res})
        return dict

    @property
    def initial_state(self):
        return State.objects.get(workflow=self, initial=True)

    @property
    def initial_transition(self):
        return Transition.objects.filter(workflow=self, initial_state=None, final_state=self.initial_state).first()

    def prefetch_initial_objects(self):
        objects = self \
            .object_class().objects.filter(**self.initial_prefetch_dict) \
            .exclude(id__in=CurrentObjectState.objects.filter(workflow=self).values_list("object_id",
            flat=True)) if self.initial_prefetch else []
        return objects

    def __unicode__(self):
        return self.name

    def is_initial_transition_available(self, *, user, object_id, object_state_id=None, automatic=False):
        if object_state_id:
            return not CurrentObjectState.objects.filter(id=object_state_id, workflow=self).exists()
        else:
            last = CurrentObjectState.objects.filter(object_id=object_id, workflow=self).order_by('-id').first()
            if last and last.state.active:
                return False
            elif self.initial_transition is not None:
                conditions = self.initial_transition.condition_set.all()
                if len(conditions) == 0:
                    if automatic:
                        return self.initial_transition.automatic
                    else:
                        return not self.initial_transition.automatic
                else:
                    root_condition = conditions.first()
                    if root_condition.check_condition(user=user, object_id=object_id, object_state=last):
                        if automatic:
                            return self.initial_transition.automatic
                        else:
                            return not self.initial_transition.automatic
                    else:
                        return False
            else:
                return False

    def natural_key(self):
        return (self.name,)

    def add_object(self, object_id, execute_async=True):
        process = CurrentObjectState.objects.create(object_id=object_id, state=self.initial_state)
        _execute_atomatic_transitions(self.initial_state, object_id, process.id, execute_async=execute_async)
        return process

    def object_class(self):
        return import_from_path(self.object_type)

    def transition_by_name(self, name):
        transition = Transition.objects.filter(workflow=self, name__iexact=name)
        if len(transition) > 0:
            return transition.first()
        return None

    def clone(self, *, name, **defaults) -> ('Workflow', 'Workflow', dict, dict):
        state_map = dict()
        transition_map = dict()
        new_wf, old_wf = clone(self, name=name, **defaults)
        for state in old_wf.state_set.all():
            new_state, old_state = state.clone(workflow=new_wf)
            state_map.update({old_state.id: new_state.id})
        for transition in old_wf.transition_set.all():
            new_transition, old_transition = transition.clone(workflow=new_wf, state_map=state_map)
            transition_map.update({old_transition.id: new_transition.id})

        return new_wf, old_wf, state_map, transition_map


class StateManager(models.Manager):
    def get_by_natural_key(self, name, workflow):
        return self.get(name=name, workflow__name=workflow)


class State(models.Model):
    objects = StateManager()

    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"))
    name = models.CharField(max_length=200, verbose_name=ugettext_lazy("Name"))
    active = models.BooleanField(verbose_name=ugettext_lazy("Active"))
    initial = models.BooleanField(default=False, verbose_name=ugettext_lazy("Initial"))

    class Meta:
        unique_together = (('name', 'workflow'),)

    def __unicode__(self):
        return "{}: {}".format(self.workflow.name, self.name)

    @property
    def is_final_state(self):
        return self.outgoing_transitions.count() == 0

    def natural_key(self):
        return (self.name, self.workflow.name)

    def available_transitions(self, user, object_id, automatic=False):
        return [t for t in self.outgoing_transitions.all() if
            t.is_available(user=user, object_id=object_id, automatic=automatic)]

    def clone(self, *, workflow: Workflow, **defaults) -> ('State', 'State'):
        new_state, old_state = clone(self, workflow=workflow, **defaults)
        for variableDef in old_state.variable_definitions.all():
            new_var, _ = clone(variableDef, state=new_state, workflow=workflow, **defaults)
        return new_state, old_state


class StateVariableDefManager(models.Manager):

    def get_by_natural_key(self, name, workflow):
        return self.get(name=name, workflow__name=workflow)

class StateVariableDef(models.Model):
    objects = StateVariableDefManager()
    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"))
    state = models.ForeignKey(State, on_delete=PROTECT, verbose_name=ugettext_lazy("State"),
        related_name="variable_definitions")
    name = models.CharField(max_length=100, verbose_name=ugettext_lazy("Name"))

    class Meta:
        unique_together = (('name', 'workflow', 'state'),)

    def __unicode__(self):
        return "{}: {} - {}".format(self.workflow.name, self.state.name, self.name)

    def natural_key(self):
        return self.name, self.state.name, self.workflow.name

class TransitionManager(models.Manager):

    def get_by_natural_key(self, name, workflow, initial_state, final_state):
        return self.get(
            name=name,
            workflow__name=workflow,
            initial_state__name=initial_state,
            final_state__name=final_state
        )


class Transition(models.Model):
    objects = TransitionManager()

    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    name = models.CharField(max_length=50, verbose_name=ugettext_lazy("Name"))
    label = models.CharField(max_length=50, verbose_name=ugettext_lazy("Label"), null=True, blank=True)
    description = models.CharField(max_length=400, null=True, blank=True, verbose_name=ugettext_lazy("Description"))
    initial_state = models.ForeignKey(State, on_delete=SET_NULL, null=True, blank=True,
        verbose_name=ugettext_lazy("Initial State"),
        related_name="outgoing_transitions")
    final_state = models.ForeignKey(State, on_delete=PROTECT, verbose_name=ugettext_lazy("Final State"),
        related_name="incoming_transitions")
    priority = models.IntegerField(null=True, blank=True, verbose_name=ugettext_lazy("Priority"))
    automatic = models.BooleanField(verbose_name=ugettext_lazy("Automatic"))
    automatic_delay = models.FloatField(null=True, blank=True,
        verbose_name=ugettext_lazy("Automatic Delay in Days"))

    class Meta:
        ordering = ["priority"]
        unique_together = (('name', 'final_state'),)

    @property
    def is_initial(self):
        return self.initial_state is None

    def __unicode__(self):
        return "{}{}: {} to {}".format(self.final_state.workflow.name, "(" + self.name + ")" if self.name else "",
            self.initial_state.name if self.initial_state else "START", self.final_state.name)

    def natural_key(self):
        return (self.name, self.workflow.name, self.initial_state.name if self.initial_state else None,
        self.final_state.name)

    def save(self, **qwargs):