"""ManagedEntry dataclass for storing values with metadata.

The ManagedEntry class wraps stored values with metadata including creation time
and expiration time. This allows stores to track TTL information consistently.
"""

import json
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, SupportsFloat

from typing_extensions import Self

from key_value.aio._utils.beartype import bear_enforce
from key_value.aio._utils.time_to_live import now, now_plus, seconds_to
from key_value.aio.errors import DeserializationError, SerializationError


@dataclass(kw_only=True)
class ManagedEntry:
    """A managed cache entry containing value data and TTL metadata.

    The entry supports either TTL seconds or absolute expiration datetime. On init:
    - If `ttl` is provided but `expires_at` is not, an `expires_at` will be computed.
    - If `expires_at` is provided but `ttl` is not, a live TTL will be computed on access.
    """

    value: Mapping[str, Any]

    created_at: datetime | None = field(default=None)
    expires_at: datetime | None = field(default=None)

    @property
    def is_expired(self) -> bool:
        if self.expires_at is None:
            return False
        return self.expires_at <= now()

    @property
    def ttl(self) -> float | None:
        if self.expires_at is None:
            return None
        return seconds_to(datetime=self.expires_at)

    @property
    def value_as_json(self) -> str:
        """Return the value as a JSON string."""
        return dump_to_json(obj=self.value_as_dict)

    @property
    def value_as_dict(self) -> dict[str, Any]:
        return verify_dict(obj=self.value)

    @property
    def created_at_isoformat(self) -> str | None:
        return self.created_at.isoformat() if self.created_at else None

    @property
    def expires_at_isoformat(self) -> str | None:
        return self.expires_at.isoformat() if self.expires_at else None

    @classmethod
    def from_ttl(cls, *, value: Mapping[str, Any], created_at: datetime | None = None, ttl: SupportsFloat) -> Self:
        return cls(
            value=value,
            created_at=created_at,
            expires_at=(now_plus(seconds=float(ttl)) if ttl else None),
        )


@bear_enforce
def dump_to_json(obj: dict[str, Any]) -> str:
    """Serialize a dictionary to a JSON string."""
    try:
        return json.dumps(obj, sort_keys=True)
    except (json.JSONDecodeError, TypeError) as e:
        msg: str = f"Failed to serialize object to JSON: {e}"
        raise SerializationError(msg) from e


@bear_enforce
def load_from_json(json_str: str) -> dict[str, Any]:
    """Deserialize a JSON string to a dictionary."""
    try:
        return verify_dict(obj=json.loads(json_str))

    except (json.JSONDecodeError, TypeError) as e:
        msg: str = f"Failed to deserialize JSON string: {e}"
        raise DeserializationError(msg) from e


@bear_enforce
def verify_dict(obj: Any) -> dict[str, Any]:
    """Verify that an object is a mapping with string keys."""
    if not isinstance(obj, Mapping):
        msg = "Object is not a Mapping"
        raise TypeError(msg)

    if not all(isinstance(key, str) for key in obj):  # pyright: ignore[reportUnknownVariableType]
        msg = "Object contains non-string keys"
        raise TypeError(msg)

    return dict(obj)  # pyright: ignore[reportUnknownArgumentType]


def estimate_serialized_size(value: Mapping[str, Any]) -> int:
    """Estimate the serialized size of a value without creating a ManagedEntry.

    This function provides a more efficient way to estimate the size of a value
    when serialized to JSON, without the overhead of creating a full ManagedEntry object.
    This is useful for size-based checks in wrappers.

    Args:
        value: The value mapping to estimate the size for.

    Returns:
        The estimated size in bytes when serialized to JSON.
    """
    return len(dump_to_json(obj=dict(value)))
