"""Synap SDK Data Models.

All models use Pydantic for validation and serialization.
Response models include both typed access and raw escape hatch.
"""

from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field

from .enums import CompactionLevel


def _parse_iso_datetime(value) -> Optional[datetime]:
    """Parse an ISO 8601 string to datetime, returning None on failure."""
    if value is None:
        return None
    if isinstance(value, datetime):
        return value
    try:
        return datetime.fromisoformat(str(value))
    except (ValueError, TypeError):
        return None


# Context Items
class Fact(BaseModel):
    """A factual piece of information about an entity."""

    id: str
    content: str
    confidence: float = Field(ge=0.0, le=1.0)
    source: str
    extracted_at: datetime
    metadata: Dict[str, Any] = Field(default_factory=dict)
    event_date: Optional[datetime] = None
    valid_until: Optional[datetime] = None
    temporal_category: Optional[str] = None
    temporal_confidence: float = 0.0


class Preference(BaseModel):
    """A preference or behavioral pattern."""

    id: str
    category: str
    content: str
    strength: float = Field(ge=0.0, le=1.0)  # How strong is this preference
    source: str = ""
    extracted_at: datetime
    metadata: Dict[str, Any] = Field(default_factory=dict)
    event_date: Optional[datetime] = None
    valid_until: Optional[datetime] = None
    temporal_category: Optional[str] = None
    temporal_confidence: float = 0.0


class Episode(BaseModel):
    """A memorable event or interaction."""

    id: str
    summary: str
    occurred_at: datetime
    significance: float = Field(ge=0.0, le=1.0)
    participants: List[str] = Field(default_factory=list)
    metadata: Dict[str, Any] = Field(default_factory=dict)
    event_date: Optional[datetime] = None
    valid_until: Optional[datetime] = None
    temporal_category: Optional[str] = None
    temporal_confidence: float = 0.0


class Emotion(BaseModel):
    """Detected emotional state."""

    id: str
    emotion_type: str  # e.g., "frustrated", "satisfied", "confused"
    intensity: float = Field(ge=0.0, le=1.0)
    detected_at: datetime
    context: str  # What triggered this
    metadata: Dict[str, Any] = Field(default_factory=dict)
    event_date: Optional[datetime] = None
    valid_until: Optional[datetime] = None
    temporal_category: Optional[str] = None
    temporal_confidence: float = 0.0


class TemporalEvent(BaseModel):
    """A time-bound event or fact with explicit temporal boundaries."""

    id: str
    content: str
    event_date: datetime
    valid_until: Optional[datetime] = None
    temporal_category: str  # "perpetual" | "temporal_fact" | "episode"
    temporal_confidence: float = Field(ge=0.0, le=1.0)
    confidence: float = Field(ge=0.0, le=1.0, default=0.0)
    source: str = ""
    extracted_at: Optional[datetime] = None
    metadata: Dict[str, Any] = Field(default_factory=dict)


# Response Metadata
class ResponseMetadata(BaseModel):
    """Metadata about a context response."""

    correlation_id: str
    ttl_seconds: int
    source: str  # "cache" or "cloud"
    retrieved_at: datetime
    compaction_applied: Optional[CompactionLevel] = None


class ConversationContextModel(BaseModel):
    """Current session context from compaction + raw buffer.

    Provides the narrative summary, current state, key extractions,
    and recent turns as a coherent block (not atomized into items).
    """
    summary: Optional[str] = None
    current_state: Dict[str, Any] = Field(default_factory=dict)
    key_extractions: Dict[str, List[Dict[str, Any]]] = Field(default_factory=dict)
    recent_turns: List[Dict[str, Any]] = Field(default_factory=list)
    compaction_id: Optional[str] = None
    compacted_at: Optional[str] = None
    conversation_id: Optional[str] = None


