from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from typing import cast
from uuid import uuid4

from eventsourcing.domain import Aggregate, AggregateCreated, AggregateEvent


@dataclass(frozen=True)
class EmailAddress:
    address: str


class BankAccount(Aggregate):
    """Aggregate root for bank accounts."""

    def __init__(self, full_name: str, email_address: EmailAddress):
        self.full_name = full_name
        self.email_address = email_address
        self.balance = Decimal("0.00")
        self.overdraft_limit = Decimal("0.00")
        self.is_closed = False

    @classmethod
    def open(cls, full_name: str, email_address: str) -> BankAccount:
        """Creates new bank account object."""
        return cls._create(
            cls.Opened,
            id=uuid4(),
            full_name=full_name,
            email_address=EmailAddress(email_address),
        )

    class Opened(AggregateCreated):
        full_name: str
        email_address: str

    def append_transaction(self, amount: Decimal) -> None:
        """Appends given amount as transaction on account."""
        self.check_account_is_not_closed()
        self.check_has_sufficient_funds(amount)
        self.trigger_event(
            self.TransactionAppended,
            amount=amount,
        )

    def check_account_is_not_closed(self) -> None:
        if self.is_closed:
            raise AccountClosedError({"account_id": self.id})

    def check_has_sufficient_funds(self, amount: Decimal) -> None:
        if self.balance + amount < -self.overdraft_limit:
            raise InsufficientFundsError({"account_id": self.id})

    @dataclass(frozen=True)
    class TransactionAppended(AggregateEvent):
        """Domain event for when transaction
        is appended to bank account.
        """

        amount: Decimal

        def apply(self, aggregate: Aggregate) -> None:
            """Increments the account balance."""
            cast("BankAccount", aggregate).balance += self.amount

    def set_overdraft_limit(self, overdraft_limit: Decimal) -> None:
        """Sets the overdraft limit."""
        # Check the limit is not a negative value.
        assert overdraft_limit >= Decimal("0.00")
        self.check_account_is_not_closed()
        self.trigger_event(
            self.OverdraftLimitSet,
            overdraft_limit=overdraft_limit,
        )

    class OverdraftLimitSet(AggregateEvent):
        """Domain event for when overdraft
        limit is set.
        """

        overdraft_limit: Decimal

        def apply(self, aggregate: Aggregate) -> None:
            cast("BankAccount", aggregate).overdraft_limit = self.overdraft_limit

    def close(self) -> None:
        """Closes the bank account."""
        self.trigger_event(self.Closed)

    class Closed(AggregateEvent):
        """Domain event for when account is closed."""

        def apply(self, aggregate: Aggregate) -> None:
            cast("BankAccount", aggregate).is_closed = True


class AccountClosedError(Exception):
    """Raised when attempting to operate a closed account."""


class InsufficientFundsError(Exception):
    """Raised when attempting to go past overdraft limit."""
