"""RunState — per-execution mutable state.

Created fresh for every ``PathwayVM.execute()`` call by ``Agent.turn()``.
Contains per-turn identity, execution defaults, working state mutated
by tools, and ephemeral VM internals.

Execution defaults (default_model, default_tools, etc.) are set by the
Agent before pathway execution. Pathway nodes read these via
``ctx.run.default_model`` etc. — they never reach into AgentDefinition.
"""

from collections.abc import Callable
from typing import Any

from pydantic import BaseModel, Field, PrivateAttr

from core.builders.pathway import PathwayBuilder
from core.pathways.models.tool import RegisteredTool
from core.pathways.session import PathwaySession
from core.types.agents import ModelRouting
from core.types.state import ThreadState
from core.types.stream import StreamEvent


class RunState(BaseModel):
    """Mutable state scoped to a single pathway execution.

    Nodes and tools access this via ``ctx.run.<field>``.
    A fresh ``RunState`` is created for each ``vm.execute()`` call.
    """

    model_config = {"arbitrary_types_allowed": True}

    # -- Execution defaults (set by Agent before pathway execution) --
    system_prompt: str | None = None
    default_model: str = "reasoning"
    default_tools: list[str] = Field(default_factory=list)
    default_temperature: float = 0.7
    max_steps: int = 10
    model_routing: ModelRouting | None = None

    # -- Per-turn identity (set by Agent) --
    agent_id: str | None = None
    thread_id: str | None = None

    # -- Per-execution identifiers (set by PathwayVM at execute time) --
    pathway_id: str = ""
    execution_id: str = ""

    # -- Policy context --
    policy_approved: bool = False
    policy_actor_id: str | None = None
    agent_skills: list[str] | None = None
    workspace_root: str | None = None
    workspace_id: str | None = None
    workspace_path: str | None = None

    # -- Thread state (tools read/write working memory) --
    state: ThreadState | None = None

    # -- Working state (pathway builder tools) --
    working_builders: dict[str, PathwayBuilder] | None = None
    current_pathway_id: str | None = None
    registered_tools: dict[str, RegisteredTool] | None = None

    # -- VM internals (ephemeral, set/cleared per node) --
    _current_node_id: str | None = PrivateAttr(default=None)
    _session: PathwaySession | None = PrivateAttr(default=None)
    _code_namespace: dict[str, Any] = PrivateAttr(default_factory=dict)
    _tools_wrapped: bool = PrivateAttr(default=False)

    # -- Streaming callback (set by Session.stream()) --
    _stream_callback: Callable[[StreamEvent], None] | None = PrivateAttr(default=None)

    # -- Observability --
    event_buffer: list[object] = Field(default_factory=list)

    # -- Public accessors for private VM internals --

    def get_code_namespace(self) -> dict[str, Any]:
        """Return the mutable code namespace (for ``code.execute`` tool)."""
        return self._code_namespace

    def update_code_namespace(self, updates: dict[str, Any]) -> None:
        """Merge new bindings into the code namespace."""
        self._code_namespace.update(updates)

    # -- Streaming --

    def set_stream_callback(self, callback: Callable[[StreamEvent], None] | None) -> None:
        """Set a callback invoked for each stream event (tokens, tool calls)."""
        self._stream_callback = callback

    def emit_stream_event(self, event: StreamEvent) -> None:
        """Emit a stream event if a callback is registered."""
        if self._stream_callback is not None:
            self._stream_callback(event)

    @property
    def is_streaming(self) -> bool:
        """Return True when a stream callback is registered."""
        return self._stream_callback is not None


__all__ = ["RunState"]
