"""Built-in tool for switching the active agent profile mid-session."""

from navi.tools._internal.base import Tool, ToolContext, ToolResult, current_event_sink, current_session_id

# Tool names are listed in the result so the model knows what it just gained and
# what it must stop calling. Long MCP-heavy profiles get truncated; list_tools
# is the full view.
_MAX_LISTED = 30


def _shorten(names: list[str]) -> str:
    shown = ", ".join(names[:_MAX_LISTED])
    if len(names) > _MAX_LISTED:
        shown += f", … (+{len(names) - _MAX_LISTED} more)"
    return shown


class SwitchProfileTool(Tool):
    name = "switch_profile"
    description = (
        "Switch this session to a different agent profile. "
        "Call this on your own judgment when the task fits another profile's specialization — "
        "do not ask the user for permission first. "
        "The new profile's system prompt and tools are in force from the next step of this same turn. "
        "Call it on its own: a tool called in the same batch still runs against the old toolset. "
        "After switching, briefly inform the user: which profile is now active and why you switched."
    )
    parameters = {
        "type": "object",
        "properties": {
            "profile_id": {
                "type": "string",
                "description": "ID of the profile to switch to.",
            }
        },
        "required": ["profile_id"],
    }

    def __init__(
        self,
        session_store,
        profile_registry,
        tool_registry=None,
        mcp_manager=None,
    ) -> None:
        self._sessions = session_store
        self._profiles = profile_registry
        self._tools = tool_registry
        self._mcp_manager = mcp_manager

    def _tool_names(self, profile) -> list[str] | None:
        """Tool names a profile resolves to, or None when no registry is wired."""
        if self._tools is None:
            return None
        from navi.core.tool_utils import build_tool_list

        try:
            scope = profile.get_agent_tools()
            tools = build_tool_list(scope.native, scope.mcp, self._tools, self._mcp_manager)
        except Exception:
            return None
        return [t.name for t in tools]

    def _tool_delta(self, before_id: str, after_profile) -> tuple[list[str], list[str], int | None]:
        """(gained, lost, total) for the switch, or ([], [], None) if unresolved."""
        after = self._tool_names(after_profile)
        if after is None:
            return [], [], None
        try:
            before = self._tool_names(self._profiles.get(before_id)) or []
        except Exception:
            before = []
        after_set = set(after)
        before_set = set(before)
        return (
            [n for n in after if n not in before_set],
            [n for n in before if n not in after_set],
            len(after),
        )

    async def execute(self, params: dict, ctx: ToolContext | None = None) -> ToolResult:
        profile_id = (params.get("profile_id") or "").strip()
        visible = [p for p in self._profiles.all() if not getattr(p, "is_hidden", False)]
        available = ", ".join(p.id for p in visible)

        try:
            profile = self._profiles.get(profile_id)
        except Exception:
            return ToolResult(
                success=False,
                output="",
                error=f"Profile '{profile_id}' not found. Available: {available}",
            )

        if getattr(profile, "is_hidden", False):
            return ToolResult(
                success=False,
                output=f"Profile '{profile_id}' is an internal role and not switchable. Available: {available}",
                error="hidden_profile",
            )

        if getattr(profile, "is_subagent_only", False):
            return ToolResult(
                success=False,
                output=f"Profile '{profile_id}' is a sub-agent specialist and cannot be switched to directly. "
                       f"Use spawn_agent(profile_id='{profile_id}') to delegate tasks to it.",
                error="subagent_only",
            )

        sid = ctx.session_id if ctx else current_session_id.get()
        if not sid:
            return ToolResult(success=False, output="", error="No active session context.")

        session = await self._sessions.get(sid)
        if session is None:
            return ToolResult(success=False, output="", error="Session not found.")

        if session.profile_id == profile_id:
            return ToolResult(
                success=True,
                output=f"Already on profile '{profile.name}' — no change.",
            )

        # Read the profile we are leaving before the repoint: for a pending
        # session set_profile() mutates this very object.
        previous_id = session.profile_id

        # Narrow UPDATE — deliberately not save(). This tool runs in the middle of
        # an agent turn that holds its own Session object; saving the copy we just
        # loaded would write to the session behind that turn's back and hand out
        # sequence numbers it is still claiming. The run picks the new profile up
        # after the turn (agent.py: profile_reloaded).
        await self._sessions.set_profile(sid, profile_id)

        # Notify the client immediately so it can update the UI (profile badge).
        # tool_ctx.event_sink is None inside the agent loop — the ContextVar is the
        # real channel — so fall through to it whenever ctx carries no sink.
        sink = ctx.event_sink if (ctx and ctx.event_sink) else current_event_sink.get()
        if sink is not None:
            from navi.core.events import ProfileSwitched
            await sink.put(ProfileSwitched(profile_id=profile_id, profile_name=profile.name))

        gained, lost, total = self._tool_delta(previous_id, profile)
        lines = [
            f"Switched to profile '{profile.name}' ({profile_id}). "
            + "Its system prompt and tools are in force from the next step of this turn."
        ]
        if total is not None:
            lines.append(f"Tools available now: {total}.")
            if gained:
                lines.append(f"Newly available: {_shorten(gained)}.")
            if lost:
                lines.append(
                    f"No longer available: {_shorten(lost)} — calling one of these now fails "
                    f"with 'tool not found'."
                )
            if not gained and not lost:
                lines.append("The available toolset is unchanged.")
        return ToolResult(success=True, output=" ".join(lines))
