import json
from datetime import (
    datetime,
    timedelta,
)
from typing import (
    Optional,
    List,
)
from uuid import (
    UUID,
    uuid4,
)

from messagebus import Event
from ._AsyncTarget import AsyncTarget
from ._PublicationAction import PublicationAction
from ._PublicationHistory import PublicationHistory


class EventPublishDetail(object):
    def __init__(
        self,
        id: Optional[UUID],
        created_at: datetime,
        external_code: Optional[str],
        event: Event,
        target: Optional[str] = None,
        async_target: Optional[AsyncTarget] = None,
        fifo: bool = False,
        acked_at: Optional[datetime] = None,
        retry_index: Optional[int] = None,
        next_retry_at: Optional[datetime] = None,
        abandoned_at: Optional[datetime] = None,
        publication_history: Optional[List[PublicationHistory]] = None,
    ):
        if acked_at is not None and (next_retry_at is not None or abandoned_at is not None):
            raise RuntimeError("An acked event cannot have a retry date or be abandoned")
        if abandoned_at is not None and next_retry_at is not None:
            raise RuntimeError("An abandoned event cannot have a retry date")

        self.id: UUID = id or uuid4()
        self.created_at = created_at
        self.external_code = external_code
        self.event = event
        self.target = target
        self.async_target = async_target
        self.fifo = fifo
        self._acked_at = acked_at
        self._next_retry_at = next_retry_at
        self._abandoned_at = abandoned_at
        self.publication_history = publication_history or []

        self._total_delay = 0
        self._previous_delay = None
        self._before_previous_delay = None
        self._retry_index = retry_index or 0

        self._async_target_id = None
        if self.async_target is not None:
            if self._acked_at is None:
                data_dict = json.loads(self.event.data.decode("utf-8"))
                self._async_target_id = data_dict[self.async_target.id_field]
        else:
            self.event.synchronous = True

        self._initialize_delays()

    @property
    def acked_at(self) -> Optional[datetime]:
        return self._acked_at

    @acked_at.setter
    def acked_at(self, value: datetime):
        if self._abandoned_at is not None:
            raise RuntimeError("A abandoned event cannot be acked")

        self._acked_at = value
        if self.async_target is not None:
            action = PublicationAction.ASYNC_REPLY_RECEIVED
        else:
            action = PublicationAction.SUCCESSFULLY_PUBLISHED

        self.publication_history.append(
            PublicationHistory(
                created_at=value,
                action=action,
            )
        )
        self._next_retry_at = None

    def is_acked(self):
        return self._acked_at is not None

    @property
    def next_retry_at(self):
        return self._next_retry_at

    @property
    def abandoned_at(self):
        return self._abandoned_at

    @abandoned_at.setter
    def abandoned_at(self, value: datetime):
        if self._acked_at is not None:
            raise RuntimeError("An acked event cannot be abandoned")

        self._abandoned_at = value
        self._next_retry_at = None

    @property
    def is_abandoned(self):
        return self._abandoned_at is not None

    @property
    def is_finished(self):
        return self.acked_at is not None or self.abandoned_at is not None

    @property
    def retry_index(self):
        return self._retry_index

    def delta_until_next_retry_at(self, moment: datetime):
        if self.is_finished:
            raise RuntimeError("Cannot calculate retry of a finished event")

        if self._next_retry_at is None:
            return timedelta()
        return self._next_retry_at - moment

    def calculate_next_retry_at(self, now: datetime):
        if self.is_finished:
            raise RuntimeError("Cannot calculate retry of a finished event")

        if self.async_target is not None:
            action = PublicationAction.ASYNC_PUBLISHED
        else:
            action = PublicationAction.UNSUCCESSFULLY_PUBLISHED

        self.publication_history.append(
            PublicationHistory(
                created_at=now,
                action=action,
            )
        )
        self._retry_index += 1
        if self.async_target is not None:
            if self._retry_index == 1440:
                self.abandoned_at = now
            else:
                self._next_retry_at = now + timedelta(seconds=60)
        else:
            if self._before_previous_delay is None:
                value = 1
                self._before_previous_delay = 1
            elif self._previous_delay is None:
                value = 1
                self._previous_delay = 1
            else:
                value = self._before_previous_delay + self._previous_delay
                self._before_previous_delay = self._previous_delay
                self._previous_delay = value

            if self._retry_index >= 33:
                self.abandoned_at = now
            else:
                self._next_retry_at = now + timedelta(seconds=value)

    def has_async_target(self):
        return self.async_target is not None

    @property
    def async_target_id(self):
        return self._async_target_id

    @property
    def async_target_reply_subject(self):
        if self.async_target is None:
            return None
        return self.async_target.reply_subject

    @property
    def async_target_id_field(self):
        if self.async_target is None:
            return None
        return self.async_target.id_field

    def _initialize_delays(self):
        if self.retry_index == 0:
            return

        for _ in range(self.retry_index):
            if self.async_target is not None:
                self._total_delay += 60
            else:
                if self._before_previous_delay is None:
                    self._before_previous_delay = 1
                elif self._previous_delay is None:
                    self._previous_delay = 1
                else:
                    value = self._before_previous_delay + self._previous_delay
                    self._before_previous_delay = self._previous_delay
                    self._previous_delay = value
