"""
Thread-safe holder for current CatalogSnapshot and ToolContextSnapshot (TZ FR-6, NFR-1, §8.2).

All read and write access is protected by a mutex. Readers observe either the previous
or the new complete snapshot pair; no partial update. Writer builds snapshots off-lock
and swaps them atomically in one critical section. This module only stores and returns
snapshots passed to swap(); it does not build them.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

import threading
from typing import Optional

from mcp_proxy_adapter.core.orchestration.catalog_snapshot import CatalogSnapshot
from mcp_proxy_adapter.core.orchestration.tool_context_snapshot import (
    ToolContextSnapshot,
)


class SnapshotHolder:
    """
    Thread-safe holder for current catalog and tool-context snapshots (TZ FR-6, NFR-1).

    Holds at most one CatalogSnapshot and one ToolContextSnapshot, both replaced
    atomically by swap(). All get/swap operations use the same mutex so that no
    reader can see a new catalog with an old tool context (version alignment).
    Initial state: no snapshots (None). Uses threading.RLock for sync mutex;
    suitable for use from a background thread (writer) and request handlers (readers).
    """

    def __init__(self) -> None:
        """Initialize holder with no snapshots and an RLock for mutex-protected access."""
        self._lock: threading.RLock = threading.RLock()
        self._catalog: Optional[CatalogSnapshot] = None
        self._tool_context: Optional[ToolContextSnapshot] = None

    def get_catalog(self) -> Optional[CatalogSnapshot]:
        """
        Return the current catalog snapshot under mutex.

        Returns None if no snapshot has been swapped yet. Callers must not mutate
        the returned snapshot; it is immutable.
        """
        with self._lock:
            return self._catalog

    def get_tool_context(self) -> Optional[ToolContextSnapshot]:
        """
        Return the current tool context snapshot under mutex.

        Returns None if no snapshot has been swapped yet. Callers must not mutate
        the returned snapshot; it is immutable.
        """
        with self._lock:
            return self._tool_context

    def get_version(self) -> Optional[int]:
        """
        Return the version of the current snapshot pair under mutex.

        Uses catalog version if present, else tool context version. Returns None
        if neither snapshot is set.
        """
        with self._lock:
            if self._catalog is not None:
                return self._catalog.version
            if self._tool_context is not None:
                return self._tool_context.get_version()
            return None

    def swap(
        self,
        catalog_snapshot: Optional[CatalogSnapshot],
        tool_context_snapshot: Optional[ToolContextSnapshot],
    ) -> None:
        """
        Atomically replace the current catalog and tool context snapshots (TZ §8.2).

        Both references are updated in one critical section so that readers never
        see a new catalog with an old tool context. Caller is responsible for
        passing a version-aligned pair (same version for both). Building of
        snapshots must be done off-lock; this method only performs the swap.
        """
        with self._lock:
            self._catalog = catalog_snapshot
            self._tool_context = tool_context_snapshot
