"""Tool Policy System — controls which tools are available to agent backends.

Profiles define presets of allowed tools. Groups are shorthand for sets of tools.
Explicit allow/deny lists override profiles.

Precedence (highest to lowest):
  1. tools_deny — always wins, blocks even if explicitly allowed
  2. tools_allow — if non-empty, only these tools are available (union with profile)
  3. tool_profile — baseline set of allowed tools

Inspired by OpenClaw's tool-policy.ts.
"""

from __future__ import annotations

import logging
from collections.abc import Sequence

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Tool groups — named sets of related tools
# ---------------------------------------------------------------------------
TOOL_GROUPS: dict[str, list[str]] = {
    "group:fs": ["read_file", "write_file", "edit_file", "list_dir"],
    "group:shell": ["shell"],
    "group:browser": ["browser"],
    "group:memory": ["remember", "recall"],
    "group:desktop": ["desktop"],
}

# ---------------------------------------------------------------------------
# Built-in profiles — from minimal to full
# ---------------------------------------------------------------------------
TOOL_PROFILES: dict[str, dict] = {
    "minimal": {
        "allow": ["group:memory"],
    },
    "coding": {
        "allow": ["group:fs", "group:shell", "group:memory"],
    },
    "full": {},  # No restrictions — everything allowed
}


class ToolPolicy:
    """Evaluates whether a tool is allowed based on profile + allow/deny lists."""

    def __init__(
        self,
        profile: str = "full",
        allow: Sequence[str] | None = None,
        deny: Sequence[str] | None = None,
    ):
        self.profile = profile
        self._allow_raw = list(allow) if allow else []
        self._deny_raw = list(deny) if deny else []

        # Pre-resolve for fast lookups
        self._allowed_set = self._resolve()
        self._denied_set = self._expand_names(self._deny_raw)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def is_tool_allowed(self, tool_name: str) -> bool:
        """Return True if *tool_name* passes the policy."""
        # Deny always wins
        if tool_name in self._denied_set:
            logger.debug("Tool '%s' blocked by deny list", tool_name)
            return False

        # If the profile is 'full' and there's no explicit allow list,
        # everything not denied is allowed.
        if not self._allowed_set:
            return True

        allowed = tool_name in self._allowed_set
        if not allowed:
            logger.debug("Tool '%s' not in allowed set", tool_name)
        return allowed

    def filter_tool_names(self, names: Sequence[str]) -> list[str]:
        """Return only the names that pass the policy."""
        return [n for n in names if self.is_tool_allowed(n)]

    # ------------------------------------------------------------------
    # Resolution helpers
    # ------------------------------------------------------------------

    @staticmethod
    def resolve_profile(profile_name: str) -> set[str]:
        """Expand a profile name into a concrete set of tool names.

        Returns an empty set for the 'full' profile (meaning no restrictions).
        Raises ValueError for unknown profiles.
        """
        if profile_name not in TOOL_PROFILES:
            raise ValueError(
                f"Unknown tool profile '{profile_name}'. Available: {', '.join(TOOL_PROFILES)}"
            )
        cfg = TOOL_PROFILES[profile_name]
        raw = cfg.get("allow", [])
        return ToolPolicy._expand_names(raw)

    @staticmethod
    def _expand_names(raw: Sequence[str]) -> set[str]:
        """Expand a list that may contain group references into tool names."""
        result: set[str] = set()
        for item in raw:
            if item.startswith("group:") and item in TOOL_GROUPS:
                result.update(TOOL_GROUPS[item])
            else:
                result.add(item)
        return result

    def _resolve(self) -> set[str]:
        """Build the final allowed set from profile + explicit allow list."""
        # Start with the profile
        try:
            profile_set = self.resolve_profile(self.profile)
        except ValueError:
            logger.warning("Unknown profile '%s', falling back to 'full'", self.profile)
            profile_set = set()  # full = no restrictions

        # Merge in explicit allow list
        explicit = self._expand_names(self._allow_raw)
        return profile_set | explicit
