"""
Base Context Types and Enums.

Provides the foundation for all context types used in
the Context Management Engine.
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from typing import Any
from uuid import uuid4


class ContextType(str, Enum):
    """
    Types of context that can be assembled.

    Each type has specific handling, formatting, and
    budget allocation rules.
    """

    SYSTEM = "system"
    TASK = "task"
    KNOWLEDGE = "knowledge"
    CONVERSATION = "conversation"
    TOOL = "tool"
    MEMORY = "memory"  # Agent memory (working, episodic, semantic, procedural)

    @classmethod
    def from_string(cls, value: str) -> "ContextType":
        """
        Convert string to ContextType.

        Args:
            value: String value

        Returns:
            ContextType enum value

        Raises:
            ValueError: If value is not a valid context type
        """
        try:
            return cls(value.lower())
        except ValueError:
            valid = ", ".join(t.value for t in cls)
            raise ValueError(f"Invalid context type '{value}'. Valid types: {valid}")


class ContextPriority(int, Enum):
    """
    Priority levels for context ordering.

    Higher values indicate higher priority.
    """

    LOWEST = 0
    LOW = 25
    NORMAL = 50
    HIGH = 75
    HIGHEST = 100
    CRITICAL = 150  # Never omit

    @classmethod
    def from_int(cls, value: int) -> "ContextPriority":
        """
        Get closest priority level for an integer.

        Args:
            value: Integer priority value

        Returns:
            Closest ContextPriority enum value
        """
        priorities = sorted(cls, key=lambda p: p.value)
        for priority in reversed(priorities):
            if value >= priority.value:
                return priority
        return cls.LOWEST


@dataclass(eq=False)
class BaseContext(ABC):
    """
    Abstract base class for all context types.

    Provides common fields and methods for context handling,
    scoring, and serialization.
    """

    # Required fields
    content: str
    source: str

    # Optional fields with defaults
    id: str = field(default_factory=lambda: str(uuid4()))
    timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
    priority: int = field(default=ContextPriority.NORMAL.value)
    metadata: dict[str, Any] = field(default_factory=dict)

    # Computed/cached fields
    _token_count: int | None = field(default=None, repr=False)
    _score: float | None = field(default=None, repr=False)

    @property
    def token_count(self) -> int | None:
        """Get cached token count (None if not counted yet)."""
        return self._token_count

    @token_count.setter
    def token_count(self, value: int) -> None:
        """Set token count."""
        self._token_count = value

    @property
    def score(self) -> float | None:
        """Get cached score (None if not scored yet)."""
        return self._score

    @score.setter
    def score(self, value: float) -> None:
        """Set score (clamped to 0.0-1.0)."""
        self._score = max(0.0, min(1.0, value))

    @abstractmethod
    def get_type(self) -> ContextType:
        """
        Get the type of this context.

        Returns:
            ContextType enum value
        """
        ...

    def get_age_seconds(self) -> float:
        """
        Get age of context in seconds.

        Returns:
            Age in seconds since creation
        """
        now = datetime.now(UTC)
        delta = now - self.timestamp
        return delta.total_seconds()

    def get_age_hours(self) -> float:
        """
        Get age of context in hours.

        Returns:
            Age in hours since creation
        """
        return self.get_age_seconds() / 3600

    def is_stale(self, max_age_hours: float = 168.0) -> bool:
        """
        Check if context is stale.

        Args:
            max_age_hours: Maximum age before considered stale (default 7 days)

        Returns:
            True if context is older than max_age_hours
        """
        return self.get_age_hours() > max_age_hours

    def to_dict(self) -> dict[str, Any]:
        """
        Convert context to dictionary for serialization.

        Returns:
            Dictionary representation
        """
        return {
            "id": self.id,
            "type": self.get_type().value,
            "content": self.content,
            "source": self.source,
            "timestamp": self.timestamp.isoformat(),
            "priority": self.priority,
            "metadata": self.metadata,
            "token_count": self._token_count,
            "score": self._score,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "BaseContext":
        """
        Create context from dictionary.

        Note: Subclasses should override this to return correct type.

        Args:
            data: Dictionary with context data

        Returns:
            Context instance
        """
        raise NotImplementedError("Subclasses must implement from_dict")

    def truncate(self, max_tokens: int, suffix: str = "... [truncated]") -> str:
        """
        Truncate content to fit within token limit.

        This is a rough estimation based on characters.
        For accurate truncation, use the TokenCalculator.

        Args:
            max_tokens: Maximum tokens allowed
            suffix: Suffix to append when truncated

        Returns:
            Truncated content
        """
        if self._token_count is None or self._token_count <= max_tokens:
            return self.content

        # Rough estimation: 4 chars per token on average
        estimated_chars = max_tokens * 4
        suffix_chars = len(suffix)

        if len(self.content) <= estimated_chars:
            return self.content

        truncated = self.content[: estimated_chars - suffix_chars]
        # Try to break at word boundary
        last_space = truncated.rfind(" ")
        if last_space > estimated_chars * 0.8:
            truncated = truncated[:last_space]

        return truncated + suffix

    def __hash__(self) -> int:
        """Hash based on ID for set/dict usage."""
        return hash(self.id)

    def __eq__(self, other: object) -> bool:
        """Equality based on ID."""
        if not isinstance(other, BaseContext):
            return False
        return self.id == other.id


@dataclass
class AssembledContext:
    """
    Result of context assembly.

    Contains the final formatted context ready for LLM consumption,
    along with metadata about the assembly process.
    """

    # Main content
    content: str
    total_tokens: int

    # Assembly metadata
    context_count: int
    excluded_count: int = 0
    assembly_time_ms: float = 0.0
    model: str = ""

    # Included contexts (optional - for inspection)
    contexts: list["BaseContext"] = field(default_factory=list)

    # Additional metadata from assembly
    metadata: dict[str, Any] = field(default_factory=dict)

    # Budget tracking
    budget_total: int = 0
    budget_used: int = 0

    # Context breakdown
    by_type: dict[str, int] = field(default_factory=dict)

    # Cache info
    cache_hit: bool = False
    cache_key: str | None = None

    # Aliases for backward compatibility
    @property
    def token_count(self) -> int:
        """Alias for total_tokens."""
        return self.total_tokens

    @property
    def contexts_included(self) -> int:
        """Alias for context_count."""
        return self.context_count

    @property
    def contexts_excluded(self) -> int:
        """Alias for excluded_count."""
        return self.excluded_count

    @property
    def budget_utilization(self) -> float:
        """Get budget utilization percentage."""
        if self.budget_total == 0:
            return 0.0
        return self.budget_used / self.budget_total

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "content": self.content,
            "total_tokens": self.total_tokens,
            "context_count": self.context_count,
            "excluded_count": self.excluded_count,
            "assembly_time_ms": round(self.assembly_time_ms, 2),
            "model": self.model,
            "metadata": self.metadata,
            "budget_total": self.budget_total,
            "budget_used": self.budget_used,
            "budget_utilization": round(self.budget_utilization, 3),
            "by_type": self.by_type,
            "cache_hit": self.cache_hit,
            "cache_key": self.cache_key,
        }

    def to_json(self) -> str:
        """Convert to JSON string."""
        import json

        return json.dumps(self.to_dict())

    @classmethod
    def from_json(cls, json_str: str) -> "AssembledContext":
        """Create from JSON string."""
        import json

        data = json.loads(json_str)
        return cls(
            content=data["content"],
            total_tokens=data["total_tokens"],
            context_count=data["context_count"],
            excluded_count=data.get("excluded_count", 0),
            assembly_time_ms=data.get("assembly_time_ms", 0.0),
            model=data.get("model", ""),
            metadata=data.get("metadata", {}),
            budget_total=data.get("budget_total", 0),
            budget_used=data.get("budget_used", 0),
            by_type=data.get("by_type", {}),
            cache_hit=data.get("cache_hit", False),
            cache_key=data.get("cache_key"),
        )
