"""
Ensure that only 1 process can be running a managment command at the same
time, even across multiple application servers.

Uses a database lock on a table row, in a separate database connection.
"""

import logging
from datetime import timedelta

from django import db
from django.core.exceptions import ObjectDoesNotExist
from django.db import models, transaction
from django.utils import timezone
from django.utils.module_loading import import_string

logger = logging.getLogger(__name__)


class CronLockManager(models.Manager):
    def lock(self, name, pid, max_age=timedelta(hours=12)):
        with transaction.atomic():
            try:
                tha_lock = (
                    CronLock.objects.filter(jobname=name)
                    .select_for_update(nowait=True)
                    .get()
                )

                if tha_lock.pid is None or tha_lock.is_expired(max_age):
                    tha_lock.pid = pid
                    tha_lock.started = timezone.now()
                    tha_lock.save()
                    return tha_lock
            except ObjectDoesNotExist:
                CronLock.objects.create(jobname=name)
                return self.lock(name, pid, max_age)
            except db.DatabaseError:
                logger.debug("Could not obtain lock for %s %s", name, pid)

        return None


class CronLock(models.Model):
    objects = CronLockManager()
    jobname = models.CharField(max_length=255, unique=True)
    started = models.DateTimeField(blank=True, null=True)
    pid = models.PositiveBigIntegerField(blank=True, null=True)

    def is_expired(self, max_age):
        return self.started < (timezone.now() - max_age)

    def __str__(self):
        return self.jobname

    def release(self):
        self.__class__.objects.filter(jobname=self.jobname, pid=self.pid).update(
            pid=None
        )


class MaintenanceManager(models.Manager):
    def open_maintenance(self, jobs):
        completed = self.in_bulk(jobs)
        return [Maintenance(key=job) for job in jobs if job not in completed]


class Maintenance(models.Model):
    objects = MaintenanceManager()
    key = models.CharField(max_length=255, unique=True, primary_key=True)
    result = models.TextField()

    def __str__(self):
        return self.key

    def run(self):
        func = import_string(self.key)
        try:
            result = func()
        except Exception as e:  # pylint: disable=broad-exception-caught
            result = None
            logger.exception(e)

        if result is not None:
            logger.info("Maintenance %s completed: %s", self.key, result)
            self.result = str(result)
            self.full_clean()
            self.save()
        else:
            logger.info(
                "No recordable result for maintenance %s. Maintenance not completed.",
                self.key,
            )

    class Meta:
        ordering = ("pk",)
