import logging
import os
from datetime import datetime, timedelta

from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db import connection, models, transaction
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from uwsgidecoratorsfallback import spool

from .choices import COMPLETED_TASK_STATUSUS, PENDING_TASK_STATUSES, LogType, TaskStatus
from .managers import TaskManager
from .signals import task_status_changed

logger = logging.getLogger(__name__)


class TaskLogger:
    def __init__(self, task):
        self.task = task

    def debug(self, message):
        self.task.log(message, LogType.DEBUG)

    def info(self, message):
        self.task.log(message, LogType.INFO)

    def error(self, message):
        self.task.log(message, LogType.ERROR)


@spool
def async_task_runner(kwargs):
    try:
        if not connection.is_usable():
            connection.close()
    except AttributeError:
        connection.close_if_unusable_or_obsolete()

    try:
        task = Task.objects.get(id=kwargs["task_id"])
        task = task.run()
    except ValidationError as e:
        logger.exception(e)
    except Task.DoesNotExist:
        logger.warning("Task with id: %s does not exists", kwargs["task_id"])


class Task(models.Model):
    objects = TaskManager()

    name = models.CharField(max_length=255)
    code = models.CharField(max_length=255, blank=True, null=True)
    status = models.IntegerField(choices=TaskStatus.choices, default=TaskStatus.PENDING)
    data = models.JSONField(blank=True, null=True)
    action = models.CharField(max_length=255)
    retry_after = models.DurationField(default=timedelta(seconds=0))
    is_highvolume = models.BooleanField(default=False)
    pid = models.PositiveBigIntegerField(blank=True, null=True)

    date_created = models.DateTimeField(
        _("Date Created"),
        auto_now_add=True,
        db_index=True,
    )
    date_updated = models.DateTimeField(_("Date Updated"), auto_now=True, db_index=True)

    def __init__(self, *args, **kwargs):
        self._action = None
        super().__init__(*args, **kwargs)

    def get_absolute_url(self):
        return reverse("wagtailsnippets_task_task:edit", kwargs={"pk": self.pk})

    def display_task_status(self, request):
        msg = render_to_string(
            "admin/task/display_status_message.html",
            {"task": self},
        )
        messages.warning(request, msg, extra_tags="safe noicon")

    def is_pending(self):
        return self.status == TaskStatus.PENDING

    def is_in_progress(self):
        return self.status == TaskStatus.PROGRESS

    def is_successful(self):
        return self.status == TaskStatus.SUCCESSFUL

    def is_failed(self):
        return self.status == TaskStatus.FAILED

    def is_cancelled(self):
        return self.status == TaskStatus.CANCELED

    def get_action(self):
        if self._action is None:
            action_cls = import_string(self.action)
            self._action = action_cls.deserialize(self)

        return self._action

    @classmethod
    def create(cls, action):
        # populate self._action so get_action will not trigger complicated stuff
        instance = cls(
            name=action.get_name(),
            code=action.get_code(),
            data=action.get_data(),
            action=action.get_importstring(),
            retry_after=action.get_retry_after(),
            is_highvolume=action.get_is_highvolume(),
        )
        instance._action = action
        instance.full_clean()
        instance.save()

        instance.log("Initialized task")
        return instance

    def clean(self):
        action = self.get_action()
        action.validate_task(self)
        return super().clean()

    def run_async(self):
        async_task_runner.spool(task_id=str(self.id))

    def run(self):
        if self.status in COMPLETED_TASK_STATUSUS:
            logger.warning(
                "Task with id: %s is already completed, status: %s",
                self.id,
                self.displayable_status,
            )
            return

        if self.status not in PENDING_TASK_STATUSES:
            raise ValidationError(
                f"Can't run task with status '{self.displayable_status}'."
            )

        self.set_status(TaskStatus.PROGRESS)

        try:
            action = self.get_action()
            log = TaskLogger(self)
            action_response = action.run(log)

            add_status_log = (
                action_response.get("add_status_log", True)
                if isinstance(action_response, dict)
                else True
            )
            # no exception means SUCCESSFUL
            self.set_status(TaskStatus.SUCCESSFUL, add_log=add_status_log)
            return action_response

        except Exception as e:  # pylint:disable=broad-exception-caught
            next_try = (datetime.now() + self.retry_after).strftime("%Y-%m-%d %H:%M")
            message = f"<b>Retry:</b> {next_try}<b> | Error:</b> {str(e)}"
            error_text = str(e)
            if (
                isinstance(e, ValueError)
                and "exact settings has no access_token" in error_text
            ):
                logger.warning(error_text.strip())
            else:
                logger.exception(e)

            # exception means failed
            self.set_status(TaskStatus.FAILED, message=message)

    def set_status(self, status, message=None, add_log=True):
        old_status = self.status
        with transaction.atomic():
            locked_self = (
                self.__class__.objects.filter(pk=self.pk).select_for_update().get()
            )
            locked_self.pid = os.getpid()
            locked_self.status = status
            locked_self.save()
            self.status = locked_self.status
            if add_log:
                self.log(message, log_type=status)
            # Send signal for handling status changed
            task_status_changed.send(
                sender=self,
                task=self,
                old_status=old_status,
                new_status=status,
            )

    @property
    def displayable_status(self):
        return TaskStatus.get(self.status)

    displayable_status.fget.short_description = _("Status")

    @property
    def displayable_logs(self):
        return render_to_string(
            "admin/partials/display_logs.html",
            dict(logs=self.logs.order_by("-timestamp")),
        )

    displayable_logs.fget.short_description = _("Logs")

    def retry_datetime(self):
        if self.is_failed():
            return self.date_updated + self.retry_after

    retry_datetime.short_description = _("Next attempt")

    def log(self, message, log_type=LogType.INFO):
        self.logs.create(
            log_type=log_type,
            message=message,
        )

    def __str__(self):
        return self.name

    class Meta:
        app_label = "task"
        ordering = ("-date_updated",)


class TaskLog(models.Model):
    timestamp = models.DateTimeField(auto_now=True, db_index=True)
    log_type = models.IntegerField(choices=LogType.choices, default=LogType.INFO)
    message = models.TextField(null=True, blank=True)

    task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="logs")

    @property
    def displayable_log(self):
        return LogType.get(self.log_type)

    class Meta:
        app_label = "task"
