from threading import (
    Lock,
    Condition,
    Thread,
)

from typing import (
    Dict,
    Optional,
)

from .. import logger
from actions.models import UserLockType


class UserLock(object):
    def __init__(
        self,               # type: UserLock
        user_id,            # type: str
        lock_reason,        # type: UserLockType
        lock_timeout,       # type: Optional[float]
    ):
        # type: (...) -> None

        self.user_id = user_id
        self.lock_reason = lock_reason
        self.lock_timeout = lock_timeout

        self._lock = Lock()
        self._lock.acquire()

        self.wait_condition = Condition()
        self.wait_thread = self._create_automatic_unlock_thread()
        self.wait_thread.start()

    def get_reason(self):
        return self.lock_reason

    def unlock(self):
        logger.debug("Unlock called for user: {}".format(self.user_id))
        with self.wait_condition:
            self.wait_condition.notify()

        self.wait_thread.join()

    def is_locked(self):
        return self._lock.locked()

    def _create_automatic_unlock_thread(self):
        thread = Thread(target=self._wait_and_unlock)
        thread.daemon = True
        return thread

    def _wait_and_unlock(self):
        self._wait_timeout()
        self._lock.release()

    def _wait_timeout(self):
        logger.debug("Waiting {} seconds to unlock {}".format(self.lock_timeout, self.user_id))
        with self.wait_condition:
            leave_without_timeout = self.wait_condition.wait(timeout=self.lock_timeout)

        if not leave_without_timeout:
            logger.debug("Timeout expired for user lock: {}".format(self.user_id))


class UserLocker(object):
    management_lock = Lock()

    def __init__(self):
        self.user_locks = {}  # type: Dict[str, UserLock]

    def lock_user(
        self,                    # type: UserLocker
        user_id,                 # type: str
        lock_reason,             # type: UserLockType
        lock_timeout=None,       # type: Optional[float]
    ):
        # type: (...) -> bool

        with UserLocker.management_lock:
            if self._is_locked(user_id=user_id):
                return False

            self.user_locks[user_id] = UserLock(
                user_id=user_id,
                lock_reason=lock_reason,
                lock_timeout=lock_timeout,
            )

        return True

    def unlock_user(self, user_id):
        with UserLocker.management_lock:
            if not self._is_locked(user_id=user_id):
                return

            self.user_locks[user_id].unlock()

    def get_lock_reason(
        self,           # type: UserLocker
        user_id,        # type: str
    ):
        # type: (...) -> Optional[UserLockType]

        with UserLocker.management_lock:
            if not self._is_locked(user_id=user_id):
                return None

            return self.user_locks[user_id].get_reason()

    def _is_locked(
        self,       # type: UserLocker
        user_id,    # type: str
    ):
        # type: (...) -> bool

        return user_id in self.user_locks and self.user_locks[user_id].is_locked()


user_locker = UserLocker()


def acquire_user_lock(
    user_id,                # type: str
    lock_reason,            # type: UserLockType
    lock_timeout=None,      # type: Optional[float]
):
    # type: (...) -> bool

    return user_locker.lock_user(
        user_id=user_id,
        lock_reason=lock_reason,
        lock_timeout=lock_timeout,
    )


def release_user_lock(
    user_id,  # type: str
):
    # type: (...) -> None

    user_locker.unlock_user(user_id=user_id)


def get_lock_reason(
    user_id,    # type: str
):
    # type: (...) -> Optional[UserLockType]

    return user_locker.get_lock_reason(user_id=user_id)
