import json
import logging
from collections import deque

import time
from typing import Text, Optional, Union, Deque, Dict, Any

logger = logging.getLogger(__name__)

NO_TICKET_ISSUED = -1  # index of latest issued ticket if no tickets exist


class Ticket:
    def __init__(self, number: int, expires: float) -> None:
        self.number = number
        self.expires = expires

    def has_expired(self) -> bool:
        return time.time() > self.expires

    def as_dict(self) -> Dict[Text, Any]:
        return dict(number=self.number, expires=self.expires)

    def dumps(self) -> Text:
        """Return json dump of `Ticket` as dictionary."""
        return json.dumps(self.as_dict())

    @classmethod
    def from_dict(cls, data: Dict[Text, Union[int, float]]) -> "Ticket":
        """Creates `Ticket` from dictionary."""
        return cls(number=data["number"], expires=data["expires"])

    def __repr__(self) -> Text:
        return f"Ticket(number: {self.number}, expires: {self.expires})"


class TicketLock:
    """Locking mechanism that issues tickets managing access to conversation IDs.

    Tickets are issued in the order in which they are requested. A detailed
    explanation of the ticket lock algorithm can be found at
    http://pages.cs.wisc.edu/~remzi/OSTEP/threads-locks.pdf#page=13
    """

    def __init__(
        self, conversation_id: Text, tickets: Optional[Deque[Ticket]] = None
    ) -> None:
        self.conversation_id = conversation_id
        self.tickets = tickets or deque()

    @classmethod
    def from_dict(cls, data: Dict[Text, Any]) -> "TicketLock":
        """Create `TicketLock` from dictionary."""
        tickets = [Ticket.from_dict(json.loads(d)) for d in data.get("tickets", [])]
        return cls(data.get("conversation_id"), deque(tickets))

    def dumps(self) -> Text:
        """Return json dump of `TicketLock`."""
        tickets = [ticket.dumps() for ticket in self.tickets]
        return json.dumps(dict(conversation_id=self.conversation_id, tickets=tickets))

    def is_locked(self, ticket_number: int) -> bool:
        """Return whether `ticket_number` is locked.

        Returns:
             True if `now_serving` is not equal to `ticket`.
        """
        return self.now_serving != ticket_number

    def issue_ticket(self, lifetime: float) -> int:
        """Issue a new ticket and return its number."""
        self.remove_expired_tickets()
        number = self.last_issued + 1
        ticket = Ticket(number, time.time() + lifetime)
        self.tickets.append(ticket)

        return number

    def remove_expired_tickets(self) -> None:
        """Remove expired tickets."""
        # iterate over copy of self.tickets so we can remove items
        for ticket in list(self.tickets):
            if ticket.has_expired():
                self.tickets.remove(ticket)

    @property
    def last_issued(self) -> int:
        """Return number of the ticket that was last added.

        Returns:
             Number of `Ticket` that was last added. `NO_TICKET_ISSUED` if no
             tickets exist.
        """
        ticket_number = self._ticket_number_for(-1)

        return ticket_number if ticket_number is not None else NO_TICKET_ISSUED

    @property
    def now_serving(self) -> Optional[int]:
        """Get number of the ticket to be served next.

        Returns:
             Number of `Ticket` that is served next. 0 if no `Ticket` exists.
        """
        return self._ticket_number_for(0) or 0

    def _ticket_number_for(self, ticket_index: int) -> Optional[int]:
        """Get ticket number for `ticket_index`.

        Returns:
             Ticket number for `Ticket` with index `ticket_index`. None if there are no
             tickets, or if `ticket_index` is out of bounds of `self.tickets`.
        """
        self.remove_expired_tickets()

        try:
            return self.tickets[ticket_index].number
        except IndexError:
            return None

    def _ticket_for_ticket_number(self, ticket_number: int) -> Optional[Ticket]:
        """Return ticket for `ticket_number`."""
        self.remove_expired_tickets()

        return next((t for t in self.tickets if t.number == ticket_number), None)

    def is_someone_waiting(self) -> bool:
        """Return whether someone is waiting for the lock to become available.

        Returns:
             True if the `self.tickets` queue has length greater than 0.
        """
        return len(self.tickets) > 0

    def remove_ticket_for(self, ticket_number: int) -> None:
        """Remove `Ticket` for `ticket_number."""
        ticket = self._ticket_for_ticket_number(ticket_number)
        if ticket:
            self.tickets.remove(ticket)
