import logging
from abc import ABCMeta
from uuid import UUID
from typing import List, Type, TypeVar, Optional, Any
from datetime import date, datetime

from staff.departments.models import ProposalMetadata, Vacancy
from staff.headcounts.models import CreditManagementApplication

from staff.budget_position.const import WORKFLOW_STATUS, PUSH_STATUS
from staff.budget_position.workflow_service.entities.change import Change


logger = logging.getLogger(__name__)


class WorkflowInvalidStateError(Exception):
    def __init__(self, workflow_id: UUID, *args: Any):
        super().__init__(*args)
        self.workflow_id = workflow_id


class AbstractWorkflow(metaclass=ABCMeta):
    """
    This is our aggregate root. An abstraction for our workflows for working with budget position changes.
    The aggregate root is responsible for enforcing business invariants inside the aggregate,
    ensuring that the aggregate is in a consistent state at all times
    An aggregate can be referenced from the outside through its root only.
    Objects outside of the aggregate may not reference any other entities inside the aggregate.
    IMPORTANT: Probably we will have only one workflow class in future, but different workflow creation processes
    """

    code: str
    description: str

    status: Optional[str] = None
    manually_processed: Optional[bool] = None

    catalyst_id: Optional[int] = None
    should_be_pushed_automatically: bool = False
    should_be_marked_manually_processed_automatically: bool = False
    should_be_pushed_to_oebs_hire: bool = False
    should_push_new_department_to_femida: bool = False
    should_push_new_bp_to_femida: bool = False
    should_close_vacancy: bool = False
    permission_date: Optional[date] = None
    confirmed_at: Optional[datetime] = None

    proposal_id: Optional[int] = None
    vacancy_id: Optional[int] = None
    credit_management_id: Optional[int] = None

    def __init__(self, workflow_id: UUID, changes: List[Change]) -> None:
        self._changes: List[Change] = []
        self._changes_by_effective_date: List[Change] = []

        self.id = workflow_id
        self.changes: List[Change] = changes

        self.status = WORKFLOW_STATUS.PENDING
        self.manually_processed = None

        self._proposal_meta = None
        self._vacancy = None
        self._credit_management = None

    @property
    def proposal_meta(self) -> ProposalMetadata:
        assert self.proposal_id is not None
        if self._proposal_meta is None:
            self._proposal_meta = ProposalMetadata.objects.get(pk=self.proposal_id)
        return self._proposal_meta

    @property
    def vacancy(self) -> Vacancy:
        assert self.vacancy_id is not None
        if self._vacancy is None:
            self._vacancy = Vacancy.objects.get(id=self.vacancy_id)
        return self._vacancy

    @property
    def credit_management(self) -> CreditManagementApplication:
        assert self.credit_management_id is not None
        if self._credit_management is None:
            self._credit_management = CreditManagementApplication.objects.get(pk=self.credit_management_id)
        return self._credit_management

    @property
    def changes(self) -> List[Change]:
        return self._changes

    @changes.setter
    def changes(self, value):
        self._changes = value
        self.changes_by_effective_date = value

    @property
    def changes_by_effective_date(self) -> List[Change]:
        return self._changes_by_effective_date

    @changes_by_effective_date.setter
    def changes_by_effective_date(self, value):
        self._changes_by_effective_date = sorted(value, key=lambda ch: ch.effective_date)

    def confirm(self):
        if self.status != WORKFLOW_STATUS.PENDING:
            raise WorkflowInvalidStateError(self.id, self.status)

        self.status = WORKFLOW_STATUS.CONFIRMED
        self.confirmed_at = datetime.now()

    def mark_manually_processed(self, person_id: int) -> None:
        if self.status not in (WORKFLOW_STATUS.CONFIRMED, WORKFLOW_STATUS.QUEUED):
            raise WorkflowInvalidStateError(self.id, self.status)

        self.status = WORKFLOW_STATUS.FINISHED
        self.manually_processed = True
        self.catalyst_id = person_id

    def can_be_cancelled(self):
        already_done_statuses = (
            WORKFLOW_STATUS.FINISHED,
            WORKFLOW_STATUS.SENDING_NOTIFICATION,
            WORKFLOW_STATUS.PUSHED,
            WORKFLOW_STATUS.CANCELLED,
        )

        return self.status not in already_done_statuses

    def cancel(self, person_id: int) -> None:
        if self.can_be_cancelled():
            self.status = WORKFLOW_STATUS.CANCELLED
            self.catalyst_id = person_id

    def mark_pushing_to_oebs(self) -> None:
        if self.status not in (WORKFLOW_STATUS.CONFIRMED, WORKFLOW_STATUS.QUEUED):
            raise WorkflowInvalidStateError(self.id, self.status)

        self.status = WORKFLOW_STATUS.PUSHED
        self.manually_processed = False

    def mark_queued(self) -> None:
        if self.status not in (WORKFLOW_STATUS.CONFIRMED, ):
            raise WorkflowInvalidStateError(self.id, self.status)

        self.status = WORKFLOW_STATUS.QUEUED

    def mark_pushed_to_oebs(self) -> None:
        if self.status not in (WORKFLOW_STATUS.PUSHED, WORKFLOW_STATUS.SENDING_NOTIFICATION):
            raise WorkflowInvalidStateError(self.id, self.status)

        self.status = WORKFLOW_STATUS.SENDING_NOTIFICATION

    def mark_finished(self) -> None:
        if self.status != WORKFLOW_STATUS.SENDING_NOTIFICATION:
            raise WorkflowInvalidStateError(self.id, self.status)

        self.status = WORKFLOW_STATUS.FINISHED

    def get_next_change_for_sending(self) -> Optional[Change]:
        if self.has_errors():
            return None

        for change in self.changes_by_effective_date:
            if change.push_status is None:
                return change

        return None

    def has_errors(self) -> bool:
        return any(change.push_status == PUSH_STATUS.ERROR for change in self.changes)

    def is_pending(self) -> bool:
        return self.status == WORKFLOW_STATUS.PENDING

    def is_finished(self) -> bool:
        if self.has_errors():
            return True

        return all(change.push_status == PUSH_STATUS.FINISHED for change in self.changes)


WorkflowT = TypeVar('WorkflowT', bound=AbstractWorkflow)
WorkflowClassT = Type[WorkflowT]
