"""Base class for transforms that need to read the real component catalog.

Some transforms replace ``list_tools()`` output with synthetic components
(e.g. a search interface) while still needing access to the *real*
(auth-filtered) catalog at call time.  ``CatalogTransform`` provides the
bypass machinery so subclasses can call ``get_tool_catalog()`` without
triggering their own replacement logic.

Re-entrancy problem
-------------------

When a synthetic tool handler calls ``get_tool_catalog()``, that calls
``ctx.fastmcp.list_tools()`` which re-enters the transform pipeline —
including *this* transform's ``list_tools()``.  If the subclass overrides
``list_tools()`` directly, the re-entrant call would hit the subclass's
replacement logic again (returning synthetic tools instead of the real
catalog).  A ``super()`` call can't prevent this because Python can't
short-circuit a method after ``super()`` returns.

Solution: ``CatalogTransform`` owns ``list_tools()`` and uses a
per-instance ``ContextVar`` to detect re-entrant calls.  During bypass,
it passes through to the base ``Transform.list_tools()`` (a no-op).
Otherwise, it delegates to ``transform_tools()`` — the subclass hook
where replacement logic lives.  Same pattern for resources, prompts,
and resource templates.

This is *not* the same as the ``Provider._list_tools()`` convention
(which produces raw components with no arguments).  ``transform_tools()``
receives the current catalog and returns a transformed version.  The
distinct name avoids confusion between the two patterns.

Usage::

    class MyTransform(CatalogTransform):
        async def transform_tools(self, tools):
            return [self._make_search_tool()]

        def _make_search_tool(self):
            async def search(ctx: Context = None):
                real_tools = await self.get_tool_catalog(ctx)
                ...
            return Tool.from_function(fn=search, name="search")
"""

from __future__ import annotations

import itertools
from collections.abc import Sequence
from contextvars import ContextVar
from typing import TYPE_CHECKING

from fastmcp.server.transforms import Transform
from fastmcp.utilities.versions import dedupe_with_versions

if TYPE_CHECKING:
    from fastmcp.prompts.base import Prompt
    from fastmcp.resources.base import Resource
    from fastmcp.resources.template import ResourceTemplate
    from fastmcp.server.context import Context
    from fastmcp.tools.base import Tool

_instance_counter = itertools.count()


