"""Shared memory types and constants."""

from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, Protocol, TypedDict
from uuid import uuid4

if TYPE_CHECKING:
    from pathlib import Path

    from mindroom.constants import RuntimePaths


class MemoryResult(TypedDict, total=False):
    """Type for memory search results from the configured backend."""

    id: str
    memory: str
    hash: str
    metadata: dict[str, Any] | None
    score: float
    created_at: str
    updated_at: str | None
    user_id: str


class ScopedMemoryWriter(Protocol):
    """Minimal protocol for writing scoped memory entries."""

    async def add(
        self,
        messages: list[dict],
        *,
        user_id: str | None = None,
        metadata: dict[str, object] | None = None,
    ) -> object:
        """Persist messages for a scoped memory user ID."""


class ScopedMemoryCrud(ScopedMemoryWriter, Protocol):
    """Minimal protocol for mem0 CRUD operations used by this package."""

    async def get(self, memory_id: str) -> dict[str, Any] | None:
        """Return the memory payload for a given memory ID."""

    async def get_all(
        self,
        *,
        filters: dict[str, object] | None = None,
        top_k: int = 100,
    ) -> dict[str, list[MemoryResult]]:
        """List memories for one scoped user ID."""

    async def update(self, memory_id: str, data: str) -> object:
        """Update one memory by its backend-native ID."""

    async def delete(self, memory_id: str) -> object:
        """Delete one memory by its backend-native ID."""

    async def search(
        self,
        query: str,
        *,
        filters: dict[str, object] | None = None,
        top_k: int = 100,
    ) -> dict[str, list[MemoryResult]] | list[MemoryResult]:
        """Search memories for one scoped user ID."""


@dataclass(frozen=True)
class MemorySearchOutcome:
    """One memory search's results plus its semantic-degradation state.

    ``degraded_reason`` carries the safe classified failure detail when all or
    part of the semantic path was unavailable. Results may hold keyword
    fallback matches or semantic matches from healthy scopes; it stays ``None``
    for a healthy search, including a healthy empty one.
    """

    results: list[MemoryResult]
    degraded_reason: str | None = None


class MemoryNotFoundError(ValueError):
    """Raised when a memory ID does not exist in the caller's allowed scope."""

    def __init__(self, memory_id: str) -> None:
        super().__init__(f"No memory found with id={memory_id}")


@dataclass(frozen=True)
class MemoryEntrypointContext:
    """One agent's preloaded memory entrypoint plus what the preload left out.

    The line counts make the preload self-describing: the prompt layer can tell
    the model that its `MEMORY.md` is already inlined, and how much of the file
    the ``memory.file.max_entrypoint_lines`` cap withheld.
    """

    text: str = ""
    source_path: Path | None = None
    included_lines: int = 0
    total_lines: int = 0

    @property
    def omitted_lines(self) -> int:
        """Return how many entrypoint lines the preload cap withheld."""
        return max(0, self.total_lines - self.included_lines)


@dataclass(frozen=True)
class FileMemoryResolution:
    """Resolved file-memory storage settings for a specific caller/context."""

    storage_path: Path
    runtime_paths: RuntimePaths
    use_configured_path: bool
    agent_memory_scope_path: Path | None = None


FILE_MEMORY_DEFAULT_DIRNAME = "memory_files"
FILE_MEMORY_ENTRYPOINT = "MEMORY.md"
FILE_MEMORY_DAILY_DIR = "memory"
FILE_MEMORY_ENTRY_PATTERN = re.compile(r"^- \[id=(?P<id>[^\]]+)\]\s*(?P<memory>.+?)\s*$")
FILE_MEMORY_PATH_ID_PATTERN = re.compile(r"^file:(?P<path>[^:]+):(?P<line>\d+)$")
MEM0_REPLICA_KEY = "mindroom_replica_key"


def new_memory_id() -> str:
    """Return a timestamped unique memory ID."""
    timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
    return f"m_{timestamp}_{uuid4().hex[:8]}"
