"""MemoryUnit dataclass — HU3-prime text interchange protocol.

Separates portable fields (text, category, metadata, timestamps) from
local fields (embedding, id, similarity_score). Embeddings are LOCAL CACHE,
not portable data — each system re-embeds text in its own model.

This enables cross-system memory portability via text-level interchange,
bypassing the incompatibility of different embedding models (e.g. MiniLM-L6-v2
384d vs OpenAI text-embedding-3-large 3072d).
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any


@dataclass
class MemoryUnit:
    """A single memory with clear portable vs local field separation.

    Portable fields survive cross-system transfer (export/import).
    Local fields are system-specific and regenerated on import.

    Portable:
        content, category, metadata, created_at, valid_from, valid_to,
        scope, source_system

    Local (non-portable — regenerated by receiving system):
        id, embedding, similarity_score, is_active, superseded_by, token_count
    """

    # -- Portable fields (survive cross-system transfer) --
    content: str
    category: str = "general"
    metadata: dict[str, Any] = field(default_factory=dict)
    created_at: datetime = field(default_factory=lambda: datetime.now(tz=UTC))
    valid_from: datetime | None = None
    valid_to: datetime | None = None
    scope: str = ""  # maps to proj_tag in storage ("" = global)
    source_system: str = "engram"

    # -- Local fields (non-portable, regenerated by receiving system) --
    id: int | None = None
    embedding: list[float] | None = None
    similarity_score: float | None = None
    is_active: bool = True
    superseded_by: int | None = None
    token_count: int | None = None

    def to_portable(self) -> dict[str, Any]:
        """Export only portable fields for cross-system transfer.

        Embedding, id, similarity_score, and other local fields are
        stripped — the receiving system will re-embed the text content
        in its own model.

        Returns:
            Dict with only portable fields. Datetimes are ISO 8601 strings.
        """
        data: dict[str, Any] = {
            "content": self.content,
            "category": self.category,
            "metadata": self.metadata,
            "created_at": self.created_at.isoformat(),
            "scope": self.scope,
            "source_system": self.source_system,
        }
        if self.valid_from is not None:
            data["valid_from"] = self.valid_from.isoformat()
        if self.valid_to is not None:
            data["valid_to"] = self.valid_to.isoformat()
        return data

    @classmethod
    def from_portable(cls, data: dict[str, Any]) -> MemoryUnit:
        """Create a MemoryUnit from a portable dict (e.g. imported from another system).

        Local fields (id, embedding, similarity_score, etc.) are set to
        defaults — the receiving system will populate them during storage.

        Args:
            data: Dict with portable fields as produced by to_portable().

        Returns:
            A new MemoryUnit with portable fields populated and local fields defaulted.
        """
        created_at = data.get("created_at")
        if isinstance(created_at, str):
            created_at = datetime.fromisoformat(created_at)
        elif created_at is None:
            created_at = datetime.now(tz=UTC)

        valid_from = data.get("valid_from")
        if isinstance(valid_from, str):
            valid_from = datetime.fromisoformat(valid_from)

        valid_to = data.get("valid_to")
        if isinstance(valid_to, str):
            valid_to = datetime.fromisoformat(valid_to)

        return cls(
            content=data["content"],
            category=data.get("category", "general"),
            metadata=data.get("metadata", {}),
            created_at=created_at,
            valid_from=valid_from,
            valid_to=valid_to,
            scope=data.get("scope", ""),
            source_system=data.get("source_system", "unknown"),
        )

    def to_json(self) -> str:
        """Full serialization including local fields.

        Returns:
            JSON string with all fields (portable + local).
        """
        data = self.to_portable()
        # Add local fields
        data["id"] = self.id
        data["similarity_score"] = self.similarity_score
        data["is_active"] = self.is_active
        data["superseded_by"] = self.superseded_by
        data["token_count"] = self.token_count
        # Embedding serialized as list (can be large — omit if None)
        if self.embedding is not None:
            data["embedding"] = self.embedding
        return json.dumps(data)

    @classmethod
    def from_json(cls, s: str) -> MemoryUnit:
        """Full deserialization from JSON string.

        Args:
            s: JSON string as produced by to_json().

        Returns:
            A fully populated MemoryUnit.
        """
        data = json.loads(s)

        created_at = data.get("created_at")
        if isinstance(created_at, str):
            created_at = datetime.fromisoformat(created_at)
        elif created_at is None:
            created_at = datetime.now(tz=UTC)

        valid_from = data.get("valid_from")
        if isinstance(valid_from, str):
            valid_from = datetime.fromisoformat(valid_from)

        valid_to = data.get("valid_to")
        if isinstance(valid_to, str):
            valid_to = datetime.fromisoformat(valid_to)

        return cls(
            content=data["content"],
            category=data.get("category", "general"),
            metadata=data.get("metadata", {}),
            created_at=created_at,
            valid_from=valid_from,
            valid_to=valid_to,
            scope=data.get("scope", ""),
            source_system=data.get("source_system", "engram"),
            id=data.get("id"),
            embedding=data.get("embedding"),
            similarity_score=data.get("similarity_score"),
            is_active=data.get("is_active", True),
            superseded_by=data.get("superseded_by"),
            token_count=data.get("token_count"),
        )

    def to_engram_store_args(self) -> dict[str, Any]:
        """Convert to kwargs for MemoryStorage.store().

        Maps MemoryUnit fields to the store() signature:
            content, category, metadata, proj_tag, valid_from

        Returns:
            Dict ready to be unpacked as **kwargs to storage.store().
        """
        args: dict[str, Any] = {
            "content": self.content,
            "category": self.category,
            "metadata": self.metadata if self.metadata else None,
        }
        if self.scope:
            args["proj_tag"] = self.scope
        if self.valid_from is not None:
            args["valid_from"] = self.valid_from.isoformat()
        return args

    @classmethod
    def from_search_result(cls, result: dict[str, Any]) -> MemoryUnit:
        """Create a MemoryUnit from a storage.search() result dict.

        Args:
            result: Dict as returned by MemoryStorage.search().

        Returns:
            A MemoryUnit populated from the search result.
        """
        created_at = result.get("created_at")
        if isinstance(created_at, str):
            created_at = datetime.fromisoformat(created_at)
        elif created_at is None:
            created_at = datetime.now(tz=UTC)

        valid_from = result.get("valid_from")
        if isinstance(valid_from, str):
            valid_from = datetime.fromisoformat(valid_from)

        valid_to = result.get("valid_to")
        if isinstance(valid_to, str):
            valid_to = datetime.fromisoformat(valid_to)

        return cls(
            content=result.get("content", ""),
            category=result.get("category", "general"),
            metadata=result.get("metadata", {}),
            created_at=created_at,
            valid_from=valid_from,
            valid_to=valid_to,
            scope=result.get("proj_tag") or "",
            id=result.get("id"),
            similarity_score=result.get("similarity"),
            is_active=result.get("is_active", True),
            token_count=result.get("token_count"),
        )
