"""
fsrs.card
---------

This module defines the Card and State classes.

Classes:
    Card: Represents a flashcard in the FSRS system.
    State: Enum representing the learning state of a Card object.
"""

from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import time
import json
from typing import TypedDict
from typing_extensions import Self
from fsrs.state import State


class CardDict(TypedDict):
    """
    JSON-serializable dictionary representation of a Card object.
    """

    card_id: int
    state: int
    step: int | None
    stability: float | None
    difficulty: float | None
    due: str
    last_review: str | None


@dataclass(init=False)
class Card:
    """
    Represents a flashcard in the FSRS system.

    Attributes:
        card_id: The id of the card. Defaults to the epoch milliseconds of when the card was created.
        state: The card's current learning state.
        step: The card's current learning or relearning step or None if the card is in the Review state.
        stability: Core mathematical parameter used for future scheduling.
        difficulty: Core mathematical parameter used for future scheduling.
        due: The date and time when the card is due next.
        last_review: The date and time of the card's last review.
    """

    card_id: int
    state: State
    step: int | None
    stability: float | None
    difficulty: float | None
    due: datetime
    last_review: datetime | None

    def __init__(
        self,
        card_id: int | None = None,
        state: State = State.Learning,
        step: int | None = None,
        stability: float | None = None,
        difficulty: float | None = None,
        due: datetime | None = None,
        last_review: datetime | None = None,
    ) -> None:
        if card_id is None:
            # epoch milliseconds of when the card was created
            card_id = int(datetime.now(timezone.utc).timestamp() * 1000)
            # wait 1ms to prevent potential card_id collision on next Card creation
            time.sleep(0.001)
        self.card_id = card_id

        self.state = state

        if self.state == State.Learning and step is None:
            step = 0
        self.step = step

        self.stability = stability
        self.difficulty = difficulty

        if due is None:
            due = datetime.now(timezone.utc)
        self.due = due

        self.last_review = last_review

    def to_dict(self) -> CardDict:
        """
        Returns a dictionary representation of the Card object.

        Returns:
            CardDict: A dictionary representation of the Card object.
        """

        return {
            "card_id": self.card_id,
            "state": self.state.value,
            "step": self.step,
            "stability": self.stability,
            "difficulty": self.difficulty,
            "due": self.due.isoformat(),
            "last_review": self.last_review.isoformat() if self.last_review else None,
        }

    @classmethod
    def from_dict(cls, source_dict: CardDict) -> Self:
        """
        Creates a Card object from an existing dictionary.

        Args:
            source_dict: A dictionary representing an existing Card object.

        Returns:
            Self: A Card object created from the provided dictionary.
        """

        return cls(
            card_id=int(source_dict["card_id"]),
            state=State(int(source_dict["state"])),
            step=source_dict["step"],
            stability=(
                float(source_dict["stability"]) if source_dict["stability"] else None
            ),
            difficulty=(
                float(source_dict["difficulty"]) if source_dict["difficulty"] else None
            ),
            due=datetime.fromisoformat(source_dict["due"]),
            last_review=(
                datetime.fromisoformat(source_dict["last_review"])
                if source_dict["last_review"]
                else None
            ),
        )

    def to_json(self, indent: int | str | None = None) -> str:
        """
        Returns a JSON-serialized string of the Card object.

        Args:
            indent: Equivalent argument to the indent in json.dumps()

        Returns:
            str: A JSON-serialized string of the Card object.
        """
        return json.dumps(self.to_dict(), indent=indent)

    @classmethod
    def from_json(cls, source_json: str) -> Self:
        """
        Creates a Card object from a JSON-serialized string.

        Args:
            source_json: A JSON-serialized string of an existing Card object.

        Returns:
            Self: A Card object created from the JSON string.
        """

        source_dict: CardDict = json.loads(source_json)
        return cls.from_dict(source_dict=source_dict)


__all__ = ["Card"]
