"""This module contains the Redis models for the ``trainings`` app."""

from limpyd import model
from limpyd_extensions.dynamic.fields import DynamicStringField
from limpyd_extensions.dynamic.model import ModelWithDynamicFieldMixin

from mtp.apps.common.limpyd_base import DjangoBackedRedisModel

from .constants import LockableFields
from .models import (
    Training as DjangoTraining,
    TrainingSession as DjangoTrainingSession,
)


class Training(ModelWithDynamicFieldMixin, DjangoBackedRedisModel):
    """Redis model for the ``Training`` django model.

    It allows to store in redis the lock for a duplicate currently running.

    Attributes
    ----------
    DUPLICATE_LOCK_TIMEOUT : int
        Number of second a lock on a duplicate will be held

    duplicate_to_members_locks : DynamicStringField
        Will store a ``String`` key for each duplicate running.
        The key is computed with the arguments that identify a specific duplicate.
        When this key is present, no duplicate could be created that would have the same key.
        This key will only live for the amount of time defined in ``DUPLICATE_LOCK_TIMEOUT``
    create_draft_locks : DynamicStringField
        Will store a ``String`` key for each draft creation running.
        The key is computed with the arguments that identify a specific draft, ie the owner for now.
        When this key is present, no draft could be created that would have the same key.
        This key will only live for the amount of time defined in ``DUPLICATE_LOCK_TIMEOUT``

    """

    django_model = DjangoTraining

    DUPLICATE_LOCK_TIMEOUT = 300

    duplicate_to_member_locks = DynamicStringField()
    create_draft_locks = DynamicStringField()

    @staticmethod
    def get_lock_key_from_params(**params):
        """Get a key to use for lock fields given a dict of params.

        Parameters
        ----------
        params : dict
            A dict, where each value must be hashable, representing the thing we want to lock.

        Returns
        -------
            A string representing the the ``params`` dict.

        """

        return ','.join(
            '%s:%s' % (key, hash(val))
            for key, val in sorted(params.items())
        )

    def get_action_lock_field(self, kind, **params):
        """Return a specific field from the field defined by ``kind`` for the given params.

        Parameters
        ----------
        kind : string
            The name of a "lock" field (without the ``_locks`` suffix) to use for the lock.
        params : dict
            A dict, where each value must be hashable, representing the thing we want to lock.

        Returns
        -------
        limpyd.fields.StringField
            The specific field

        """

        main_field = getattr(self, '%s_locks' % kind)

        return main_field.get_for(
            self.get_lock_key_from_params(**params)
        )

    def is_action_locked(self, kind, **params):
        """Tell if the action defined by ``kind`` for the current params is actually locked.

        Parameters
        ----------
        kind : string
            The name of a "lock" field (without the ``_locks`` suffix) to use for the lock.
        params : dict
            A dict, where each value must be hashable, representing the thing we want to lock.

        Returns
        -------
        bool
            ``True`` if the action for the given params is actually locked. ``False`` otherwise.

        """

        return self.get_action_lock_field(kind, **params).exists()

    def is_duplicate_to_member_locked(self, **params):
        """Tell if the duplicate of the tied training for the current params is actually locked.

        Parameters
        ----------
        params : dict
            A dict, where each value must be hashable, representing the duplicate we want to lock.

        Returns
        -------
        bool
            ``True`` if the duplicate for the given params is actually locked. ``False`` otherwise.

        """

        return self.is_action_locked(LockableFields.DUPLICATE_TO_MEMBER, **params)

    def is_create_draft_locked(self, **params):
        """Tell if the draft creation of the tied training for the current params is actually locked

        Parameters
        ----------
        params : dict
            A dict, where each value must be hashable, representing the draft creation we want to
            lock.

        Returns
        -------
        bool
            ``True`` if the draft creation for the given params is actually locked.
            ``False`` otherwise.

        """

        return self.is_action_locked(LockableFields.CREATE_DRAFT, **params)

    def lock_action(self, kind, lock_value="", **params):
        """Set the lock for the action defined by ``kind`` with the given params.

        Parameters
        ----------
        kind : string
            The name of a "lock" field (without the ``_locks`` suffix) to use for the lock.
        params : dict
            A dict, where each value must be hashable, representing the thing we want to lock.

        Returns
        -------
        bool
            ``True`` if the lock have been set. ``False`` if the lock was already set.

        """

        field = self.get_action_lock_field(kind, **params)

        # Try to set the field. ``setnx`` will return a truly value if the key was updated,
        # and a falsy one if the key was already present.
        lock_set = bool(field.setnx(lock_value))

        if lock_set:
            # Auto-expire the lock (not done by the redis lock)
            self.connection.expire(field.key, self.DUPLICATE_LOCK_TIMEOUT)

        return lock_set

    def lock_duplicate_to_member(self, lock_value="", **params):
        """Set the lock for the duplicate of the tied training with the given params.

        Parameters
        ----------
        params : dict
            A dict, where each value must be hashable, representing the duplicate we want to lock.

        Returns
        -------
        bool
            ``True`` if the lock have been set. ``False`` if the lock was already set.

        """

        return self.lock_action(LockableFields.DUPLICATE_TO_MEMBER, lock_value, **params)

    def lock_create_draft(self, lock_value="", **params):
        """Set the lock for the draft creation of the tied training with the given params.

        Parameters
        ----------
        params : dict
            A dict, where each value must be hashable, representing the draft creation we want to
            lock.

        Returns
        -------
        bool
            ``True`` if the lock have been set. ``False`` if the lock was already set.

        """

        return self.lock_action(LockableFields.CREATE_DRAFT, lock_value, **params)

    def release_action_lock(self, kind, **params):
        """Remove the lock for the action defined by ``kind`` with the given params.

        Parameters
        ----------
        kind : string
            The name of a "lock" field (without the ``_locks`` suffix) to use for the lock.
        params : dict
            A dict, where each value must be hashable, representing the thing we want to lock.

        Returns
        -------
        None

        """
        self.get_action_lock_field(kind, **params).delete()

    def release_duplicate_to_member_lock(self, **params):
        """Remove the lock for the duplicate of the tied training with the given params.

        Parameters
        ----------
        params : dict
            A dict, where each value must be hashable, representing the duplicate we want to lock.

        Returns
        -------
        None

        """

        self.release_action_lock(LockableFields.DUPLICATE_TO_MEMBER, **params)

    def release_create_draft_lock(self, **params):
        """Remove the lock for the draft creation of the tied training with the given params.

        Parameters
        ----------
        params : dict
            A dict, where each value must be hashable, representing the draft creation we want to
            lock.

        Returns
        -------
        None

        """

        self.release_action_lock(LockableFields.CREATE_DRAFT, **params)


class TrainingSession(DjangoBackedRedisModel):
    """Redis model for the ``TrainingSession`` django model"""

    django_model = DjangoTrainingSession

    join_times = model.SortedSetField()
    last_pings = model.SortedSetField()
