"""Anki domain entities."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal

from .enums import CardState, ReviewRating
from .exceptions import InvalidCardError, InvalidDeckError, InvalidReviewError
from .value_objects import CardText, DeckName, Tag


@dataclass(frozen=True)
class ReviewRecord:
    reviewed_on: date
    rating: ReviewRating
    previous_state: CardState
    next_state: CardState
    previous_interval_days: int
    next_interval_days: int
    previous_ease_factor: Decimal
    next_ease_factor: Decimal
    previous_due_at: date
    next_due_at: date
    previous_repetitions: int
    next_repetitions: int
    previous_lapses: int
    next_lapses: int

    def __post_init__(self) -> None:
        if not isinstance(self.reviewed_on, date):
            raise InvalidReviewError("reviewed_on must be a date")
        if not isinstance(self.rating, ReviewRating):
            raise InvalidReviewError("rating must be a ReviewRating")
        if not isinstance(self.previous_state, CardState) or not isinstance(self.next_state, CardState):
            raise InvalidReviewError("review states must be CardState values")
        for name, value in {
            "previous_interval_days": self.previous_interval_days,
            "next_interval_days": self.next_interval_days,
            "previous_repetitions": self.previous_repetitions,
            "next_repetitions": self.next_repetitions,
            "previous_lapses": self.previous_lapses,
            "next_lapses": self.next_lapses,
        }.items():
            if isinstance(value, bool) or not isinstance(value, int) or value < 0:
                raise InvalidReviewError(f"{name} must be a non-negative integer")
        if not isinstance(self.previous_ease_factor, Decimal) or not isinstance(self.next_ease_factor, Decimal):
            raise InvalidReviewError("ease factors must be Decimal values")
        if not isinstance(self.previous_due_at, date) or not isinstance(self.next_due_at, date):
            raise InvalidReviewError("due dates must be dates")


@dataclass
class Card:
    front: CardText
    back: CardText
    deck_name: DeckName | None = None
    tags: list[Tag] = field(default_factory=list)
    state: CardState = CardState.NEW
    ease_factor: Decimal = Decimal("2.50")
    interval_days: int = 0
    repetitions: int = 0
    lapses: int = 0
    due_at: date = field(default_factory=date.today)
    review_history: list[ReviewRecord] = field(default_factory=list, repr=False)

    def __post_init__(self) -> None:
        if not isinstance(self.front, CardText):
            raise InvalidCardError("front must be a CardText instance")
        if not isinstance(self.back, CardText):
            raise InvalidCardError("back must be a CardText instance")
        if self.deck_name is not None and not isinstance(self.deck_name, DeckName):
            raise InvalidCardError("deck_name must be a DeckName instance or None")
        if any(not isinstance(tag, Tag) for tag in self.tags):
            raise InvalidCardError("tags must contain Tag instances")
        if not isinstance(self.state, CardState):
            raise InvalidCardError("state must be a CardState")
        if not isinstance(self.ease_factor, Decimal):
            raise InvalidCardError("ease_factor must be a Decimal")
        if self.ease_factor < Decimal("1.30"):
            raise InvalidCardError("ease_factor cannot be lower than 1.30")
        for name, value in {
            "interval_days": self.interval_days,
            "repetitions": self.repetitions,
            "lapses": self.lapses,
        }.items():
            if isinstance(value, bool) or not isinstance(value, int) or value < 0:
                raise InvalidCardError(f"{name} must be a non-negative integer")
        if not isinstance(self.due_at, date):
            raise InvalidCardError("due_at must be a date")
        if any(not isinstance(record, ReviewRecord) for record in self.review_history):
            raise InvalidCardError("review_history must contain ReviewRecord instances")

    def is_due(self, on_date: date | None = None) -> bool:
        if on_date is not None and not isinstance(on_date, date):
            raise InvalidCardError("on_date must be a date or None")
        if self.state == CardState.SUSPENDED:
            return False
        today = on_date or date.today()
        return self.due_at <= today

    def add_tag(self, tag: Tag) -> Tag:
        if not isinstance(tag, Tag):
            raise InvalidCardError("tag must be a Tag instance")
        if tag not in self.tags:
            self.tags.append(tag)
        return tag

    def suspend(self) -> None:
        self.state = CardState.SUSPENDED

    def resume(self) -> None:
        if self.state != CardState.SUSPENDED:
            return
        self.state = CardState.REVIEW if self.repetitions > 0 else CardState.NEW

    def reset(self) -> None:
        self.state = CardState.NEW
        self.ease_factor = Decimal("2.50")
        self.interval_days = 0
        self.repetitions = 0
        self.lapses = 0
        self.due_at = date.today()
        self.review_history.clear()


@dataclass
class Deck:
    name: DeckName
    description: str | None = None
    cards: list[Card] = field(default_factory=list)
    archived: bool = False

    def __post_init__(self) -> None:
        if not isinstance(self.name, DeckName):
            raise InvalidDeckError("name must be a DeckName instance")
        if self.description is not None and (not isinstance(self.description, str) or not self.description.strip()):
            raise InvalidDeckError("description must be a non-empty string when provided")
        if any(not isinstance(card, Card) for card in self.cards):
            raise InvalidDeckError("cards must contain Card instances")
        if isinstance(self.archived, bool) is False:
            raise InvalidDeckError("archived must be a boolean")
        for card in self.cards:
            if card.deck_name is None:
                card.deck_name = self.name
            elif card.deck_name != self.name:
                raise InvalidDeckError("card belongs to a different deck")

    def add_card(self, card: Card) -> Card:
        if not isinstance(card, Card):
            raise InvalidDeckError("card must be a Card instance")
        if self.archived:
            raise InvalidDeckError("cannot add cards to an archived deck")
        if card.deck_name is not None and card.deck_name != self.name:
            raise InvalidDeckError("card belongs to a different deck")
        if card not in self.cards:
            card.deck_name = self.name
            self.cards.append(card)
        return card

    def remove_card(self, card: Card) -> None:
        if not isinstance(card, Card):
            raise InvalidDeckError("card must be a Card instance")
        try:
            self.cards.remove(card)
        except ValueError as exc:
            raise InvalidDeckError("card is not part of this deck") from exc
        card.deck_name = None

    def due_cards(self, on_date: date | None = None) -> list[Card]:
        today = on_date or date.today()
        return [card for card in self.cards if card.is_due(today)]

    def active_cards(self) -> list[Card]:
        return [card for card in self.cards if card.state != CardState.SUSPENDED]

    def new_cards(self) -> list[Card]:
        return [card for card in self.cards if card.state == CardState.NEW]

    def archive(self) -> None:
        self.archived = True

    def unarchive(self) -> None:
        self.archived = False
