"""Persistent conversation history per logged-in user.

Stores conversation history as JSON files in
``~/.aicippy/conversations/{tenant_id}/{user_id}/``
Each session gets its own file. History is loaded on login and saved after
each message.

When a ``TenantContext`` is provided, storage is isolated per tenant.
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 TYPE_CHECKING, Any, Final

from aicippy.platform.tenant_context import TenantContext

if TYPE_CHECKING:
    pass

from aicippy.utils.logging import get_logger

logger = get_logger(__name__)

MAX_HISTORY_MESSAGES: Final[int] = 200
MAX_CONTEXT_MESSAGES: Final[int] = 80  # Keep last 80 messages for context window
MAX_SESSIONS_RETAINED: Final[int] = 50
HISTORY_DIR_NAME: Final[str] = "conversations"


def _resolve_namespace(
    tenant_ctx: TenantContext | None,
    user_id: str | None,
) -> str:
    """Return the sub-path namespace for history storage.

    With a ``TenantContext`` the namespace is ``{tenant_id}/{user_id}``.
    With a bare ``user_id`` (deprecated) it falls back to ``{user_id}``.
    """
    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 session_id"
        raise ValueError(msg)


class ConversationHistory:
    """Persistent conversation history manager per user.

    Stores conversations in
    ``~/.aicippy/conversations/{tenant_id}/{user_id}/{session_id}.json``
    Provides context window for model invocation from recent messages.
    """

    __slots__ = (
        "_history_dir",
        "_messages",
        "_session_file",
        "_session_id",
        "_user_id",
    )

    def __init__(  # noqa: D107
        self,
        session_id: str,
        *,
        tenant_ctx: TenantContext | None = None,
        user_id: str | None = None,
        base_dir: Path | None = None,
    ) -> None:
        namespace = _resolve_namespace(tenant_ctx, user_id)
        self._user_id = tenant_ctx.user_id if tenant_ctx else (user_id or "")
        self._session_id = session_id

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

        self._session_file = self._history_dir / f"{session_id}.json"
        _assert_no_traversal(self._session_file, self._history_dir)
        self._messages: list[dict[str, Any]] = []

        self._load()
        logger.debug(
            "conversation_history_initialized",
            user_id=self._user_id,
            session_id=session_id,
            namespace=namespace,
            messages_loaded=len(self._messages),
        )

    def _load(self) -> None:
        """Load existing session history from disk."""
        _assert_no_traversal(self._session_file, self._history_dir)
        if self._session_file.exists():
            try:
                data = json.loads(self._session_file.read_text(encoding="utf-8"))
                self._messages = data.get("messages", [])
            except (json.JSONDecodeError, OSError) as e:
                logger.warning("history_load_failed", error=str(e))
                self._messages = []

    def _save(self) -> None:
        """Persist conversation to disk."""
        data = {
            "user_id": self._user_id,
            "session_id": self._session_id,
            "updated_at": datetime.now(UTC).isoformat(),
            "message_count": len(self._messages),
            "messages": self._messages[-MAX_HISTORY_MESSAGES:],
        }
        try:
            self._session_file.write_text(
                json.dumps(data, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
        except OSError as e:
            logger.warning("history_save_failed", error=str(e))

    def add_user_message(self, content: str) -> None:
        """Record a user message."""
        self._messages.append(
            {
                "role": "user",
                "content": content,
                "timestamp": datetime.now(UTC).isoformat(),
            },
        )
        self._save()

    def add_assistant_message(self, content: str) -> None:
        """Record an assistant response."""
        self._messages.append(
            {
                "role": "assistant",
                "content": content,
                "timestamp": datetime.now(UTC).isoformat(),
            },
        )
        self._save()

    def get_context_messages(self, limit: int = MAX_CONTEXT_MESSAGES) -> list[dict[str, Any]]:
        """Get recent messages formatted for model context.

        Returns messages in the format expected by Claude/Llama message arrays.
        Only includes role and content (no timestamps) for model consumption.
        """
        recent = self._messages[-limit:]
        return [{"role": msg["role"], "content": msg["content"]} for msg in recent]

    def get_all_messages(self) -> list[dict[str, Any]]:
        """Get all messages in this session."""
        return list(self._messages)

    def get_recent(self, count: int = 10) -> list[dict[str, Any]]:
        """Get the most recent N messages for display."""
        return self._messages[-count:]

    def clear(self) -> None:
        """Clear current session history."""
        self._messages.clear()
        self._save()
        logger.info("history_cleared", session_id=self._session_id)

    def list_sessions(self) -> list[dict[str, Any]]:
        """List all previous sessions for this user."""
        sessions = []
        for f in sorted(self._history_dir.glob("*.json"), reverse=True)[:MAX_SESSIONS_RETAINED]:
            try:
                data = json.loads(f.read_text(encoding="utf-8"))
                sessions.append(
                    {
                        "session_id": data.get("session_id", f.stem),
                        "updated_at": data.get("updated_at", ""),
                        "message_count": data.get("message_count", 0),
                    },
                )
            except (json.JSONDecodeError, OSError):
                continue
        return sessions

    def load_session(self, session_id: str) -> list[dict[str, Any]]:
        """Load messages from a specific previous session."""
        session_file = self._history_dir / f"{session_id}.json"
        _assert_no_traversal(session_file, self._history_dir)
        if not session_file.exists():
            return []
        try:
            data = json.loads(session_file.read_text(encoding="utf-8"))
            return data.get("messages", [])
        except (json.JSONDecodeError, OSError):
            return []

    @property
    def message_count(self) -> int:
        """Number of messages in current session."""
        return len(self._messages)

    def cleanup_old_sessions(self) -> int:
        """Remove sessions beyond retention limit. Returns count removed."""
        all_files = sorted(
            self._history_dir.glob("*.json"),
            key=lambda f: f.stat().st_mtime,
            reverse=True,
        )
        removed = 0
        for f in all_files[MAX_SESSIONS_RETAINED:]:
            f.unlink(missing_ok=True)
            removed += 1
        if removed:
            logger.info("old_sessions_cleaned", removed=removed)
        return removed


class PlatformBackedHistory:
    """Conversation history backed by the AiVibe platform API.

    Saves conversations via AppClient.save_conversation() and retrieves
    via direct DB (if enabled) or API fallback.
    Falls back to local ConversationHistory when platform is unreachable.

    Args:
        session_id: Current session identifier.
        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",
        "_messages",
        "_session_id",
        "_user_id",
    )

    def __init__(  # noqa: D107, PLR0913
        self,
        session_id: str,
        app_client: Any,  # noqa: ANN401
        *,
        tenant_ctx: TenantContext | None = None,
        user_id: str | None = None,
        db_reader: Any = None,  # noqa: ANN401
        base_dir: Path | None = None,
    ) -> None:
        self._user_id = tenant_ctx.user_id if tenant_ctx else (user_id or "")
        self._session_id = session_id
        self._app_client = app_client
        self._db_reader = db_reader
        self._background_tasks: set[asyncio.Task[None]] = set()
        # Local fallback always available
        self._local = ConversationHistory(
            session_id=session_id,
            tenant_ctx=tenant_ctx,
            user_id=user_id,
            base_dir=base_dir,
        )
        self._messages: list[dict[str, Any]] = list(self._local._messages)

    def add_user_message(self, content: str) -> None:
        """Record a user message (local + async platform save)."""
        msg = {
            "role": "user",
            "content": content,
            "timestamp": datetime.now(UTC).isoformat(),
        }
        self._messages.append(msg)
        self._local.add_user_message(content)
        self._save_to_platform_async()

    def add_assistant_message(self, content: str) -> None:
        """Record an assistant response (local + async platform save)."""
        msg = {
            "role": "assistant",
            "content": content,
            "timestamp": datetime.now(UTC).isoformat(),
        }
        self._messages.append(msg)
        self._local.add_assistant_message(content)
        self._save_to_platform_async()

    def _save_to_platform_async(self) -> None:
        """Fire-and-forget save to platform API.

        Note: This pattern (create_task + _background_tasks bookkeeping) is
        intentionally duplicated in tenant_memory.py.  The coroutine signatures
        differ (no args here vs key/value 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())
            # 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) -> None:
        """Save current conversation to the platform API."""
        try:
            await self._app_client.save_conversation(
                user_id=self._user_id,
                session_id=self._session_id,
                messages=self._messages[-MAX_HISTORY_MESSAGES:],
            )
        except Exception as e:
            logger.warning("platform_conversation_save_failed", error=str(e))

    def get_context_messages(self, limit: int = MAX_CONTEXT_MESSAGES) -> list[dict[str, Any]]:
        """Get recent messages for model context."""
        return self._local.get_context_messages(limit)

    def get_all_messages(self) -> list[dict[str, Any]]:
        """Get all messages in this session."""
        return self._local.get_all_messages()

    def get_recent(self, count: int = 10) -> list[dict[str, Any]]:
        """Get the most recent N messages for display."""
        return self._local.get_recent(count)

    def clear(self) -> None:
        """Clear current session history."""
        self._messages.clear()
        self._local.clear()

    def list_sessions(self) -> list[dict[str, Any]]:
        """List all previous sessions."""
        return self._local.list_sessions()

    def load_session(self, session_id: str) -> list[dict[str, Any]]:
        """Load messages from a specific previous session."""
        return self._local.load_session(session_id)

    @property
    def message_count(self) -> int:
        """Number of messages in current session."""
        return self._local.message_count

    def cleanup_old_sessions(self) -> int:
        """Remove sessions beyond retention limit."""
        return self._local.cleanup_old_sessions()