class CatalogTransform(Transform):
    """Transform that needs access to the real component catalog.

    Subclasses override ``transform_tools()`` / ``transform_resources()``
    / ``transform_prompts()`` / ``transform_resource_templates()``
    instead of the ``list_*()`` methods.  The base class owns
    ``list_*()`` and handles re-entrant bypass automatically — subclasses
    never see re-entrant calls from ``get_*_catalog()``.

    The ``get_*_catalog()`` methods fetch the real (auth-filtered) catalog
    by temporarily setting a bypass flag so that this transform's
    ``list_*()`` passes through without calling the subclass hook.
    """

    def __init__(self) -> None:
        self._instance_id: int = next(_instance_counter)
        self._bypass: ContextVar[bool] = ContextVar(
            f"_catalog_bypass_{self._instance_id}", default=False
        )

    # ------------------------------------------------------------------
    # list_* (bypass-aware — subclasses override transform_* instead)
    # ------------------------------------------------------------------

    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
        if self._bypass.get():
            return await super().list_tools(tools)
        return await self.transform_tools(tools)

    async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
        if self._bypass.get():
            return await super().list_resources(resources)
        return await self.transform_resources(resources)

    async def list_resource_templates(
        self, templates: Sequence[ResourceTemplate]
    ) -> Sequence[ResourceTemplate]:
        if self._bypass.get():
            return await super().list_resource_templates(templates)
        return await self.transform_resource_templates(templates)

    async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
        if self._bypass.get():
            return await super().list_prompts(prompts)
        return await self.transform_prompts(prompts)

    # ------------------------------------------------------------------
    # Subclass hooks (override these, not list_*)
    # ------------------------------------------------------------------

    async def transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
        """Transform the tool catalog.

        Override this method to replace, filter, or augment the tool listing.
        The default implementation passes through unchanged.

        Do NOT override ``list_tools()`` directly — the base class uses it
        to handle re-entrant bypass when ``get_tool_catalog()`` reads the
        real catalog.
        """
        return tools

    async def transform_resources(
        self, resources: Sequence[Resource]
    ) -> Sequence[Resource]:
        """Transform the resource catalog.

        Override this method to replace, filter, or augment the resource listing.
        The default implementation passes through unchanged.

        Do NOT override ``list_resources()`` directly — the base class uses it
        to handle re-entrant bypass when ``get_resource_catalog()`` reads the
        real catalog.
        """
        return resources

    async def transform_resource_templates(
        self, templates: Sequence[ResourceTemplate]
    ) -> Sequence[ResourceTemplate]:
        """Transform the resource template catalog.

        Override this method to replace, filter, or augment the template listing.
        The default implementation passes through unchanged.

        Do NOT override ``list_resource_templates()`` directly — the base class
        uses it to handle re-entrant bypass when
        ``get_resource_template_catalog()`` reads the real catalog.
        """
        return templates

    async def transform_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
        """Transform the prompt catalog.

        Override this method to replace, filter, or augment the prompt listing.
        The default implementation passes through unchanged.

        Do NOT override ``list_prompts()`` directly — the base class uses it
        to handle re-entrant bypass when ``get_prompt_catalog()`` reads the
        real catalog.
        """
        return prompts

    # ------------------------------------------------------------------
    # Catalog accessors
    # ------------------------------------------------------------------

    async def get_tool_catalog(
        self, ctx: Context, *, run_middleware: bool = True
    ) -> Sequence[Tool]:
        """Fetch the real tool catalog, bypassing this transform.

        The result is deduplicated by name so that only the highest version
        of each tool is returned — matching what protocol handlers expose
        on the wire.

        Args:
            ctx: The current request context.
            run_middleware: Whether to run middleware on the inner call.
                Defaults to True because this is typically called from a
                tool handler where list_tools middleware has not yet run.
        """
        token = self._bypass.set(True)
        try:
            tools = await ctx.fastmcp.list_tools(run_middleware=run_middleware)
        finally:
            self._bypass.reset(token)
        return dedupe_with_versions(tools, lambda t: t.name)

    async def get_resource_catalog(
        self, ctx: Context, *, run_middleware: bool = True
    ) -> Sequence[Resource]:
        """Fetch the real resource catalog, bypassing this transform.

        Args:
            ctx: The current request context.
            run_middleware: Whether to run middleware on the inner call.
                Defaults to True because this is typically called from a
                tool handler where list_resources middleware has not yet run.
        """
        token = self._bypass.set(True)
        try:
            return await ctx.fastmcp.list_resources(run_middleware=run_middleware)
        finally:
            self._bypass.reset(token)

    async def get_prompt_catalog(
        self, ctx: Context, *, run_middleware: bool = True
    ) -> Sequence[Prompt]:
        """Fetch the real prompt catalog, bypassing this transform.

        Args:
            ctx: The current request context.
            run_middleware: Whether to run middleware on the inner call.
                Defaults to True because this is typically called from a
                tool handler where list_prompts middleware has not yet run.
        """
        token = self._bypass.set(True)
        try:
            return await ctx.fastmcp.list_prompts(run_middleware=run_middleware)
        finally:
            self._bypass.reset(token)

    async def get_resource_template_catalog(
        self, ctx: Context, *, run_middleware: bool = True
    ) -> Sequence[ResourceTemplate]:
        """Fetch the real resource template catalog, bypassing this transform.

        Args:
            ctx: The current request context.
            run_middleware: Whether to run middleware on the inner call.
                Defaults to True because this is typically called from a
                tool handler where list_resource_templates middleware has
                not yet run.
        """
        token = self._bypass.set(True)
        try:
            return await ctx.fastmcp.list_resource_templates(
                run_middleware=run_middleware
            )
        finally:
            self._bypass.reset(token)
