"""Tenant-specific persistent memory for AiCippy.

Each logged-in user has isolated memory storage that persists across sessions.
Stores user preferences, learned context, and workspace metadata.

When a ``TenantContext`` is provided, storage is namespaced as
``{tenant_id}/{user_id}``. A bare ``user_id`` is still accepted for
backward compatibility but logs a deprecation warning.
"""

from __future__ import annotations

import asyncio
import json
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final

from aicippy.platform.tenant_context import TenantContext
from aicippy.utils.logging import get_logger

logger = get_logger(__name__)

MEMORY_DIR_NAME: Final[str] = "tenant_memory"
MEMORY_FILE: Final[str] = "memory.json"
MAX_MEMORY_ENTRIES: Final[int] = 500
SUMMARY_CONTEXT_MAX_LENGTH: Final[int] = 200
SUMMARY_RECENT_ENTRIES_COUNT: Final[int] = 10
SUMMARY_VALUE_MAX_LENGTH: Final[int] = 100


def _resolve_namespace(
    tenant_ctx: TenantContext | None,
    user_id: str | None,
) -> str:
    """Return the sub-path namespace for memory storage."""
    if tenant_ctx is not None:
        return tenant_ctx.storage_namespace
    if user_id:
        TenantContext._warn_deprecated_user_id()
        return user_id
    msg = "Either tenant_ctx or user_id must be provided"
    raise ValueError(msg)


def _assert_no_traversal(resolved: Path, base: Path) -> None:
    """Raise ValueError if *resolved* escapes *base*."""
    if not resolved.resolve().is_relative_to(base.resolve()):
        msg = "Path traversal detected in memory path"
        raise ValueError(msg)


