"""tool-provisioning strategy protocol.

defines the contract an agent bootstrap delegates tool registration to.
concrete implementations live in the consuming package (e.g.
``<consumer>.runtime.tool_strategies``). the protocol isolates
environment-specific choices -- "do we host tools in-process" vs
"tools register themselves from external pods" -- from the bootstrap
body so the bootstrap stays environment-agnostic.

a strategy owns zero or one :class:`ToolServer`. the agent bootstrap
NEVER constructs a ToolServer directly; it hands the strategy a
:class:`BootstrapContext` carrying the shared NATS connection and
other runtime handles, and the strategy decides whether to start a
ToolServer, which tools to register on it, and when the tool surface
is ready for traffic.

four lifecycle methods:

- :meth:`ToolProvisioningStrategy.provision` wires up tools. called
  exactly once during the ``TOOL_STRATEGY_PROVISIONED`` bootstrap
  phase, after the NATS connection and (if configured) workspace
  runtime are live.
- :meth:`ToolProvisioningStrategy.await_ready` blocks until the tools
  are discoverable through the Registry. in-process strategies that
  register synchronously return immediately; strategies that expect
  external pods poll the Registry for the tool manifest.
- :meth:`ToolProvisioningStrategy.reload_workspace_tools` swaps the
  workspace tool bundle while the agent keeps running. called when
  a developer edits ``workspace.yaml``; strategies that host
  workspace tools atomically deregister the old bundle and register
  the new one through the ToolServer's public helpers. strategies
  that do not host workspace tools (prod-external with no workspace
  block) return immediately.
- :meth:`ToolProvisioningStrategy.teardown` is called during shutdown
  in reverse-phase order. strategies that own a ToolServer stop and
  disconnect it here.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
from uuid import UUID

if TYPE_CHECKING:
    pass


__all__ = [
    "BootstrapContext",
    "ToolProvisioningStrategy",
]


@dataclass(frozen=True)
class BootstrapContext:
    """shared bootstrap handles passed to a tool-provisioning strategy.

    frozen so strategies cannot mutate the bootstrap's state. carries
    only the minimum runtime handles a strategy needs: the NATS
    client (so strategies can reuse the bootstrap's connection rather
    than opening a competing session), the agent identity, the
    namespace prefix for subject construction, and optional runtime
    handles a strategy MAY consume (workspace runtime, registry
    client) when present.

    strategies must NOT open their own NATS connections. the bootstrap
    owns the connection lifecycle; strategies publish and subscribe
    via the supplied ``nats_client``.

    :ivar nats_client: connected NATS client owned by the bootstrap;
        strategies publish / subscribe on this connection rather than
        opening their own
    :ivar agent_id: agent UUID this bootstrap is initializing
    :ivar pod_id: this pod instance's id -- the SAME value the bootstrap
        used as the NATS connect ``client_name`` (used directly for the
        ``agents.internal.{agent_id}.{pod_id}`` / heartbeat / reregister
        subjects, which already lead with the authenticated agent id). a
        strategy that stands up an IN-PROCESS :class:`ToolServer` on the
        agent's own connection (the devx ``DevInProcessStrategy`` builtins,
        the prod ``ProdExternalPodsStrategy`` workspace + ``knowledge_drafts``
        tools) does NOT pass this raw instance id as the tool server's
        pod_id: it composes the ``{agent_id}.{instance}`` routing pod-id
        (:meth:`Subjects.agent_inprocess_pod_id`) so the server's
        ``tools.internal`` / ``tools.probe`` / ``tools.heartbeat`` subjects
        nest under the ``tools.internal.{agent_id}.>`` subtree the agent's
        least-privilege JWT grants -- bound to the AUTHENTICATED agent
        identity, never the spoofable connect-name pod id; a peer-agent or
        freshly-generated id would fall outside the grant and the registry's
        reachability probe would find no responder. ``None`` when the caller
        did not thread it (the ToolServer then generates its own id, matching
        pre-auth behavior)
    :ivar namespace: NATS subject-namespace prefix
    :ivar nats_url: NATS server URL the bootstrap connected with.
        strategies that stand up their own :class:`ToolServer` (which
        opens a dedicated client internally for its own subscription
        lifecycle) need the URL so they connect to the same server.
        the bootstrap-owned :attr:`nats_client` is NOT reused because
        the ToolServer owns subscription ordering its own way
    :ivar bootstrap_token: optional bootstrap token for tool-registry
        authentication; strategies that stand up a ToolServer pass it
        through so the Registry accepts the registration
    :ivar workspace_runtime: optional workspace runtime handle carrying
        the L3 proxy + collection factories used by workspace tools;
        strategies that register workspace tools consume this. None
        when the agent config declares no workspace block
    :ivar registry_client: optional handle for Registry discovery
        polling; strategies that expect tools from external pods use
        this to poll the registry for readiness. None when the
        strategy registers tools in-process
    :ivar acl_cache: shared :class:`threetears.agent.acl.AclCache`
        constructed during the three-tier stack phase. strategies
        that build workspace tools thread this into
        :func:`build_workspace_tools` so every dispatch authorizes
        against one bootstrap-owned cache + loader set. typed ``Any``
        at the strategy-protocol boundary to keep this package free
        of an agent-acl dependency; concrete construction paths pass
        the real :class:`AclCache` instance
    :ivar knowledge_integration: per-pod knowledge integration handle
        (the consuming app's SDK's ``KnowledgeIntegration``) constructed during
        the three-tier stack phase. strategies that own an in-process
        :class:`ToolServer` register the context-bound
        ``knowledge_drafts`` tool against this handle so the agent can
        list / confirm / edit / discard its own correction-harvest
        drafts in conversation (knowledge-task-06, KNW-54). ``None`` when
        the agent has no knowledge layer. typed ``Any`` at the
        strategy-protocol boundary to keep this package free of the
        consuming app's SDK dependency (the concrete ``KnowledgeIntegration``
        lives in the consumer's agent package)
    :ivar namespace_collection: three-tier
        :class:`NamespaceCollection` constructed during the
        three-tier stack phase. strategies that own a ToolServer
        thread this into :class:`ToolServer` so
        :meth:`register_tool` / :meth:`deregister_tool` materialize
        ``platform.namespaces`` rows via
        :meth:`NamespaceCollection.save_entity` /
        :meth:`NamespaceCollection.delete`. strategies that build
        workspace tools thread it into
        :func:`register_workspace_tools` so
        :class:`WorkspaceCreateTool` can emit the paired workspace
        namespace row through the same Collection. typed ``Any`` at
        the strategy-protocol boundary to keep this package free of
        an upstream hub dependency (the concrete
        :class:`NamespaceCollection` lives in the consumer package)
    :ivar context_integration: per-pod conversation-context integration
        handle (the consuming app's SDK's ``ContextIntegration``) constructed
        during the three-tier stack phase. its
        ``get_manager(conversation_id, user_id)`` mints / caches the
        per-conversation :class:`ToolContextManager` the agent graph
        uses. a strategy that owns an in-process :class:`ToolServer` can
        derive a ``context_factory`` from it so conversation-aware
        builtins (e.g. ``context_recall``) resolve the SAME manager the
        graph reads/writes -- without depending on a workspace runtime.
        ``None`` when the agent has no context layer. typed ``Any`` at
        the strategy-protocol boundary to keep this package free of the
        consuming app's SDK dependency (the concrete ``ContextIntegration`` lives
        in the consumer's agent package)
    :ivar engagement_provider: per-pod active-engagement provider handle
        (the consuming app's SDK's ``EngagementProvider``) constructed during the
        three-tier stack phase. a strategy that owns an in-process
        :class:`ToolServer` binds it into the context-bound
        ``set_active_engagement`` tool so an operator can select / clear
        the conversation's active engagement in-conversation; the tool
        writes ``conversations.metadata['active_engagement_id']`` through
        it. ``None`` when the agent has no conversations layer. typed
        ``Any`` at the strategy-protocol boundary to keep this package
        free of the consuming app's SDK dependency (the concrete
        ``EngagementProvider`` lives in the consumer's agent package)
    """

    nats_client: Any
    agent_id: UUID
    namespace: str
    pod_id: str | None = None
    nats_url: str = ""
    bootstrap_token: str | None = None
    workspace_runtime: Any = None
    registry_client: Any = None
    acl_cache: Any = None
    namespace_collection: Any = None
    knowledge_integration: Any = None
    context_integration: Any = None
    engagement_provider: Any = None


@runtime_checkable
class ToolProvisioningStrategy(Protocol):
    """contract a tool-provisioning strategy must satisfy.

    runtime-checkable so bootstrap code can assert
    ``isinstance(obj, ToolProvisioningStrategy)`` at the call site
    before wiring it in. the three methods are called in fixed order
    during an agent lifecycle:

    1. :meth:`provision` during the ``TOOL_STRATEGY_PROVISIONED``
       phase, after ``BACKEND_CONNECTED`` and
       ``WORKSPACE_RUNTIME_READY`` have completed.
    2. :meth:`await_ready` immediately after provision returns,
       blocking until tools are discoverable in the Registry. the
       bootstrap forwards failures so startup aborts before
       handler subscription.
    3. :meth:`teardown` during shutdown, after the handler stops and
       before NATS drain. called in reverse phase order with the
       other teardown steps.

    strategies MAY hold references to TearsTool instances, a
    :class:`ToolServer` they own, or nothing at all (a prod-external
    strategy with no workspace block owns no server). they MUST NOT
    hold references to the NATS client beyond the context handed to
    :meth:`provision` -- reaching for the bootstrap's client without
    going through the context breaks the "one connection per agent"
    invariant.
    """

    async def provision(self, bootstrap_context: BootstrapContext) -> None:
        """wire up tools for the agent.

        called exactly once during the ``TOOL_STRATEGY_PROVISIONED``
        bootstrap phase. strategies that own a :class:`ToolServer`
        should construct it here (reusing the supplied NATS client
        rather than opening a new connection), register their tools,
        and start the server's serve loop. strategies that expect
        tools from external pods may use this phase to record the
        expected manifest or publish a "ready to receive" signal.

        :param bootstrap_context: shared bootstrap handles including
            the live NATS connection and agent identity
        :ptype bootstrap_context: BootstrapContext
        :return: nothing
        :rtype: None
        :raises Exception: any failure propagates to the phase runner
            which aborts bootstrap (fail-fast); handlers have not yet
            subscribed so no external traffic is dropped
        """
        ...

    async def await_ready(self, timeout: float) -> None:
        """block until tools are discoverable through the Registry.

        called immediately after :meth:`provision` returns, before
        the bootstrap enters the ``GRAPH_BUILT`` phase. in-process
        strategies that register tools synchronously may short-circuit
        to an immediate return; external-pod strategies poll the
        Registry's discovery catalog until the expected tools report
        ``status=available``.

        :param timeout: maximum seconds to wait before raising
        :ptype timeout: float
        :return: nothing
        :rtype: None
        :raises TimeoutError: when the timeout elapses without the
            expected tools appearing in the Registry; the message
            names the missing tools so the operator knows which
            external pod to investigate
        """
        ...

    async def reload_workspace_tools(
        self,
        workspace_runtime: Any,
        workspace_config: Any,
    ) -> None:
        """swap the workspace tool bundle without stopping the agent.

        called when a developer edits ``workspace.yaml`` (or the
        operator applies a hot workspace-config change). strategies
        that host workspace tools deregister the old bundle and
        register the new one through the :class:`ToolServer`
        public ``deregister_tool`` / ``register_tool`` helpers,
        which each publish an updated manifest atomically.
        strategies that do not host workspace tools return
        immediately.

        :param workspace_runtime: live workspace runtime handle
            (context factory, L3 proxy, collection factories) for
            the new bundle. caller is responsible for the runtime
            lifecycle
        :ptype workspace_runtime: Any
        :param workspace_config: new workspace configuration
            declaring the tool allow-list + bind roots for the
            reloaded bundle
        :ptype workspace_config: Any
        :return: nothing
        :rtype: None
        :raises RuntimeError: when a strategy that supports
            workspace tools is not in a state that permits reload
            (e.g. :meth:`provision` has not run, or teardown has
            already stopped the ToolServer)
        """
        ...

    async def teardown(self) -> None:
        """release any resources the strategy acquired.

        called during agent shutdown in reverse phase order. strategies
        that own a :class:`ToolServer` call ``shutdown()`` on it here.
        strategies with no owned resources return immediately.

        :return: nothing
        :rtype: None
        """
        ...