# Response Wrappers (Hybrid: typed + raw)
class ContextResponse(BaseModel):
    """Response from context fetch operations.

    Provides typed access to common fields and raw escape hatch.
    """

    facts: List[Fact] = Field(default_factory=list)
    preferences: List[Preference] = Field(default_factory=list)
    episodes: List[Episode] = Field(default_factory=list)
    emotions: List[Emotion] = Field(default_factory=list)
    temporal_events: List[TemporalEvent] = Field(default_factory=list)
    conversation_context: Optional[ConversationContextModel] = None
    metadata: ResponseMetadata

    # Raw response for forward compatibility
    model_config = {"extra": "allow"}
    
    # Private storage for raw data (not a field)
    def __init__(self, **data):
        # Extract raw data before calling parent init
        raw_data = data.pop("raw_data", data.copy())
        super().__init__(**data)
        object.__setattr__(self, "_raw_data", raw_data)

    @property
    def raw(self) -> Dict[str, Any]:
        """Access raw response for fields not yet in typed model."""
        return getattr(self, "_raw_data", {})

    @classmethod
    def from_cloud_response(
        cls, data: Dict[str, Any], metadata: ResponseMetadata
    ) -> "ContextResponse":
        """Factory to create from cloud API response.

        Maps generic ContextItem fields from the cloud API to type-specific
        SDK model fields (e.g. confidence -> strength for Preference).
        """
        facts = []
        for f in data.get("facts", []):
            facts.append(Fact(
                id=f.get("id", ""),
                content=f.get("content", ""),
                confidence=f.get("confidence", 0.0),
                source=f.get("source", ""),
                extracted_at=f.get("extracted_at") or datetime.now(timezone.utc),
                metadata=f.get("metadata", {}),
                event_date=_parse_iso_datetime(f.get("event_date")),
                valid_until=_parse_iso_datetime(f.get("valid_until")),
                temporal_category=f.get("temporal_category"),
                temporal_confidence=f.get("temporal_confidence", 0.0),
            ))

        preferences = []
        for p in data.get("preferences", []):
            preferences.append(Preference(
                id=p.get("id", ""),
                category=p.get("category", ""),
                content=p.get("content", ""),
                strength=p.get("strength", p.get("confidence", 0.0)),
                extracted_at=p.get("extracted_at") or datetime.now(timezone.utc),
                metadata=p.get("metadata", {}),
                event_date=_parse_iso_datetime(p.get("event_date")),
                valid_until=_parse_iso_datetime(p.get("valid_until")),
                temporal_category=p.get("temporal_category"),
                temporal_confidence=p.get("temporal_confidence", 0.0),
            ))

        episodes = []
        for e in data.get("episodes", []):
            episodes.append(Episode(
                id=e.get("id", ""),
                summary=e.get("summary", e.get("content", "")),
                occurred_at=e.get("occurred_at") or e.get("extracted_at") or datetime.now(timezone.utc),
                significance=e.get("significance", e.get("confidence", 0.0)),
                participants=e.get("participants", []),
                metadata=e.get("metadata", {}),
                event_date=_parse_iso_datetime(e.get("event_date")),
                valid_until=_parse_iso_datetime(e.get("valid_until")),
                temporal_category=e.get("temporal_category"),
                temporal_confidence=e.get("temporal_confidence", 0.0),
            ))

        emotions = []
        for em in data.get("emotions", []):
            emotions.append(Emotion(
                id=em.get("id", ""),
                emotion_type=em.get("emotion_type", em.get("category", "")),
                intensity=em.get("intensity", em.get("confidence", 0.0)),
                detected_at=em.get("detected_at") or em.get("extracted_at") or datetime.now(timezone.utc),
                context=em.get("context", em.get("content", "")),
                metadata=em.get("metadata", {}),
                event_date=_parse_iso_datetime(em.get("event_date")),
                valid_until=_parse_iso_datetime(em.get("valid_until")),
                temporal_category=em.get("temporal_category"),
                temporal_confidence=em.get("temporal_confidence", 0.0),
            ))

        temporal_events = []
        for t in data.get("temporal_events", []):
            evt_date = _parse_iso_datetime(t.get("event_date"))
            if evt_date is None:
                continue
            temporal_events.append(TemporalEvent(
                id=t.get("id", ""),
                content=t.get("content", ""),
                event_date=evt_date,
                valid_until=_parse_iso_datetime(t.get("valid_until")),
                temporal_category=t.get("temporal_category", ""),
                temporal_confidence=t.get("temporal_confidence", 0.0),
                confidence=t.get("confidence", 0.0),
                source=t.get("source", ""),
                extracted_at=_parse_iso_datetime(t.get("extracted_at")),
                metadata=t.get("metadata", {}),
            ))

        conv_ctx = None
        conv_ctx_data = data.get("conversation_context")
        if conv_ctx_data and isinstance(conv_ctx_data, dict):
            conv_ctx = ConversationContextModel(**conv_ctx_data)

        return cls(
            facts=facts,
            preferences=preferences,
            episodes=episodes,
            emotions=emotions,
            temporal_events=temporal_events,
            conversation_context=conv_ctx,
            metadata=metadata,
            raw_data=data,
        )