class TenantMemory:
    """Persistent key-value memory isolated per tenant+user.

    Stores:
    - User preferences (model, mode, theme)
    - Project context (last init results, tech stacks)
    - Learned patterns (frequently used commands, common queries)
    - Custom notes and bookmarks
    """

    __slots__ = ("_data", "_memory_dir", "_memory_file", "_user_id")

    def __init__(
        self,
        *,
        tenant_ctx: TenantContext | None = None,
        user_id: str | None = None,
        base_dir: Path | None = None,
    ) -> None:
        """Initialize tenant memory.

        Args:
            tenant_ctx: Optional tenant context for scoped storage.
            user_id: Optional user ID (deprecated in favor of tenant_ctx).
            base_dir: Optional base directory for storage.

        """
        namespace = _resolve_namespace(tenant_ctx, user_id)
        self._user_id = tenant_ctx.user_id if tenant_ctx else (user_id or "")

        base = base_dir or (Path.home() / ".aicippy")
        self._memory_dir = base / MEMORY_DIR_NAME / namespace
        self._memory_dir.mkdir(parents=True, exist_ok=True)

        self._memory_file = self._memory_dir / MEMORY_FILE
        _assert_no_traversal(self._memory_file, self._memory_dir)
        self._data: dict[str, Any] = {}

        self._load()
        logger.debug(
            "tenant_memory_initialized",
            user_id=self._user_id,
            namespace=namespace,
            entries=len(self._data.get("entries", {})),
        )

    def _load(self) -> None:
        """Load memory from disk."""
        if self._memory_file.exists():
            try:
                self._data = json.loads(self._memory_file.read_text(encoding="utf-8"))
            except (json.JSONDecodeError, OSError) as e:
                logger.warning("tenant_memory_load_failed", error=str(e))
                self._data = {"entries": {}, "preferences": {}, "context": {}}
        else:
            self._data = {
                "user_id": self._user_id,
                "created_at": datetime.now(UTC).isoformat(),
                "entries": {}, 
                "preferences": {}, 
                "context": {},
            }

    def _save(self) -> None:
        """Persist memory to disk."""
        self._data["updated_at"] = datetime.now(UTC).isoformat()
        try:
            self._memory_file.write_text(
                json.dumps(self._data, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
        except OSError as e:
            logger.warning("tenant_memory_save_failed", error=str(e))

    def set(self, key: str, value: object) -> None:
        """Store a key-value pair in memory."""
        entries = self._data.setdefault("entries", {})
        entries[key] = {
            "value": value,
            "updated_at": datetime.now(UTC).isoformat(),
        }
        # Enforce entry limit (remove oldest)
        if len(entries) > MAX_MEMORY_ENTRIES:
            sorted_keys = sorted(entries, key=lambda k: entries[k].get("updated_at", ""))
            for old_key in sorted_keys[: len(entries) - MAX_MEMORY_ENTRIES]:
                del entries[old_key]
        self._save()

    def get(self, key: str, default: object = None) -> object:
        """Retrieve a value from memory."""
        entry = self._data.get("entries", {}).get(key)
        if entry is None:
            return default
        return entry.get("value", default)

    def delete(self, key: str) -> bool:
        """Remove a key from memory."""
        entries = self._data.get("entries", {})
        if key in entries:
            del entries[key]
            self._save()
            return True
        return False

    def list_keys(self) -> list[str]:
        """List all memory keys."""
        return list(self._data.get("entries", {}).keys())

    def set_preference(self, key: str, value: object) -> None:
        """Store a user preference."""
        self._data.setdefault("preferences", {})[key] = value
        self._save()

    def get_preference(self, key: str, default: object = None) -> object:
        """Get a user preference."""
        return self._data.get("preferences", {}).get(key, default)

    def set_context(self, key: str, value: object) -> None:
        """Store workspace/project context."""
        self._data.setdefault("context", {})[key] = {
            "value": value,
            "updated_at": datetime.now(UTC).isoformat(),
        }
        self._save()

    def get_context(self, key: str, default: object = None) -> object:
        """Get workspace/project context."""
        entry = self._data.get("context", {}).get(key)
        if entry is None:
            return default
        return entry.get("value", default)

    def get_summary(self) -> str:
        """Get a text summary of stored memory for injection into prompts."""
        entries = self._data.get("entries", {})
        prefs = self._data.get("preferences", {})
        ctx = self._data.get("context", {})

        parts = []
        if prefs:
            parts.append("User Preferences: " + ", ".join(f"{k}={v}" for k, v in prefs.items()))
        if ctx:
            for k, v in ctx.items():
                summary_val = json.dumps(v.get('value', ''), default=str)
                parts.append(f"Context [{k}]: {summary_val[:SUMMARY_CONTEXT_MAX_LENGTH]}")
        if entries:
            recent = sorted(
                entries.items(),
                key=lambda x: x[1].get("updated_at", ""),
                reverse=True,
            )[:SUMMARY_RECENT_ENTRIES_COUNT]
            for key, entry in recent:
                val_str = json.dumps(
                    entry.get("value", ""),
                    default=str,
                )[:SUMMARY_VALUE_MAX_LENGTH]
                parts.append(f"Memory [{key}]: {val_str}")

        return "\n".join(parts) if parts else ""

    def clear(self) -> None:
        """Clear all tenant memory."""
        self._data = {
            "user_id": self._user_id,
            "created_at": self._data.get("created_at", datetime.now(UTC).isoformat()),
            "entries": {}, 
            "preferences": {}, 
            "context": {},
        }
        self._save()
        logger.info("tenant_memory_cleared", user_id=self._user_id)


class PlatformBackedTenantMemory:
    """Tenant memory backed by the AiVibe platform API.

    Writes via AppClient (api.aicippy.com) and reads from direct DB
    (if enabled) or API fallback. Falls back to local TenantMemory
    when the platform is unreachable.

    Args:
        tenant_ctx: TenantContext for tenant-scoped storage (preferred).
        user_id: Deprecated — bare user_id for backward compat.
        app_client: AppClient for api.aicippy.com.
        db_reader: Optional TenantDBReader for direct RDS reads.
        base_dir: Base directory for local fallback storage.

    """

    __slots__ = ("_app_client", "_background_tasks", "_db_reader", "_local", "_user_id")

    def __init__(
        self,
        app_client: object,
        *,
        tenant_ctx: TenantContext | None = None,
        user_id: str | None = None,
        db_reader: object | None = None,
        base_dir: Path | None = None,
    ) -> None:
        """Initialize platform backed tenant memory.

        Args:
            app_client: AppClient for platform API interactions.
            tenant_ctx: Optional tenant context for scoped storage.
            user_id: Optional user ID (deprecated).
            db_reader: Optional direct database reader.
            base_dir: Optional base directory for local fallback.

        """
        self._user_id = tenant_ctx.user_id if tenant_ctx else (user_id or "")
        self._app_client = app_client
        self._db_reader = db_reader
        self._background_tasks: set[asyncio.Task[None]] = set()
        # Local fallback always available
        self._local = TenantMemory(
            tenant_ctx=tenant_ctx,
            user_id=user_id,
            base_dir=base_dir,
        )

    def set(self, key: str, value: object) -> None:
        """Store a key-value pair (local + async platform save)."""
        self._local.set(key, value)
        self._save_to_platform_async(key, value)

    def get(self, key: str, default: object = None) -> object:
        """Retrieve a value from memory (local)."""
        return self._local.get(key, default)

    def delete(self, key: str) -> bool:
        """Remove a key from memory."""
        return self._local.delete(key)

    def list_keys(self) -> list[str]:
        """List all memory keys."""
        return self._local.list_keys()

    def set_preference(self, key: str, value: object) -> None:
        """Store a user preference (local + async platform save)."""
        self._local.set_preference(key, value)
        self._save_to_platform_async(f"pref:{key}", value)

    def get_preference(self, key: str, default: object = None) -> object:
        """Get a user preference."""
        return self._local.get_preference(key, default)

    def set_context(self, key: str, value: object) -> None:
        """Store workspace/project context (local + async platform save)."""
        self._local.set_context(key, value)
        self._save_to_platform_async(f"ctx:{key}", value)

    def get_context(self, key: str, default: object = None) -> object:
        """Get workspace/project context."""
        return self._local.get_context(key, default)

    def get_summary(self) -> str:
        """Get a text summary of stored memory for AI prompt injection."""
        return self._local.get_summary()

    def clear(self) -> None:
        """Clear all tenant memory."""
        self._local.clear()

    def _save_to_platform_async(self, key: str, value: object) -> None:
        """Fire-and-forget save to platform API.

        Note: This pattern (create_task + _background_tasks bookkeeping) is
        intentionally duplicated in history.py.  The coroutine signatures
        differ (key/value here vs no args there), so a shared abstraction would
        add complexity without meaningful deduplication.
        """
        try:
            loop = asyncio.get_running_loop()
            task = loop.create_task(self._save_to_platform(key, value))
            # Store reference to prevent garbage collection of the task (RUF006)
            self._background_tasks.add(task)
            task.add_done_callback(self._background_tasks.discard)
        except RuntimeError:
            # No running loop - skip platform save (local already saved)
            logger.warning("platform_save_skipped_no_event_loop")

    async def _save_to_platform(self, key: str, value: object) -> None:
        """Save a memory entry to the platform API."""
        try:
            await self._app_client.save_memory(
                user_id=self._user_id,
                key=key,
                value=value,
            )
        except Exception as e:
            logger.warning("platform_memory_save_failed", key=key, error=str(e))