class CompactionResponse(BaseModel):
    """Response from context compaction."""

    compacted_context: str
    original_token_count: int
    compacted_token_count: int
    compression_ratio: float
    level_applied: CompactionLevel
    metadata: ResponseMetadata

    # New fields for enhanced compaction
    compaction_id: Optional[str] = None
    strategy_used: Optional[str] = None
    validation_score: Optional[float] = None
    validation_passed: Optional[bool] = None
    facts: List[Dict[str, Any]] = Field(default_factory=list)
    decisions: List[Dict[str, Any]] = Field(default_factory=list)
    preferences: List[Dict[str, Any]] = Field(default_factory=list)
    current_state: Optional[Dict[str, Any]] = None
    quality_warning: Optional[bool] = None

    model_config = {"extra": "allow"}
    
    def __init__(self, **data):
        raw_data = data.pop("raw_data", data.copy())
        super().__init__(**data)
        object.__setattr__(self, "_raw_data", raw_data)

    @property
    def raw(self) -> Dict[str, Any]:
        return getattr(self, "_raw_data", {})


class CompactionTriggerResponse(BaseModel):
    """Response from triggering an async compaction.

    Returned by compact() when compaction is triggered asynchronously.
    Use get_compaction_status() to poll for completion, then
    get_compacted() to retrieve the result.

    If a previous compaction exists for this conversation, it is returned
    in ``previous_context`` so the caller has usable context immediately
    (stale-while-revalidate pattern).
    """

    compaction_id: str
    conversation_id: str
    status: str  # "in_progress"
    trigger_type: str  # "manual_api"
    initiated_at: datetime
    estimated_completion_seconds: Optional[int] = 60

    # Stale-while-revalidate: existing compacted context (if any)
    previous_context: Optional[Dict[str, Any]] = None
    previous_context_age_seconds: Optional[int] = None
    previous_compaction_id: Optional[str] = None


class CompactionStatusResponse(BaseModel):
    """Response from checking compaction status."""

    conversation_id: str
    status: str  # "not_started" | "in_progress" | "completed" | "failed"
    compaction_id: Optional[str] = None
    completed_at: Optional[datetime] = None
    compression_ratio: Optional[float] = None
    validation_score: Optional[float] = None
    estimated_completion_seconds: Optional[int] = None
    error_message: Optional[str] = None
    latest_version: Optional[int] = None
    latest_created_at: Optional[datetime] = None


class RecentMessage(BaseModel):
    """A recent un-compacted conversation message."""
    role: str
    content: str
    timestamp: datetime
    message_id: str


class ContextForPromptResponse(BaseModel):
    """Response from get_context_for_prompt().

    Provides pre-formatted compacted context + recent un-compacted messages
    ready for LLM prompt injection. ``formatted_context`` is the combined
    output (compacted history + recent messages). ``recent_messages`` gives
    raw access for custom formatting.
    """

    # Combined output (compacted + recent messages formatted together)
    formatted_context: Optional[str] = None
    available: bool = False
    is_stale: bool = False
    compression_ratio: Optional[float] = None
    validation_score: Optional[float] = None
    compaction_age_seconds: Optional[int] = None
    quality_warning: bool = False

    # Recent un-compacted messages since last compaction
    recent_messages: List[RecentMessage] = []
    recent_message_count: int = 0
    compacted_message_count: int = 0
    total_message_count: int = 0


# Backward compatibility - deprecated dataclass-style models
class ContextItem:
    """Individual context item within a bundle.

    DEPRECATED: Use Fact, Preference, Episode, or Emotion instead.
    """

    def __init__(
        self,
        context_type: str,
        content: str,
        metadata: Optional[Dict[str, Any]] = None,
        confidence_score: float = 1.0,
        timestamp: Optional[datetime] = None,
    ):
        self.context_type = context_type
        self.content = content
        self.metadata = metadata or {}
        self.confidence_score = confidence_score
        self.timestamp = timestamp


class ContextBundle:
    """Bundle of context items returned by the SDK.

    DEPRECATED: Use ContextResponse instead.
    """

    def __init__(
        self,
        items: Optional[List[ContextItem]] = None,
        total_count: int = 0,
        metadata: Optional[Dict[str, Any]] = None,
        correlation_id: Optional[str] = None,
    ):
        self.items = items or []
        self.total_count = total_count
        self.metadata = metadata or {}
        self.correlation_id = correlation_id
