# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/schemas.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

ContextForge Schema Definitions.
This module provides Pydantic models for request/response validation in ContextForge.
It implements schemas for:
- Tool registration and invocation
- Resource management and subscriptions
- Prompt templates and arguments
- Gateway federation
- RPC message formats
- Event messages
- Admin interface

The schemas ensure proper validation according to the MCP specification while adding
gateway-specific extensions for federation support.
"""

# Standard
import base64
from datetime import datetime, timezone
from enum import Enum
import logging
import re
from typing import Any, Dict, List, Literal, Optional, Pattern, Self, Union
from urllib.parse import urlparse

# Third-Party
import orjson
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, EmailStr, Field, field_serializer, field_validator, model_serializer, model_validator, SecretStr, ValidationInfo

# First-Party
from mcpgateway.common.models import Annotations, ImageContent
from mcpgateway.common.models import Prompt as MCPPrompt
from mcpgateway.common.models import Resource as MCPResource
from mcpgateway.common.models import ResourceContent, TextContent
from mcpgateway.common.models import Tool as MCPTool
from mcpgateway.common.oauth import OAUTH_SENSITIVE_KEYS
from mcpgateway.common.validators import SecurityValidator, validate_core_url
from mcpgateway.config import settings
from mcpgateway.utils.base_models import BaseModelWithConfigDict
from mcpgateway.utils.services_auth import decode_auth, encode_auth
from mcpgateway.validation.tags import validate_tags_field

logger = logging.getLogger(__name__)


# ============================================================================
# Shared validation helpers
# ============================================================================


def _validate_association_ids(v: Any, field_name: str = "associated IDs") -> Any:
    """Validate and normalize server association IDs (tools, resources, prompts, agents).

    Accepts a comma-separated string or a list of strings, validates each item
    as a UUID using SecurityValidator, and returns the normalized hex form.
    Empty or None items are silently skipped.

    Args:
        v: Input string or list of IDs.
        field_name: Human-readable name of the association field (for errors).

    Returns:
        List of validated, normalized UUID strings, or the original value.

    Raises:
        ValueError: If any item is not a valid UUID.
    """
    if isinstance(v, str):
        v = [item.strip() for item in v.split(",") if item.strip()]
    if isinstance(v, list):
        validated: list[str] = []
        for item in v:
            if not item:
                continue
            item_str = str(item).strip()
            if not item_str:
                continue
            try:
                validated.append(SecurityValidator.validate_uuid(item_str, field_name))
            except ValueError:
                raise ValueError(f"Invalid ID format: '{item_str}'. " f"{field_name} must contain UUID values, not names. " f"Use UUIDs from the respective entity listings.")
        return validated
    return v


# ============================================================================
# Precompiled regex patterns (compiled once at module load for performance)
# ============================================================================
# Note: Only truly static patterns are precompiled here. Settings-based patterns
# (e.g., from settings.* or SecurityValidator.*) are NOT precompiled because tests
# override class/settings attributes at runtime via monkeypatch.
_HOSTNAME_RE: Pattern[str] = re.compile(r"^(https?://)?([a-zA-Z0-9.-]+)(:[0-9]+)?$")
_SLUG_RE: Pattern[str] = re.compile(r"^[a-z0-9-]+$")

_VALID_VISIBILITY = {"private", "team", "public"}

_MAX_MAPPING_ENTRIES = 50
_MAX_MAPPING_KEY_LENGTH = 128

_VALID_HTTP_HEADER_NAME = re.compile(r"^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$")
_BLOCKED_HEADER_MAPPING_TARGETS = frozenset(
    name.lower()
    for name in (
        "authorization",
        "proxy-authorization",
        "cookie",
        "set-cookie",
        "host",
        "transfer-encoding",
        "content-length",
        "connection",
        "upgrade",
    )
)
_SENSITIVE_HEADER_MAPPING_PATTERNS = (
    re.compile(r"^x-api-key$", re.IGNORECASE),
    re.compile(r"^api-key$", re.IGNORECASE),
    re.compile(r"^apikey$", re.IGNORECASE),
    re.compile(r"^x-(?:auth|api|access|refresh|client|bearer|session|security)[-_]?(?:token|secret|key)$", re.IGNORECASE),
    re.compile(r"^(?:auth|api|access|refresh|client|bearer|session|security)[-_]?(?:token|secret|key)$", re.IGNORECASE),
)


def _validate_oauth_config_urls(v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
    """Validate URL-bearing OAuth config entries against core URL/SSRF rules.

    Applies only to known outbound or redirect endpoints that the gateway may
    later contact or surface to a client. This closes the gap where
    ``oauth_config["token_url"]`` previously bypassed [`validate_core_url`](mcpgateway/common/validators.py:1939).

    Args:
        v: OAuth configuration dict or ``None``.

    Returns:
        The original dict when valid.

    Raises:
        ValueError: If a URL-bearing field is not a string or fails validation.
    """
    if v is None:
        return v
    if not isinstance(v, dict):
        raise ValueError("oauth_config must be an object")
    for field_name in ("token_url", "authorization_url", "issuer", "authorization_server"):
        raw_value = v.get(field_name)
        if raw_value in (None, ""):
            continue
        if not isinstance(raw_value, str):
            raise ValueError(f"oauth_config.{field_name} must be a string URL")
        validate_core_url(raw_value, f"OAuth config {field_name}")
    raw_servers = v.get("authorization_servers")
    if raw_servers in (None, ""):
        return v
    if not isinstance(raw_servers, list):
        raise ValueError("oauth_config.authorization_servers must be a list of URLs")
    for idx, server in enumerate(raw_servers):
        if not isinstance(server, str):
            raise ValueError(f"oauth_config.authorization_servers[{idx}] must be a string URL")
        validate_core_url(server, f"OAuth config authorization_servers[{idx}]")
    return v


def _validate_mapping_size(v: dict | None) -> dict | None:
    """Validate that a mapping dict does not exceed size limits.

    Shared by ToolCreate and ToolUpdate field validators.
    """
    if v is None:
        return v
    if len(v) > _MAX_MAPPING_ENTRIES:
        raise ValueError(f"Mapping must not contain more than {_MAX_MAPPING_ENTRIES} entries")
    for k, val in v.items():
        if len(k) > _MAX_MAPPING_KEY_LENGTH:
            raise ValueError(f"Mapping key exceeds {_MAX_MAPPING_KEY_LENGTH} characters: '{k[:32]}...'")
        if len(val) > _MAX_MAPPING_KEY_LENGTH:
            raise ValueError(f"Mapping value exceeds {_MAX_MAPPING_KEY_LENGTH} characters: '{val[:32]}...'")
    return v


def _validate_header_mapping_targets(v: dict | None) -> dict | None:
    """Validate that header_mapping target names are safe and well-formed.

    Rejects sensitive headers (Authorization, Cookie, Host, etc.) and
    names that violate RFC 7230 token syntax. Applied at registration time;
    tool_service applies the same checks at invocation as defense-in-depth.
    """
    if v is None:
        return v
    for target in v.values():
        if target.strip().lower() in _BLOCKED_HEADER_MAPPING_TARGETS:
            raise ValueError(f"header_mapping targets blocked header {repr(target[:64])}")
        if any(p.match(target) for p in _SENSITIVE_HEADER_MAPPING_PATTERNS):
            raise ValueError(f"header_mapping targets sensitive header {repr(target[:64])}")
        if not _VALID_HTTP_HEADER_NAME.match(target):
            raise ValueError(f"header_mapping contains invalid header name {repr(target[:64])}")
    return v


def _coerce_visibility(v: Optional[str]) -> Optional[str]:
    """Normalize legacy visibility values in Read/response schemas.

    DB columns are unconstrained strings, so historical rows may contain
    values outside the Literal enum.  Coerce them to 'public' (the DB
    default) instead of letting Pydantic raise a ValidationError on the
    read path.

    Args:
        v: Visibility value to normalize.

    Returns:
        The original value if valid, 'public' if invalid, or None if None.
    """
    if v is not None and v not in _VALID_VISIBILITY:
        logger.warning("Coercing invalid visibility value %r to 'public'", v)
        return "public"
    return v


def encode_datetime(v: datetime) -> str:
    """
    Convert a datetime object to an ISO 8601 formatted string.

    Args:
        v (datetime): The datetime object to be encoded.

    Returns:
        str: The ISO 8601 formatted string representation of the datetime object.

    Examples:
        >>> from datetime import datetime, timezone
        >>> encode_datetime(datetime(2023, 5, 22, 14, 30, 0))
        '2023-05-22T14:30:00'
        >>> encode_datetime(datetime(2024, 12, 25, 9, 15, 30))
        '2024-12-25T09:15:30'
        >>> encode_datetime(datetime(2025, 1, 1, 0, 0, 0))
        '2025-01-01T00:00:00'
        >>> # Test with timezone
        >>> dt_utc = datetime(2023, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
        >>> encode_datetime(dt_utc)
        '2023-06-15T12:00:00+00:00'
        >>> # Test microseconds
        >>> dt_micro = datetime(2023, 7, 20, 16, 45, 30, 123456)
        >>> encode_datetime(dt_micro)
        '2023-07-20T16:45:30.123456'
    """
    return v.isoformat()


# --- Metrics Schemas ---


class ToolMetrics(BaseModelWithConfigDict):
    """
    Represents the performance and execution statistics for a tool.

    Attributes:
        total_executions (int): Total number of tool invocations.
        successful_executions (int): Number of successful tool invocations.
        failed_executions (int): Number of failed tool invocations.
        failure_rate (float): Failure rate (failed invocations / total invocations).
        min_response_time (Optional[float]): Minimum response time in seconds.
        max_response_time (Optional[float]): Maximum response time in seconds.
        avg_response_time (Optional[float]): Average response time in seconds.
        last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.

    Examples:
        >>> from datetime import datetime
        >>> metrics = ToolMetrics(
        ...     total_executions=100,
        ...     successful_executions=95,
        ...     failed_executions=5,
        ...     failure_rate=0.05,
        ...     min_response_time=0.1,
        ...     max_response_time=2.5,
        ...     avg_response_time=0.8
        ... )
        >>> metrics.total_executions
        100
        >>> metrics.failure_rate
        0.05
        >>> metrics.successful_executions + metrics.failed_executions == metrics.total_executions
        True
        >>> # Test with minimal data
        >>> minimal_metrics = ToolMetrics(
        ...     total_executions=10,
        ...     successful_executions=8,
        ...     failed_executions=2,
        ...     failure_rate=0.2
        ... )
        >>> minimal_metrics.min_response_time is None
        True
        >>> # Test model dump functionality
        >>> data = metrics.model_dump()
        >>> isinstance(data, dict)
        True
        >>> data['total_executions']
        100
    """

    total_executions: int = Field(..., description="Total number of tool invocations")
    successful_executions: int = Field(..., description="Number of successful tool invocations")
    failed_executions: int = Field(..., description="Number of failed tool invocations")
    failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
    min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
    max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
    avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
    last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")


class ResourceMetrics(BaseModelWithConfigDict):
    """
    Represents the performance and execution statistics for a resource.

    Attributes:
        total_executions (int): Total number of resource invocations.
        successful_executions (int): Number of successful resource invocations.
        failed_executions (int): Number of failed resource invocations.
        failure_rate (float): Failure rate (failed invocations / total invocations).
        min_response_time (Optional[float]): Minimum response time in seconds.
        max_response_time (Optional[float]): Maximum response time in seconds.
        avg_response_time (Optional[float]): Average response time in seconds.
        last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.
    """

    total_executions: int = Field(..., description="Total number of resource invocations")
    successful_executions: int = Field(..., description="Number of successful resource invocations")
    failed_executions: int = Field(..., description="Number of failed resource invocations")
    failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
    min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
    max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
    avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
    last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")


class ServerMetrics(BaseModelWithConfigDict):
    """
    Represents the performance and execution statistics for a server.

    Attributes:
        total_executions (int): Total number of server invocations.
        successful_executions (int): Number of successful server invocations.
        failed_executions (int): Number of failed server invocations.
        failure_rate (float): Failure rate (failed invocations / total invocations).
        min_response_time (Optional[float]): Minimum response time in seconds.
        max_response_time (Optional[float]): Maximum response time in seconds.
        avg_response_time (Optional[float]): Average response time in seconds.
        last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.
    """

    total_executions: int = Field(..., description="Total number of server invocations")
    successful_executions: int = Field(..., description="Number of successful server invocations")
    failed_executions: int = Field(..., description="Number of failed server invocations")
    failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
    min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
    max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
    avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
    last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")


class PromptMetrics(BaseModelWithConfigDict):
    """
    Represents the performance and execution statistics for a prompt.

    Attributes:
        total_executions (int): Total number of prompt invocations.
        successful_executions (int): Number of successful prompt invocations.
        failed_executions (int): Number of failed prompt invocations.
        failure_rate (float): Failure rate (failed invocations / total invocations).
        min_response_time (Optional[float]): Minimum response time in seconds.
        max_response_time (Optional[float]): Maximum response time in seconds.
        avg_response_time (Optional[float]): Average response time in seconds.
        last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.
    """

    total_executions: int = Field(..., description="Total number of prompt invocations")
    successful_executions: int = Field(..., description="Number of successful prompt invocations")
    failed_executions: int = Field(..., description="Number of failed prompt invocations")
    failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
    min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
    max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
    avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
    last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")


class A2AAgentMetrics(BaseModelWithConfigDict):
    """
    Represents the performance and execution statistics for an A2A agent.

    Attributes:
        total_executions (int): Total number of agent interactions.
        successful_executions (int): Number of successful agent interactions.
        failed_executions (int): Number of failed agent interactions.
        failure_rate (float): Failure rate (failed interactions / total interactions).
        min_response_time (Optional[float]): Minimum response time in seconds.
        max_response_time (Optional[float]): Maximum response time in seconds.
        avg_response_time (Optional[float]): Average response time in seconds.
        last_execution_time (Optional[datetime]): Timestamp of the most recent interaction.
    """

    total_executions: int = Field(..., description="Total number of agent interactions")
    successful_executions: int = Field(..., description="Number of successful agent interactions")
    failed_executions: int = Field(..., description="Number of failed agent interactions")
    failure_rate: float = Field(..., description="Failure rate (failed interactions / total interactions)")
    min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
    max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
    avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
    last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent interaction")


class A2AAgentAggregateMetrics(BaseModelWithConfigDict):
    """
    Represents aggregated metrics for all A2A agents in the system.

    This model is used for the /metrics endpoint to provide system-wide A2A agent statistics
    with consistent camelCase field naming.

    Attributes:
        total_agents (int): Total number of A2A agents registered.
        active_agents (int): Number of currently active A2A agents.
        total_interactions (int): Total number of agent interactions.
        successful_interactions (int): Number of successful agent interactions.
        failed_interactions (int): Number of failed agent interactions.
        success_rate (float): Success rate as a percentage (0-100).
        avg_response_time (float): Average response time in seconds.
        min_response_time (float): Minimum response time in seconds.
        max_response_time (float): Maximum response time in seconds.
    """

    total_agents: int = Field(..., description="Total number of A2A agents registered")
    active_agents: int = Field(..., description="Number of currently active A2A agents")
    total_interactions: int = Field(..., description="Total number of agent interactions")
    successful_interactions: int = Field(..., description="Number of successful agent interactions")
    failed_interactions: int = Field(..., description="Number of failed agent interactions")
    success_rate: float = Field(..., description="Success rate as a percentage (0-100)")
    avg_response_time: float = Field(..., description="Average response time in seconds")
    min_response_time: float = Field(..., description="Minimum response time in seconds")
    max_response_time: float = Field(..., description="Maximum response time in seconds")


class MetricsResponse(BaseModelWithConfigDict):
    """
    Response model for the aggregated metrics endpoint.

    Contains metrics for all entity types with consistent camelCase field names.
    When A2A metrics are disabled, the a2a_agents key is omitted entirely
    to preserve backwards compatibility with existing consumers.
    """

    tools: ToolMetrics
    resources: ResourceMetrics
    servers: ServerMetrics
    prompts: PromptMetrics
    a2a_agents: Optional[A2AAgentAggregateMetrics] = None

    @model_serializer(mode="wrap")
    def _exclude_none_a2a(self, handler):
        """Omit the A2A metrics field when that feature is disabled.

        Args:
            handler: Pydantic serializer callback for the wrapped model.

        Returns:
            Dict[str, Any]: Serialized metrics payload without empty A2A fields.
        """
        result = handler(self)
        if self.a2a_agents is None:
            result.pop("a2aAgents", None)
            result.pop("a2a_agents", None)
        return result


# --- JSON Path API modifier Schema


class JsonPathModifier(BaseModelWithConfigDict):
    """Schema for JSONPath queries.

    Provides the structure for parsing JSONPath queries and optional mapping.
    """

    model_config = ConfigDict(extra="forbid")  # ← rejects unknown fields
    jsonpath: Optional[str] = Field(None, description="JSONPath expression for querying JSON data.")
    mapping: Optional[Dict[str, str]] = Field(None, description="Mapping of fields from original data to output.")


# --- Tool Schemas ---
# Authentication model
class AuthenticationValues(BaseModelWithConfigDict):
    """Schema for all Authentications.
    Provides the authentication values for different types of authentication.
    """

    auth_type: Optional[str] = Field(None, description="Type of authentication: basic, bearer, authheaders or None")
    auth_value: Optional[str] = Field(None, description="Encoded Authentication values")

    # Only For tool read and view tool
    username: Optional[str] = Field("", description="Username for basic authentication")
    password: Optional[str] = Field("", description="Password for basic authentication")
    token: Optional[str] = Field("", description="Bearer token for authentication")
    auth_header_key: Optional[str] = Field("", description="Key for custom headers authentication (legacy single header)")
    auth_header_value: Optional[str] = Field("", description="Value for custom headers authentication (legacy single header)")
    authHeaders: Optional[List[Dict[str, str]]] = Field(None, alias="authHeaders", description="List of custom headers for authentication (multi-header format)")  # noqa: N815


# Minimal valid JSON Schema used as the default input_schema for REST tools.
_DEFAULT_INPUT_SCHEMA: dict = {"type": "object", "properties": {}}


def _extract_rest_url_components(values: dict) -> dict:
    """Extract ``base_url`` and ``path_template`` from ``url`` for REST integration tools.

    Shared logic used by both :class:`ToolCreate` and :class:`ToolUpdate` model
    validators so the URL-parsing behaviour stays consistent across create and
    update paths.

    Args:
        values: The raw model input dict (mutated in-place).

    Returns:
        The same *values* dict, potentially with ``base_url`` and
        ``path_template`` populated.
    """
    url = values.get("url")
    if not url:
        return values

    parsed = urlparse(str(url))
    base_url = f"{parsed.scheme}://{parsed.netloc}"
    path_template = parsed.path

    if path_template:
        path_template = "/" + path_template.lstrip("/")

    if not values.get("base_url"):
        values["base_url"] = base_url
    if not values.get("path_template"):
        values["path_template"] = path_template

    return values


class ToolCreate(BaseModel):
    """
    Represents the configuration for creating a tool with various attributes and settings.

    Attributes:
        model_config (ConfigDict): Configuration for the model.
        name (str): Unique name for the tool.
        url (Union[str, AnyHttpUrl]): Tool endpoint URL.
        description (Optional[str]): Tool description.
        integration_type (Literal["REST", "MCP"]): Tool integration type - REST for individual endpoints, MCP for gateway-discovered tools.
        request_type (Literal["GET", "POST", "PUT", "DELETE", "PATCH"]): HTTP method to be used for invoking the tool.
        headers (Optional[Dict[str, str]]): Additional headers to send when invoking the tool.
        input_schema (Optional[Dict[str, Any]]): JSON Schema for validating tool parameters. Alias 'inputSchema'.
        output_schema (Optional[Dict[str, Any]]): JSON Schema for validating tool output. Alias 'outputSchema'.
        annotations (Optional[Dict[str, Any]]): Tool annotations for behavior hints such as title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint.
        jsonpath_filter (Optional[str]): JSON modification filter.
        auth (Optional[AuthenticationValues]): Authentication credentials (Basic or Bearer Token or custom headers) if required.
        gateway_id (Optional[str]): ID of the gateway for the tool.
    """

    model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
    allow_auto: bool = False  # Internal flag to allow system-initiated A2A tool creation

    name: str = Field(..., description="Unique name for the tool")
    displayName: Optional[str] = Field(None, description="Display name for the tool (shown in UI)")  # noqa: N815
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the tool (MCP BaseMetadata)")
    url: Optional[Union[str, AnyHttpUrl]] = Field(None, description="Tool endpoint URL")
    description: Optional[str] = Field(None, description="Tool description")
    integration_type: Literal["REST", "MCP", "A2A"] = Field("REST", description="'REST' for individual endpoints, 'MCP' for gateway-discovered tools, 'A2A' for A2A agents")
    request_type: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "SSE", "STDIO", "STREAMABLEHTTP"] = Field("SSE", description="HTTP method to be used for invoking the tool")
    headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
    input_schema: Optional[Dict[str, Any]] = Field(default_factory=lambda: dict(_DEFAULT_INPUT_SCHEMA), description="JSON Schema for validating tool parameters", alias="inputSchema")
    output_schema: Optional[Dict[str, Any]] = Field(default=None, description="JSON Schema for validating tool output", alias="outputSchema")
    annotations: Optional[Dict[str, Any]] = Field(
        default_factory=dict,
        description="Tool annotations for behavior hints (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint)",
    )
    jsonpath_filter: Optional[str] = Field(default="", description="JSON modification filter")
    auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
    gateway_id: Optional[str] = Field(None, description="id of gateway for the tool")
    tags: Optional[List[str]] = Field(default_factory=list, description="Tags for categorizing the tool")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the tool owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default=None, description="Visibility level: private, team, or public")

    # Passthrough REST fields
    base_url: Optional[str] = Field(None, description="Base URL for REST passthrough")
    path_template: Optional[str] = Field(None, description="Path template for REST passthrough")
    query_mapping: Optional[Dict[str, str]] = Field(None, description="Query mapping for REST passthrough")
    header_mapping: Optional[Dict[str, str]] = Field(None, description="Header mapping for REST passthrough")
    timeout_ms: Optional[int] = Field(default=None, description="Timeout in milliseconds for REST passthrough (20000 if integration_type='REST', else None)")
    expose_passthrough: Optional[bool] = Field(True, description="Expose passthrough endpoint for this tool")
    allowlist: Optional[List[str]] = Field(None, description="Allowed upstream hosts/schemes for passthrough")
    plugin_chain_pre: Optional[List[str]] = Field(None, description="Pre-plugin chain for passthrough")
    plugin_chain_post: Optional[List[str]] = Field(None, description="Post-plugin chain for passthrough")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure tool names follow MCP naming conventions

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe

        Raises:
            ValueError: When displayName contains unsafe content or exceeds length limits

        Examples:
            >>> from mcpgateway.schemas import ToolCreate
            >>> ToolCreate.validate_name('valid_tool')
            'valid_tool'
            >>> ToolCreate.validate_name('Invalid Tool!')
            Traceback (most recent call last):
                ...
            ValueError: ...
        """
        return SecurityValidator.validate_tool_name(v)

    @field_validator("url")
    @classmethod
    def validate_url(cls, v: Optional[str]) -> Optional[str]:
        """Validate URL format and ensure safe display

        Args:
            v (Optional[str]): Value to validate

        Returns:
            Optional[str]: Value if validated as safe

        Raises:
            ValueError: When displayName contains unsafe content or exceeds length limits
        """
        if v is None:
            return v
        return validate_core_url(v, "Tool URL")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe and VALIDATION_STRICT=true (default)

        Note:
            When ``VALIDATION_STRICT=false`` the forbidden-pattern check is skipped
            and a warning is logged instead.  This allows MCP server tools whose
            descriptions contain Markdown syntax (e.g. ``> blockquote``,
            ``< input``, ``cmd | grep``) to register successfully.

        Examples:
            >>> from mcpgateway.schemas import ToolCreate
            >>> ToolCreate.validate_description('A safe description')
            'A safe description'
            >>> ToolCreate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ToolCreate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v

        # Note: backticks (`) and semicolons (;) are allowed as they are commonly used in Markdown
        # for inline code examples in tool descriptions.
        # When VALIDATION_STRICT=false these patterns produce a warning only so
        # that MCP servers with Markdown-formatted descriptions (e.g. "> quote",
        # "< input", "cmd | grep") can register without error.
        if settings.tool_description_forbidden_patterns_enabled:
            for pat in settings.tool_description_forbidden_patterns:
                if not pat or not pat.strip():
                    continue
                if pat in v:
                    if settings.validation_strict:
                        raise ValueError(f"Description contains unsafe characters: '{pat}'")
                    logger.warning("Description contains potentially unsafe characters: '%s' (VALIDATION_STRICT=false, proceeding)", pat)
                    break

        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("displayName")
    @classmethod
    def validate_display_name(cls, v: Optional[str]) -> Optional[str]:
        """Ensure display names display safely

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe

        Raises:
            ValueError: When displayName contains unsafe content or exceeds length limits

        Examples:
            >>> from mcpgateway.schemas import ToolCreate
            >>> ToolCreate.validate_display_name('My Custom Tool')
            'My Custom Tool'
            >>> ToolCreate.validate_display_name('<script>alert("xss")</script>')
            Traceback (most recent call last):
                ...
            ValueError: ...
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_NAME_LENGTH:
            raise ValueError(f"Display name exceeds maximum length of {SecurityValidator.MAX_NAME_LENGTH}")
        return SecurityValidator.sanitize_display_text(v, "Display name")

    @field_validator("headers", "input_schema", "annotations")
    @classmethod
    def validate_json_fields(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Validate JSON structure depth

        Args:
            v (dict): Value to validate

        Returns:
            dict: Value if validated as safe

        Examples:
            >>> from mcpgateway.schemas import ToolCreate
            >>> ToolCreate.validate_json_fields({'a': 1})
            {'a': 1}
            >>> # Test depth within limit (11 levels, default limit is 30)
            >>> ToolCreate.validate_json_fields({'a': {'b': {'c': {'d': {'e': {'f': {'g': {'h': {'i': {'j': {'k': 1}}}}}}}}}}})
            {'a': {'b': {'c': {'d': {'e': {'f': {'g': {'h': {'i': {'j': {'k': 1}}}}}}}}}}}
            >>> # Test exceeding depth limit (31 levels)
            >>> deep_31 = {'1': {'2': {'3': {'4': {'5': {'6': {'7': {'8': {'9': {'10': {'11': {'12': {'13': {'14': {'15': {'16': {'17': {'18': {'19': {'20': {'21': {'22': {'23': {'24': {'25': {'26': {'27': {'28': {'29': {'30': {'31': 'too deep'}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
            >>> ToolCreate.validate_json_fields(deep_31)
            Traceback (most recent call last):
                ...
            ValueError: ...
        """
        SecurityValidator.validate_json_depth(v)
        return v

    @field_validator("request_type")
    @classmethod
    def validate_request_type(cls, v: str, info: ValidationInfo) -> str:
        """Validate request type based on integration type (REST, MCP, A2A)

        Args:
            v (str): Value to validate
            info (ValidationInfo): Values used for validation

        Returns:
            str: Value if validated as safe

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from pydantic import ValidationInfo
            >>> # REST integration types with valid methods
            >>> info_rest = type('obj', (object,), {'data': {'integration_type': 'REST'}})
            >>> ToolCreate.validate_request_type('POST', info_rest)
            'POST'
            >>> ToolCreate.validate_request_type('GET', info_rest)
            'GET'
            >>> # MCP integration types with valid transports
            >>> info_mcp = type('obj', (object,), {'data': {'integration_type': 'MCP'}})
            >>> ToolCreate.validate_request_type('SSE', info_mcp)
            'SSE'
            >>> ToolCreate.validate_request_type('STDIO', info_mcp)
            'STDIO'
            >>> # A2A integration type with valid method
            >>> info_a2a = type('obj', (object,), {'data': {'integration_type': 'A2A'}})
            >>> ToolCreate.validate_request_type('POST', info_a2a)
            'POST'
            >>> # Invalid REST type
            >>> try:
            ...     ToolCreate.validate_request_type('SSE', info_rest)
            ... except ValueError as e:
            ...     "not allowed for REST" in str(e)
            True
            >>> # Invalid MCP type
            >>> try:
            ...     ToolCreate.validate_request_type('POST', info_mcp)
            ... except ValueError as e:
            ...     "not allowed for MCP" in str(e)
            True
            >>> # Invalid A2A type
            >>> try:
            ...     ToolCreate.validate_request_type('GET', info_a2a)
            ... except ValueError as e:
            ...     "not allowed for A2A" in str(e)
            True
            >>> # Invalid integration type
            >>> info_invalid = type('obj', (object,), {'data': {'integration_type': 'INVALID'}})
            >>> try:
            ...     ToolCreate.validate_request_type('GET', info_invalid)
            ... except ValueError as e:
            ...     "Unknown integration type" in str(e)
            True
        """

        integration_type = info.data.get("integration_type")

        if integration_type not in ["REST", "MCP", "A2A"]:
            raise ValueError(f"Unknown integration type: {integration_type}")

        if integration_type == "REST":
            allowed = ["GET", "POST", "PUT", "DELETE", "PATCH"]
            if v not in allowed:
                raise ValueError(f"Request type '{v}' not allowed for REST. Only {allowed} methods are accepted.")
        elif integration_type == "MCP":
            allowed = ["SSE", "STDIO", "STREAMABLEHTTP"]
            if v not in allowed:
                raise ValueError(f"Request type '{v}' not allowed for MCP. Only {allowed} transports are accepted.")
        elif integration_type == "A2A":
            allowed = ["POST"]
            if v not in allowed:
                raise ValueError(f"Request type '{v}' not allowed for A2A. Only {allowed} methods are accepted.")
        return v

    @model_validator(mode="before")
    @classmethod
    def assemble_auth(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        """
        Assemble authentication information from separate keys if provided.

        Looks for keys "auth_type", "auth_username", "auth_password", "auth_token", "auth_header_key" and "auth_header_value".
        Constructs the "auth" field as a dictionary suitable for BasicAuth or BearerTokenAuth or HeadersAuth.

        Args:
            values: Dict with authentication information

        Returns:
            Dict: Reformatedd values dict

        Examples:
            >>> # Test basic auth
            >>> values = {'auth_type': 'basic', 'auth_username': 'user', 'auth_password': 'pass'}
            >>> result = ToolCreate.assemble_auth(values)
            >>> 'auth' in result
            True
            >>> result['auth']['auth_type']
            'basic'

            >>> # Test bearer auth
            >>> values = {'auth_type': 'bearer', 'auth_token': 'mytoken'}
            >>> result = ToolCreate.assemble_auth(values)
            >>> result['auth']['auth_type']
            'bearer'

            >>> # Test authheaders
            >>> values = {'auth_type': 'authheaders', 'auth_header_key': 'X-API-Key', 'auth_header_value': 'secret'}
            >>> result = ToolCreate.assemble_auth(values)
            >>> result['auth']['auth_type']
            'authheaders'

            >>> # Test no auth type
            >>> values = {'name': 'test'}
            >>> result = ToolCreate.assemble_auth(values)
            >>> 'auth' in result
            False
        """
        logger.debug(
            "Assembling auth in ToolCreate with raw values",
            extra={
                "auth_type": values.get("auth_type"),
                "auth_username": values.get("auth_username"),
                "auth_header_key": values.get("auth_header_key"),
                "auth_assembled": bool(values.get("auth_type") and str(values.get("auth_type")).lower() != "one_time_auth"),
            },
        )

        auth_type = values.get("auth_type")
        if auth_type and auth_type.lower() != "one_time_auth":
            if auth_type.lower() == "basic":
                creds = base64.b64encode(f"{values.get('auth_username', '')}:{values.get('auth_password', '')}".encode("utf-8")).decode()
                encoded_auth = encode_auth({"Authorization": f"Basic {creds}"})
                values["auth"] = {"auth_type": "basic", "auth_value": encoded_auth}
            elif auth_type.lower() == "bearer":
                encoded_auth = encode_auth({"Authorization": f"Bearer {values.get('auth_token', '')}"})
                values["auth"] = {"auth_type": "bearer", "auth_value": encoded_auth}
            elif auth_type.lower() == "authheaders":
                header_key = values.get("auth_header_key", "")
                header_value = values.get("auth_header_value", "")
                if header_key and header_value:
                    encoded_auth = encode_auth({header_key: header_value})
                    values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
                else:
                    # Don't encode empty headers - leave auth empty
                    values["auth"] = {"auth_type": "authheaders", "auth_value": None}
        return values

    @model_validator(mode="before")
    @classmethod
    def prevent_manual_mcp_creation(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        """
        Prevent manual creation of MCP tools via API.

        MCP tools should only be created by the gateway service when discovering
        tools from MCP servers. Users should add MCP servers via the Gateways interface.

        Args:
            values: The input values

        Returns:
            Dict[str, Any]: The validated values

        Raises:
            ValueError: If attempting to manually create MCP integration type
        """
        integration_type = values.get("integration_type")
        allow_auto = values.get("allow_auto", False)
        if integration_type == "MCP":
            raise ValueError("Cannot manually create MCP tools. Add MCP servers via the Gateways interface - tools will be auto-discovered and registered with integration_type='MCP'.")
        if integration_type == "A2A" and not allow_auto:
            raise ValueError("Cannot manually create A2A tools. Add A2A agents via the A2A interface - tools will be auto-created when agents are associated with servers.")
        return values

    @model_validator(mode="before")
    @classmethod
    def enforce_passthrough_fields_for_rest(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        """
        Enforce that passthrough REST fields are only set for integration_type 'REST'.
        If any passthrough field is set for non-REST, raise ValueError.

        Args:
            values (Dict[str, Any]): The input values to validate.

        Returns:
            Dict[str, Any]: The validated values.

        Raises:
            ValueError: If passthrough fields are set for non-REST integration_type.
        """
        passthrough_fields = ["base_url", "path_template", "query_mapping", "header_mapping", "timeout_ms", "expose_passthrough", "allowlist", "plugin_chain_pre", "plugin_chain_post"]
        integration_type = values.get("integration_type")
        if integration_type != "REST":
            for field in passthrough_fields:
                if field in values and values[field] not in (None, [], {}):
                    raise ValueError(f"Field '{field}' is only allowed for integration_type 'REST'.")
        return values

    @model_validator(mode="before")
    @classmethod
    def extract_base_url_and_path_template(cls, values: dict) -> dict:
        """For REST tools: extract URL components and ensure a default input_schema.

        Args:
            values (dict): The input values to process.

        Returns:
            dict: The updated values with base_url and path_template extracted from url.
        """
        if values.get("integration_type") != "REST":
            return values

        _extract_rest_url_components(values)

        if not values.get("input_schema"):
            values["input_schema"] = dict(_DEFAULT_INPUT_SCHEMA)

        return values

    @field_validator("base_url")
    @classmethod
    def validate_base_url(cls, v):
        """
        Validate that base_url is a valid URL with scheme and netloc.

        Args:
            v (str): The base_url value to validate.

        Returns:
            str: The validated base_url value.

        Raises:
            ValueError: If base_url is not a valid URL.
        """
        if v is None:
            return v
        parsed = urlparse(str(v))
        if not parsed.scheme or not parsed.netloc:
            raise ValueError("base_url must be a valid URL with scheme and netloc")
        return v

    @field_validator("path_template")
    @classmethod
    def validate_path_template(cls, v):
        """
        Validate that path_template starts with '/'.

        Args:
            v (str): The path_template value to validate.

        Returns:
            str: The validated path_template value.

        Raises:
            ValueError: If path_template does not start with '/'.
        """
        if v and not str(v).startswith("/"):
            raise ValueError("path_template must start with '/'")
        return v

    @field_validator("timeout_ms")
    @classmethod
    def validate_timeout_ms(cls, v):
        """
        Validate that timeout_ms is a positive integer.

        Args:
            v (int): The timeout_ms value to validate.

        Returns:
            int: The validated timeout_ms value.

        Raises:
            ValueError: If timeout_ms is not a positive integer.
        """
        if v is not None and v <= 0:
            raise ValueError("timeout_ms must be a positive integer")
        return v

    @field_validator("allowlist")
    @classmethod
    def validate_allowlist(cls, v):
        """
        Validate that allowlist is a list and each entry is a valid host or scheme string.

        Args:
            v (List[str]): The allowlist to validate.

        Returns:
            List[str]: The validated allowlist.

        Raises:
            ValueError: If allowlist is not a list or any entry is not a valid host/scheme string.
        """
        if v is None:
            return None
        if not isinstance(v, list):
            raise ValueError("allowlist must be a list of host/scheme strings")
        # Uses precompiled regex for hostname validation
        for host in v:
            if not isinstance(host, str):
                raise ValueError(f"Invalid type in allowlist: {host} (must be str)")
            if not _HOSTNAME_RE.match(host):
                raise ValueError(f"Invalid host/scheme in allowlist: {host}")
        return v

    @field_validator("plugin_chain_pre", "plugin_chain_post")
    @classmethod
    def validate_plugin_chain(cls, v):
        """
        Validate that each plugin in the chain is allowed.

        Args:
            v (List[str]): The plugin chain to validate.

        Returns:
            List[str]: The validated plugin chain.

        Raises:
            ValueError: If any plugin is not in the allowed set.
        """
        allowed_plugins = {"deny_filter", "rate_limit", "pii_filter", "response_shape", "regex_filter", "resource_filter"}
        if v is not None:
            for plugin in v:
                if plugin not in allowed_plugins:
                    raise ValueError(f"Unknown plugin: {plugin}")
        return v

    @field_validator("query_mapping", "header_mapping")
    @classmethod
    def validate_mapping_size(cls, v: dict | None) -> dict | None:
        """Validate that mapping dicts do not exceed size limits."""
        return _validate_mapping_size(v)

    @field_validator("header_mapping")
    @classmethod
    def validate_header_mapping_targets(cls, v: dict | None) -> dict | None:
        """Reject header_mapping targets that are sensitive or malformed."""
        return _validate_header_mapping_targets(v)

    @model_validator(mode="after")
    def handle_timeout_ms_defaults(self):
        """Handle timeout_ms defaults based on integration_type and expose_passthrough.

        Returns:
            self: The validated model instance with timeout_ms potentially set to default.
        """
        # If timeout_ms is None and we have REST with passthrough, set default
        if self.timeout_ms is None and self.integration_type == "REST" and getattr(self, "expose_passthrough", True):
            self.timeout_ms = 20000
        return self


class ToolUpdate(BaseModelWithConfigDict):
    """Schema for updating an existing tool.

    Similar to ToolCreate but all fields are optional to allow partial updates.
    """

    name: Optional[str] = Field(None, description="Unique name for the tool")
    displayName: Optional[str] = Field(None, description="Display name for the tool (shown in UI)")  # noqa: N815
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the tool (MCP BaseMetadata)")
    custom_name: Optional[str] = Field(None, description="Custom name for the tool")
    url: Optional[Union[str, AnyHttpUrl]] = Field(None, description="Tool endpoint URL")
    description: Optional[str] = Field(None, description="Tool description")
    integration_type: Optional[Literal["REST", "MCP", "A2A"]] = Field(None, description="Tool integration type")
    request_type: Optional[Literal["GET", "POST", "PUT", "DELETE", "PATCH"]] = Field(None, description="HTTP method to be used for invoking the tool")
    headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
    input_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool parameters")
    output_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool output")
    annotations: Optional[Dict[str, Any]] = Field(None, description="Tool annotations for behavior hints")
    jsonpath_filter: Optional[str] = Field(None, description="JSON path filter for rpc tool calls")
    auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
    gateway_id: Optional[str] = Field(None, description="id of gateway for the tool")
    tags: Optional[List[str]] = Field(None, description="Tags for categorizing the tool")
    visibility: Optional[Literal["private", "team", "public"]] = Field(None, description="Visibility level: private, team, or public")

    # Passthrough REST fields
    base_url: Optional[str] = Field(None, description="Base URL for REST passthrough")
    path_template: Optional[str] = Field(None, description="Path template for REST passthrough")
    query_mapping: Optional[Dict[str, str]] = Field(None, description="Query mapping for REST passthrough")
    header_mapping: Optional[Dict[str, str]] = Field(None, description="Header mapping for REST passthrough")
    timeout_ms: Optional[int] = Field(default=None, description="Timeout in milliseconds for REST passthrough (20000 if integration_type='REST', else None)")
    expose_passthrough: Optional[bool] = Field(True, description="Expose passthrough endpoint for this tool")
    allowlist: Optional[List[str]] = Field(None, description="Allowed upstream hosts/schemes for passthrough")
    plugin_chain_pre: Optional[List[str]] = Field(None, description="Pre-plugin chain for passthrough")
    plugin_chain_post: Optional[List[str]] = Field(None, description="Post-plugin chain for passthrough")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings or None if input is None
        """
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure tool names follow MCP naming conventions

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_tool_name(v)

    @field_validator("custom_name")
    @classmethod
    def validate_custom_name(cls, v: str) -> str:
        """Ensure custom tool names follow MCP naming conventions

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_tool_name(v)

    @field_validator("url")
    @classmethod
    def validate_url(cls, v: Optional[str]) -> Optional[str]:
        """Validate URL format and ensure safe display

        Args:
            v (Optional[str]): Value to validate

        Returns:
            Optional[str]: Value if validated as safe
        """
        if v is None:
            return v
        return validate_core_url(v, "Tool URL")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe and VALIDATION_STRICT=true (default)

        Note:
            When ``VALIDATION_STRICT=false`` the forbidden-pattern check is skipped
            and a warning is logged instead.  This allows MCP server tools whose
            descriptions contain Markdown syntax (e.g. ``> blockquote``,
            ``< input``, ``cmd | grep``) to be updated successfully.

        Examples:
            >>> from mcpgateway.schemas import ToolUpdate
            >>> ToolUpdate.validate_description('A safe description')
            'A safe description'
            >>> ToolUpdate.validate_description(None)  # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ToolUpdate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v

        # Note: backticks (`) and semicolons (;) are allowed as they are commonly used in Markdown
        # for inline code examples in tool descriptions.
        # When VALIDATION_STRICT=false these patterns produce a warning only so
        # that MCP servers with Markdown-formatted descriptions (e.g. "> quote",
        # "< input", "cmd | grep") can be updated without error.
        if settings.tool_description_forbidden_patterns_enabled:
            for pat in settings.tool_description_forbidden_patterns:
                if not pat or not pat.strip():
                    continue
                if pat in v:
                    if settings.validation_strict:
                        raise ValueError(f"Description contains unsafe characters: '{pat}'")
                    logger.warning("Description contains potentially unsafe characters: '%s' (VALIDATION_STRICT=false, proceeding)", pat)
                    break

        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("headers", "input_schema", "annotations")
    @classmethod
    def validate_json_fields(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Validate JSON structure depth

        Args:
            v (dict): Value to validate

        Returns:
            dict: Value if validated as safe
        """
        SecurityValidator.validate_json_depth(v)
        return v

    @field_validator("request_type")
    @classmethod
    def validate_request_type(cls, v: str, info: ValidationInfo) -> str:
        """Validate request type based on integration type

        Args:
            v (str): Value to validate
            info (ValidationInfo): Validation context with other field values

        Returns:
            str: Value if validated as safe

        Raises:
            ValueError: When value is unsafe
        """

        integration_type = info.data.get("integration_type", "REST")

        if integration_type == "REST":
            allowed = ["GET", "POST", "PUT", "DELETE", "PATCH"]
        elif integration_type == "MCP":
            allowed = ["SSE", "STDIO", "STREAMABLEHTTP"]
        elif integration_type == "A2A":
            allowed = ["POST"]  # A2A agents typically use POST
        else:
            raise ValueError(f"Unknown integration type: {integration_type}")

        if v not in allowed:
            raise ValueError(f"Request type '{v}' not allowed for {integration_type} integration")
        return v

    @model_validator(mode="before")
    @classmethod
    def assemble_auth(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        """
        Assemble authentication information from separate keys if provided.

        Looks for keys "auth_type", "auth_username", "auth_password", "auth_token", "auth_header_key" and "auth_header_value".
        Constructs the "auth" field as a dictionary suitable for BasicAuth or BearerTokenAuth or HeadersAuth.

        Args:
            values: Dict with authentication information

        Returns:
            Dict: Reformatedd values dict
        """
        logger.debug(
            "Assembling auth in ToolCreate with raw values",
            extra={
                "auth_type": values.get("auth_type"),
                "auth_username": values.get("auth_username"),
                "auth_header_key": values.get("auth_header_key"),
                "auth_assembled": bool(values.get("auth_type") and str(values.get("auth_type")).lower() != "one_time_auth"),
            },
        )

        auth_type = values.get("auth_type")
        if auth_type and auth_type.lower() != "one_time_auth":
            if auth_type.lower() == "basic":
                creds = base64.b64encode(f"{values.get('auth_username', '')}:{values.get('auth_password', '')}".encode("utf-8")).decode()
                encoded_auth = encode_auth({"Authorization": f"Basic {creds}"})
                values["auth"] = {"auth_type": "basic", "auth_value": encoded_auth}
            elif auth_type.lower() == "bearer":
                encoded_auth = encode_auth({"Authorization": f"Bearer {values.get('auth_token', '')}"})
                values["auth"] = {"auth_type": "bearer", "auth_value": encoded_auth}
            elif auth_type.lower() == "authheaders":
                header_key = values.get("auth_header_key", "")
                header_value = values.get("auth_header_value", "")
                if header_key and header_value:
                    encoded_auth = encode_auth({header_key: header_value})
                    values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
                else:
                    # Don't encode empty headers - leave auth empty
                    values["auth"] = {"auth_type": "authheaders", "auth_value": None}
        return values

    @model_validator(mode="before")
    @classmethod
    def extract_base_url_and_path_template(cls, values: dict) -> dict:
        """For REST tools: extract URL components and normalise empty input_schema.

        Args:
            values (dict): The input values to process.

        Returns:
            dict: The updated values with base_url and path_template extracted from url.
        """
        if values.get("integration_type") != "REST":
            return values

        _extract_rest_url_components(values)

        # Normalise explicitly-empty input_schema to the typed default.
        # None is left alone (partial update semantics — omitted fields
        # should not overwrite existing values in the database).
        input_schema = values.get("input_schema")
        if input_schema is not None and isinstance(input_schema, dict) and not input_schema:
            values["input_schema"] = dict(_DEFAULT_INPUT_SCHEMA)

        return values

    @field_validator("displayName")
    @classmethod
    def validate_display_name(cls, v: Optional[str]) -> Optional[str]:
        """Ensure display names display safely

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe

        Raises:
            ValueError: When displayName contains unsafe content or exceeds length limits

        Examples:
            >>> from mcpgateway.schemas import ToolUpdate
            >>> ToolUpdate.validate_display_name('My Custom Tool')
            'My Custom Tool'
            >>> ToolUpdate.validate_display_name('<script>alert("xss")</script>')
            Traceback (most recent call last):
                ...
            ValueError: ...
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_NAME_LENGTH:
            raise ValueError(f"Display name exceeds maximum length of {SecurityValidator.MAX_NAME_LENGTH}")
        return SecurityValidator.sanitize_display_text(v, "Display name")

    @model_validator(mode="before")
    @classmethod
    def prevent_manual_mcp_update(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        """
        Prevent updating tools to MCP integration type via API.

        MCP tools should only be managed by the gateway service. Users should not
        be able to change a REST tool to MCP type or vice versa manually.

        Args:
            values: The input values

        Returns:
            Dict[str, Any]: The validated values

        Raises:
            ValueError: If attempting to update to MCP integration type
        """
        integration_type = values.get("integration_type")
        if integration_type == "MCP":
            raise ValueError("Cannot update tools to MCP integration type. MCP tools are managed by the gateway service.")
        if integration_type == "A2A":
            raise ValueError("Cannot update tools to A2A integration type. A2A tools are managed by the A2A service.")
        return values

    @field_validator("base_url")
    @classmethod
    def validate_base_url(cls, v):
        """
        Validate that base_url is a valid URL with scheme and netloc.

        Args:
            v (str): The base_url value to validate.

        Returns:
            str: The validated base_url value.

        Raises:
            ValueError: If base_url is not a valid URL.
        """
        if v is None:
            return v
        parsed = urlparse(str(v))
        if not parsed.scheme or not parsed.netloc:
            raise ValueError("base_url must be a valid URL with scheme and netloc")
        return v

    @field_validator("path_template")
    @classmethod
    def validate_path_template(cls, v):
        """
        Validate that path_template starts with '/'.

        Args:
            v (str): The path_template value to validate.

        Returns:
            str: The validated path_template value.

        Raises:
            ValueError: If path_template does not start with '/'.
        """
        if v and not str(v).startswith("/"):
            raise ValueError("path_template must start with '/'")
        return v

    @field_validator("timeout_ms")
    @classmethod
    def validate_timeout_ms(cls, v):
        """
        Validate that timeout_ms is a positive integer.

        Args:
            v (int): The timeout_ms value to validate.

        Returns:
            int: The validated timeout_ms value.

        Raises:
            ValueError: If timeout_ms is not a positive integer.
        """
        if v is not None and v <= 0:
            raise ValueError("timeout_ms must be a positive integer")
        return v

    @field_validator("allowlist")
    @classmethod
    def validate_allowlist(cls, v):
        """
        Validate that allowlist is a list and each entry is a valid host or scheme string.

        Args:
            v (List[str]): The allowlist to validate.

        Returns:
            List[str]: The validated allowlist.

        Raises:
            ValueError: If allowlist is not a list or any entry is not a valid host/scheme string.
        """
        if v is None:
            return None
        if not isinstance(v, list):
            raise ValueError("allowlist must be a list of host/scheme strings")
        # Uses precompiled regex for hostname validation
        for host in v:
            if not isinstance(host, str):
                raise ValueError(f"Invalid type in allowlist: {host} (must be str)")
            if not _HOSTNAME_RE.match(host):
                raise ValueError(f"Invalid host/scheme in allowlist: {host}")
        return v

    @field_validator("plugin_chain_pre", "plugin_chain_post")
    @classmethod
    def validate_plugin_chain(cls, v):
        """
        Validate that each plugin in the chain is allowed.

        Args:
            v (List[str]): The plugin chain to validate.

        Returns:
            List[str]: The validated plugin chain.

        Raises:
            ValueError: If any plugin is not in the allowed set.
        """
        allowed_plugins = {"deny_filter", "rate_limit", "pii_filter", "response_shape", "regex_filter", "resource_filter"}
        if v is not None:
            for plugin in v:
                if plugin not in allowed_plugins:
                    raise ValueError(f"Unknown plugin: {plugin}")
        return v

    @field_validator("query_mapping", "header_mapping")
    @classmethod
    def validate_mapping_size(cls, v: dict | None) -> dict | None:
        """Validate that mapping dicts do not exceed size limits."""
        return _validate_mapping_size(v)

    @field_validator("header_mapping")
    @classmethod
    def validate_header_mapping_targets(cls, v: dict | None) -> dict | None:
        """Reject header_mapping targets that are sensitive or malformed."""
        return _validate_header_mapping_targets(v)


class ToolRead(BaseModelWithConfigDict):
    """Schema for reading tool information.

    Includes all tool fields plus:
    - Database ID
    - Creation/update timestamps
    - enabled: If Tool is enabled or disabled.
    - reachable: If Tool is reachable or not.
    - Gateway ID for federation
    - Execution count indicating the number of times the tool has been executed.
    - Metrics: Aggregated metrics for the tool invocations.
    - Request type and authentication settings.
    """

    id: str
    original_name: str
    url: Optional[str]
    description: Optional[str]
    original_description: Optional[str] = None
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the tool (MCP BaseMetadata)")
    request_type: str
    integration_type: str
    headers: Optional[Dict[str, str]]
    input_schema: Dict[str, Any]
    output_schema: Optional[Dict[str, Any]] = Field(None)
    annotations: Optional[Dict[str, Any]]
    jsonpath_filter: Optional[str]
    auth: Optional[AuthenticationValues]
    created_at: datetime
    updated_at: datetime
    enabled: bool
    reachable: bool
    gateway_id: Optional[str]
    grpc_service_id: Optional[str] = Field(None, description="ID of the gRPC service this tool was discovered from")
    execution_count: Optional[int] = Field(None)
    metrics: Optional[ToolMetrics] = Field(None)
    name: str
    displayName: Optional[str] = Field(None, description="Display name for the tool (shown in UI)")  # noqa: N815
    gateway_slug: str
    custom_name: str
    custom_name_slug: str
    tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the tool")

    # Comprehensive metadata for audit tracking
    created_by: Optional[str] = Field(None, description="Username who created this entity")
    created_from_ip: Optional[str] = Field(None, description="IP address of creator")
    created_via: Optional[str] = Field(None, description="Creation method: ui|api|import|federation")
    created_user_agent: Optional[str] = Field(None, description="User agent of creation request")

    modified_by: Optional[str] = Field(None, description="Username who last modified this entity")
    modified_from_ip: Optional[str] = Field(None, description="IP address of last modifier")
    modified_via: Optional[str] = Field(None, description="Modification method")
    modified_user_agent: Optional[str] = Field(None, description="User agent of modification request")

    import_batch_id: Optional[str] = Field(None, description="UUID of bulk import batch")
    federation_source: Optional[str] = Field(None, description="Source gateway for federated entities")
    version: Optional[int] = Field(1, description="Entity version for change tracking")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="ID of the team that owns this resource")
    team: Optional[str] = Field(None, description="Name of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Email of the user who owns this resource")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Visibility level: private, team, or public")

    # Passthrough REST fields
    base_url: Optional[str] = Field(None, description="Base URL for REST passthrough")
    path_template: Optional[str] = Field(None, description="Path template for REST passthrough")
    query_mapping: Optional[Dict[str, str]] = Field(None, description="Query mapping for REST passthrough")
    header_mapping: Optional[Dict[str, str]] = Field(None, description="Header mapping for REST passthrough")
    timeout_ms: Optional[int] = Field(20000, description="Timeout in milliseconds for REST passthrough")
    expose_passthrough: Optional[bool] = Field(True, description="Expose passthrough endpoint for this tool")
    allowlist: Optional[List[str]] = Field(None, description="Allowed upstream hosts/schemes for passthrough")
    plugin_chain_pre: Optional[List[str]] = Field(None, description="Pre-plugin chain for passthrough")
    plugin_chain_post: Optional[List[str]] = Field(None, description="Post-plugin chain for passthrough")

    # MCP protocol extension field
    meta: Optional[Dict[str, Any]] = Field(None, alias="_meta", description="Optional metadata for protocol extension")

    _normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))


class ToolInvocation(BaseModelWithConfigDict):
    """Schema for tool invocation requests.

    This schema validates tool invocation requests to ensure they follow MCP
    (Model Context Protocol) naming conventions and prevent security vulnerabilities
    such as XSS attacks or deeply nested payloads that could cause DoS.

    Captures:
    - Tool name to invoke (validated for safety and MCP compliance)
    - Arguments matching tool's input schema (validated for depth limits)

    Validation Rules:
    - Tool names must start with a letter, number, or underscore and contain only
      letters, numbers, periods, underscores, hyphens, and slashes (per SEP-986)
    - Tool names cannot contain HTML special characters (<, >, ", ')
    - Arguments are validated to prevent excessively deep nesting (default max: 10 levels)

    Attributes:
        name (str): Name of the tool to invoke. Must follow MCP naming conventions.
        arguments (Dict[str, Any]): Arguments to pass to the tool. Must match the
                                   tool's input schema and not exceed depth limits.

    Examples:
        >>> from pydantic import ValidationError
        >>> # Valid tool invocation
        >>> tool_inv = ToolInvocation(name="get_weather", arguments={"city": "London"})
        >>> tool_inv.name
        'get_weather'
        >>> tool_inv.arguments
        {'city': 'London'}

        >>> # Valid tool name with underscores and numbers
        >>> tool_inv = ToolInvocation(name="tool_v2_beta", arguments={})
        >>> tool_inv.name
        'tool_v2_beta'

        >>> # Invalid: Tool name with special characters
        >>> try:
        ...     ToolInvocation(name="tool-name!", arguments={})
        ... except ValidationError as e:
        ...     print("Validation failed: Special characters not allowed")
        Validation failed: Special characters not allowed

        >>> # Invalid: XSS attempt in tool name
        >>> try:
        ...     ToolInvocation(name="<script>alert('XSS')</script>", arguments={})
        ... except ValidationError as e:
        ...     print("Validation failed: HTML tags not allowed")
        Validation failed: HTML tags not allowed

        >>> # Valid: Tool name starting with number (per MCP spec)
        >>> tool_num = ToolInvocation(name="123_tool", arguments={})
        >>> tool_num.name
        '123_tool'

        >>> # Valid: Tool name starting with underscore (per MCP spec)
        >>> tool_underscore = ToolInvocation(name="_5gpt_query", arguments={})
        >>> tool_underscore.name
        '_5gpt_query'

        >>> # Invalid: Tool name starting with hyphen
        >>> try:
        ...     ToolInvocation(name="-invalid_tool", arguments={})
        ... except ValidationError as e:
        ...     print("Validation failed: Must start with letter, number, or underscore")
        Validation failed: Must start with letter, number, or underscore

        >>> # Valid: Complex but not too deep arguments
        >>> args = {"level1": {"level2": {"level3": {"data": "value"}}}}
        >>> tool_inv = ToolInvocation(name="process_data", arguments=args)
        >>> tool_inv.arguments["level1"]["level2"]["level3"]["data"]
        'value'

        >>> # Invalid: Arguments too deeply nested (>30 levels)
        >>> deep_args = {"a": {"b": {"c": {"d": {"e": {"f": {"g": {"h": {"i": {"j": {"k": {"l": {"m": {"n": {"o": {"p": {"q": {"r": {"s": {"t": {"u": {"v": {"w": {"x": {"y": {"z": {"aa": {"bb": {"cc": {"dd": {"ee": "too deep"}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
        >>> try:
        ...     ToolInvocation(name="process_data", arguments=deep_args)
        ... except ValidationError as e:
        ...     print("Validation failed: Exceeds maximum depth")
        Validation failed: Exceeds maximum depth

        >>> # Edge case: Empty tool name
        >>> try:
        ...     ToolInvocation(name="", arguments={})
        ... except ValidationError as e:
        ...     print("Validation failed: Name cannot be empty")
        Validation failed: Name cannot be empty

        >>> # Valid: Tool name with hyphen (but not starting/ending)
        >>> tool_inv = ToolInvocation(name="get_user_info", arguments={"id": 123})
        >>> tool_inv.name
        'get_user_info'

        >>> # Arguments with various types
        >>> args = {
        ...     "string": "value",
        ...     "number": 42,
        ...     "boolean": True,
        ...     "array": [1, 2, 3],
        ...     "nested": {"key": "value"}
        ... }
        >>> tool_inv = ToolInvocation(name="complex_tool", arguments=args)
        >>> tool_inv.arguments["number"]
        42
    """

    name: str = Field(..., description="Name of tool to invoke")
    arguments: Dict[str, Any] = Field(default_factory=dict, description="Arguments matching tool's input schema")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure tool names follow MCP naming conventions.

        Validates that the tool name:
        - Is not empty
        - Starts with a letter (not a number or special character)
        - Contains only letters, numbers, underscores, and hyphens
        - Does not contain HTML special characters that could cause XSS
        - Does not exceed maximum length (255 characters)

        Args:
            v (str): Tool name to validate

        Returns:
            str: The validated tool name if it passes all checks

        Raises:
            ValueError: If the tool name violates any validation rules
        """
        return SecurityValidator.validate_tool_name(v)

    @field_validator("arguments")
    @classmethod
    def validate_arguments(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Validate arguments structure depth to prevent DoS attacks.

        Ensures that the arguments dictionary doesn't have excessive nesting
        that could cause performance issues or stack overflow. The default
        maximum depth is 10 levels.

        Args:
            v (dict): Arguments dictionary to validate

        Returns:
            dict: The validated arguments if within depth limits

        Raises:
            ValueError: If the arguments exceed the maximum allowed depth
        """
        SecurityValidator.validate_json_depth(v)
        return v


class ToolResult(BaseModelWithConfigDict):
    """Schema for tool invocation results.

    Supports:
    - Multiple content types (text/image)
    - Error reporting
    - Optional error messages
    """

    content: List[Union[TextContent, ImageContent]]
    structured_content: Optional[Dict[str, Any]] = None
    is_error: bool = False
    error_message: Optional[str] = None


class ResourceCreate(BaseModel):
    """
    Schema for creating a new resource.

    Attributes:
        model_config (ConfigDict): Configuration for the model.
        uri (str): Unique URI for the resource.
        name (str): Human-readable name for the resource.
        description (Optional[str]): Optional description of the resource.
        mime_type (Optional[str]): Optional MIME type of the resource.
        template (Optional[str]): Optional URI template for parameterized resources.
        content (Union[str, bytes]): Content of the resource, which can be text or binary.
    """

    model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)

    uri: str = Field(..., description="Unique URI for the resource")
    name: str = Field(..., description="Human-readable resource name")
    description: Optional[str] = Field(None, description="Resource description")
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the resource (MCP BaseMetadata)")
    mime_type: Optional[str] = Field(None, alias="mimeType", description="Resource MIME type")
    uri_template: Optional[str] = Field(None, description="URI template for parameterized resources")
    content: Union[str, bytes] = Field(..., description="Resource content (text or binary)")
    tags: Optional[List[str]] = Field(default_factory=list, description="Tags for categorizing the resource")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the resource owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default=None, description="Visibility level: private, team, or public")
    gateway_id: Optional[str] = Field(None, description="ID of the gateway for the resource")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("uri")
    @classmethod
    def validate_uri(cls, v: str) -> str:
        """Validate URI format

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_uri(v, "Resource URI")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate resource name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Resource name")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import ResourceCreate
            >>> ResourceCreate.validate_description('A safe description')
            'A safe description'
            >>> ResourceCreate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ResourceCreate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("mime_type")
    @classmethod
    def validate_mime_type(cls, v: Optional[str]) -> Optional[str]:
        """Validate MIME type format

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        if v is None:
            return v
        return SecurityValidator.validate_mime_type(v)

    @field_validator("content")
    @classmethod
    def validate_content(cls, v: Optional[Union[str, bytes]]) -> Optional[Union[str, bytes]]:
        """Validate content safety.

        Note: Size validation is performed at the service layer using configurable limits.
        This validator only checks encoding and dangerous patterns.

        Args:
            v (Union[str, bytes]): Value to validate

        Returns:
            Union[str, bytes]: Value if validated as safe

        Raises:
            ValueError: When value is unsafe
        """
        if v is None:
            return v

        # Validate UTF-8 encoding for bytes
        if isinstance(v, bytes):
            try:
                text = v.decode("utf-8")
            except UnicodeDecodeError:
                raise ValueError("Content must be UTF-8 decodable")
        else:
            text = v

        # Check for dangerous HTML patterns
        # Runtime pattern matching (not precompiled to allow test monkeypatching)
        if re.search(SecurityValidator.DANGEROUS_HTML_PATTERN, text, re.IGNORECASE):
            raise ValueError("Content contains HTML tags that may cause display issues")

        return v


class ResourceUpdate(BaseModelWithConfigDict):
    """Schema for updating an existing resource.

    Similar to ResourceCreate but URI is not required and all fields are optional.
    """

    uri: Optional[str] = Field(None, description="Unique URI for the resource")
    name: Optional[str] = Field(None, description="Human-readable resource name")
    description: Optional[str] = Field(None, description="Resource description")
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the resource (MCP BaseMetadata)")
    mime_type: Optional[str] = Field(None, description="Resource MIME type")
    uri_template: Optional[str] = Field(None, description="URI template for parameterized resources")
    content: Optional[Union[str, bytes]] = Field(None, description="Resource content (text or binary)")
    tags: Optional[List[str]] = Field(None, description="Tags for categorizing the resource")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the resource owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(None, description="Visibility level: private, team, or public")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings or None if input is None
        """
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate resource name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Resource name")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import ResourceUpdate
            >>> ResourceUpdate.validate_description('A safe description')
            'A safe description'
            >>> ResourceUpdate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ResourceUpdate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("mime_type")
    @classmethod
    def validate_mime_type(cls, v: Optional[str]) -> Optional[str]:
        """Validate MIME type format

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        if v is None:
            return v
        return SecurityValidator.validate_mime_type(v)

    @field_validator("content")
    @classmethod
    def validate_content(cls, v: Optional[Union[str, bytes]]) -> Optional[Union[str, bytes]]:
        """Validate content safety.

        Note: Size validation is performed at the service layer using configurable limits.
        This validator only checks encoding and dangerous patterns.

        Args:
            v (Union[str, bytes]): Value to validate

        Returns:
            Union[str, bytes]: Value if validated as safe

        Raises:
            ValueError: When value is unsafe
        """
        if v is None:
            return v

        # Validate UTF-8 encoding for bytes
        if isinstance(v, bytes):
            try:
                text = v.decode("utf-8")
            except UnicodeDecodeError:
                raise ValueError("Content must be UTF-8 decodable")
        else:
            text = v

        # Check for dangerous HTML patterns
        # Runtime pattern matching (not precompiled to allow test monkeypatching)
        if re.search(SecurityValidator.DANGEROUS_HTML_PATTERN, text, re.IGNORECASE):
            raise ValueError("Content contains HTML tags that may cause display issues")

        return v


class ResourceRead(BaseModelWithConfigDict):
    """Schema for reading resource information.

    Includes all resource fields plus:
    - Database ID
    - Content size
    - Creation/update timestamps
    - Active status
    - Metrics: Aggregated metrics for the resource invocations.
    """

    id: str = Field(description="Unique ID of the resource")
    uri: str
    name: str
    description: Optional[str]
    mime_type: Optional[str]
    gateway_id: Optional[str] = Field(None, description="ID of the gateway for the resource")
    uri_template: Optional[str] = Field(None, description="URI template for parameterized resources")
    size: Optional[int]
    created_at: datetime
    updated_at: datetime
    enabled: bool
    metrics: Optional[ResourceMetrics] = Field(None, description="Resource metrics (may be None in list operations)")
    tags: List[str] = Field(default_factory=list, description="Tags for categorizing the resource")

    # Comprehensive metadata for audit tracking
    created_by: Optional[str] = Field(None, description="Username who created this entity")
    created_from_ip: Optional[str] = Field(None, description="IP address of creator")
    created_via: Optional[str] = Field(None, description="Creation method: ui|api|import|federation")
    created_user_agent: Optional[str] = Field(None, description="User agent of creation request")

    modified_by: Optional[str] = Field(None, description="Username who last modified this entity")
    modified_from_ip: Optional[str] = Field(None, description="IP address of last modifier")
    modified_via: Optional[str] = Field(None, description="Modification method")
    modified_user_agent: Optional[str] = Field(None, description="User agent of modification request")

    import_batch_id: Optional[str] = Field(None, description="UUID of bulk import batch")
    federation_source: Optional[str] = Field(None, description="Source gateway for federated entities")
    version: Optional[int] = Field(1, description="Entity version for change tracking")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="ID of the team that owns this resource")
    team: Optional[str] = Field(None, description="Name of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Email of the user who owns this resource")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Visibility level: private, team, or public")

    # MCP protocol fields
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the resource")
    annotations: Optional[Annotations] = Field(None, description="Optional annotations for client rendering hints")
    meta: Optional[Dict[str, Any]] = Field(None, alias="_meta", description="Optional metadata for protocol extension")

    _normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))


class ResourceSubscription(BaseModelWithConfigDict):
    """Schema for resource subscriptions.

    This schema validates resource subscription requests to ensure URIs are safe
    and subscriber IDs follow proper formatting rules. It prevents various
    injection attacks and ensures data consistency.

    Tracks:
    - Resource URI being subscribed to (validated for safety)
    - Unique subscriber identifier (validated for proper format)

    Validation Rules:
    - URIs cannot contain HTML special characters (<, >, ", ', backslash)
    - URIs cannot contain directory traversal sequences (..)
    - URIs must contain only safe characters (alphanumeric, _, -, :, /, ?, =, &, %)
    - Subscriber IDs must contain only alphanumeric characters, underscores, hyphens, and dots
    - Both fields have maximum length limits (255 characters)

    Attributes:
        uri (str): URI of the resource to subscribe to. Must be a safe, valid URI.
        subscriber_id (str): Unique identifier for the subscriber. Must follow
                            identifier naming conventions.

    Examples:
        >>> from pydantic import ValidationError
        >>> # Valid subscription
        >>> sub = ResourceSubscription(uri="/api/v1/users/123", subscriber_id="client_001")
        >>> sub.uri
        '/api/v1/users/123'
        >>> sub.subscriber_id
        'client_001'

        >>> # Valid URI with query parameters
        >>> sub = ResourceSubscription(uri="/data?type=json&limit=10", subscriber_id="app.service.1")
        >>> sub.uri
        '/data?type=json&limit=10'

        >>> # Valid subscriber ID with dots (common for service names)
        >>> sub = ResourceSubscription(uri="/events", subscriber_id="com.example.service")
        >>> sub.subscriber_id
        'com.example.service'

        >>> # Invalid: XSS attempt in URI
        >>> try:
        ...     ResourceSubscription(uri="<script>alert('XSS')</script>", subscriber_id="sub1")
        ... except ValidationError as e:
        ...     print("Validation failed: HTML characters not allowed")
        Validation failed: HTML characters not allowed

        >>> # Invalid: Directory traversal in URI
        >>> try:
        ...     ResourceSubscription(uri="/api/../../../etc/passwd", subscriber_id="sub1")
        ... except ValidationError as e:
        ...     print("Validation failed: Directory traversal detected")
        Validation failed: Directory traversal detected

        >>> # Invalid: SQL injection attempt in URI
        >>> try:
        ...     ResourceSubscription(uri="/users'; DROP TABLE users;--", subscriber_id="sub1")
        ... except ValidationError as e:
        ...     print("Validation failed: Invalid characters in URI")
        Validation failed: Invalid characters in URI

        >>> # Invalid: Special characters in subscriber ID
        >>> try:
        ...     ResourceSubscription(uri="/api/data", subscriber_id="sub@123!")
        ... except ValidationError as e:
        ...     print("Validation failed: Invalid subscriber ID format")
        Validation failed: Invalid subscriber ID format

        >>> # Invalid: Empty URI
        >>> try:
        ...     ResourceSubscription(uri="", subscriber_id="sub1")
        ... except ValidationError as e:
        ...     print("Validation failed: URI cannot be empty")
        Validation failed: URI cannot be empty

        >>> # Invalid: Empty subscriber ID
        >>> try:
        ...     ResourceSubscription(uri="/api/data", subscriber_id="")
        ... except ValidationError as e:
        ...     print("Validation failed: Subscriber ID cannot be empty")
        Validation failed: Subscriber ID cannot be empty

        >>> # Valid: Complex but safe URI
        >>> sub = ResourceSubscription(
        ...     uri="/api/v2/resources/category:items/filter?status=active&limit=50",
        ...     subscriber_id="monitor-service-01"
        ... )
        >>> sub.uri
        '/api/v2/resources/category:items/filter?status=active&limit=50'

        >>> # Edge case: Maximum length validation (simulated)
        >>> long_uri = "/" + "a" * 254  # Just under limit
        >>> sub = ResourceSubscription(uri=long_uri, subscriber_id="sub1")
        >>> len(sub.uri)
        255

        >>> # Invalid: Quotes in URI (could break out of attributes)
        >>> try:
        ...     ResourceSubscription(uri='/api/data"onclick="alert(1)', subscriber_id="sub1")
        ... except ValidationError as e:
        ...     print("Validation failed: Quotes not allowed in URI")
        Validation failed: Quotes not allowed in URI
    """

    uri: str = Field(..., description="URI of resource to subscribe to")
    subscriber_id: str = Field(..., description="Unique subscriber identifier")

    @field_validator("uri")
    @classmethod
    def validate_uri(cls, v: str) -> str:
        """Validate URI format for safety and correctness.

        Ensures the URI:
        - Is not empty
        - Does not contain HTML special characters that could cause XSS
        - Does not contain directory traversal sequences (..)
        - Contains only allowed characters for URIs
        - Does not exceed maximum length (255 characters)

        This prevents various injection attacks including XSS, path traversal,
        and other URI-based vulnerabilities.

        Args:
            v (str): URI to validate

        Returns:
            str: The validated URI if it passes all security checks

        Raises:
            ValueError: If the URI contains dangerous patterns or invalid characters
        """
        return SecurityValidator.validate_uri(v, "Resource URI")

    @field_validator("subscriber_id")
    @classmethod
    def validate_subscriber_id(cls, v: str) -> str:
        """Validate subscriber ID format.

        Ensures the subscriber ID:
        - Is not empty
        - Contains only safe identifier characters
        - Allows email-style IDs for authenticated subscribers
        - Does not contain HTML special characters
        - Follows standard identifier naming conventions
        - Does not exceed maximum length (255 characters)

        This ensures consistency and prevents injection attacks through
        subscriber identifiers.

        Args:
            v (str): Subscriber ID to validate

        Returns:
            str: The validated subscriber ID if it passes all checks

        Raises:
            ValueError: If the subscriber ID violates naming conventions
        """
        if not v:
            raise ValueError("Subscriber ID cannot be empty")

        # Allow email-like subscriber IDs while keeping strict character controls.
        if re.match(r"^[A-Za-z0-9_.@+-]+$", v):
            if re.search(SecurityValidator.VALIDATION_UNSAFE_URI_PATTERN, v):
                raise ValueError("Subscriber ID cannot contain HTML special characters")
            if len(v) > SecurityValidator.MAX_NAME_LENGTH:
                raise ValueError(f"Subscriber ID exceeds maximum length of {SecurityValidator.MAX_NAME_LENGTH}")
            return v

        return SecurityValidator.validate_identifier(v, "Subscriber ID")


class ResourceNotification(BaseModelWithConfigDict):
    """Schema for resource update notifications.

    Contains:
    - Resource URI
    - Updated content
    - Update timestamp
    """

    uri: str
    content: ResourceContent
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

    @field_serializer("timestamp")
    def serialize_timestamp(self, dt: datetime) -> str:
        """Serialize the `timestamp` field as an ISO 8601 string with UTC timezone.

        Converts the given datetime to UTC and returns it in ISO 8601 format,
        replacing the "+00:00" suffix with "Z" to indicate UTC explicitly.

        Args:
            dt (datetime): The datetime object to serialize.

        Returns:
            str: ISO 8601 formatted string in UTC, ending with 'Z'.
        """
        return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


# --- Prompt Schemas ---


class PromptArgument(BaseModelWithConfigDict):
    """Schema for prompt template arguments.

    Defines:
    - Argument name
    - Optional description
    - Required flag
    """

    name: str = Field(..., description="Argument name")
    description: Optional[str] = Field(None, description="Argument description")
    required: bool = Field(default=False, description="Whether argument is required")

    # Use base config; example metadata removed to avoid config merging type issues in static checks


class PromptCreate(BaseModelWithConfigDict):
    """
    Schema for creating a new prompt.

    Attributes:
        model_config (ConfigDict): Configuration for the model.
        name (str): Unique name for the prompt.
        description (Optional[str]): Optional description of the prompt.
        template (str): Template text for the prompt.
        arguments (List[PromptArgument]): List of arguments for the template.
    """

    model_config = ConfigDict(**dict(BaseModelWithConfigDict.model_config), str_strip_whitespace=True)

    name: str = Field(..., description="Unique name for the prompt")
    custom_name: Optional[str] = Field(None, description="Custom prompt name used for MCP invocation")
    display_name: Optional[str] = Field(None, description="Display name for the prompt (shown in UI)")
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the prompt (MCP BaseMetadata)")
    description: Optional[str] = Field(None, description="Prompt description")
    template: str = Field(..., description="Prompt template text")
    arguments: List[PromptArgument] = Field(default_factory=list, description="List of arguments for the template")
    tags: Optional[List[str]] = Field(default_factory=list, description="Tags for categorizing the prompt")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the prompt owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default=None, description="Visibility level: private, team, or public")
    gateway_id: Optional[str] = Field(None, description="ID of the gateway for the prompt")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure prompt names display correctly in UI

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Prompt name")

    @field_validator("custom_name")
    @classmethod
    def validate_custom_name(cls, v: Optional[str]) -> Optional[str]:
        """Ensure custom prompt names follow MCP naming conventions.

        Args:
            v: Custom prompt name to validate.

        Returns:
            The validated custom name or None.
        """
        if v is None:
            return v
        return SecurityValidator.validate_name(v, "Prompt name")

    @field_validator("display_name")
    @classmethod
    def validate_display_name(cls, v: Optional[str]) -> Optional[str]:
        """Ensure display names render safely in UI.

        Args:
            v: Display name to validate.

        Returns:
            The validated display name or None.
        """
        if v is None:
            return v
        return SecurityValidator.sanitize_display_text(v, "Prompt display name")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import PromptCreate
            >>> PromptCreate.validate_description('A safe description')
            'A safe description'
            >>> PromptCreate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = PromptCreate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("template")
    @classmethod
    def validate_template(cls, v: str) -> str:
        """Validate template content for safe display

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_template(v)

    @field_validator("arguments")
    @classmethod
    def validate_arguments(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Ensure JSON structure is valid and within complexity limits

        Args:
            v (dict): Value to validate

        Returns:
            dict: Value if validated as safe
        """
        SecurityValidator.validate_json_depth(v)
        return v


class PromptExecuteArgs(BaseModel):
    """
    Schema for args executing a prompt

    Attributes:
        args (Dict[str, str]): Arguments for prompt execution.
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    args: Dict[str, str] = Field(default_factory=dict, description="Arguments for prompt execution")

    @field_validator("args")
    @classmethod
    def validate_args(cls, v: dict) -> dict:
        """Ensure prompt arguments pass XSS validation

        Args:
            v (dict): Value to validate

        Returns:
            dict: Value if validated as safe
        """
        for val in v.values():
            SecurityValidator.validate_no_xss(val, "Prompt execution arguments")
        return v


class PromptUpdate(BaseModelWithConfigDict):
    """Schema for updating an existing prompt.

    Similar to PromptCreate but all fields are optional to allow partial updates.
    """

    name: Optional[str] = Field(None, description="Unique name for the prompt")
    custom_name: Optional[str] = Field(None, description="Custom prompt name used for MCP invocation")
    display_name: Optional[str] = Field(None, description="Display name for the prompt (shown in UI)")
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the prompt (MCP BaseMetadata)")
    description: Optional[str] = Field(None, description="Prompt description")
    template: Optional[str] = Field(None, description="Prompt template text")
    arguments: Optional[List[PromptArgument]] = Field(None, description="List of arguments for the template")

    tags: Optional[List[str]] = Field(None, description="Tags for categorizing the prompt")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the prompt owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(None, description="Visibility level: private, team, or public")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Ensure prompt names display correctly in UI

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Prompt name")

    @field_validator("custom_name")
    @classmethod
    def validate_custom_name(cls, v: Optional[str]) -> Optional[str]:
        """Ensure custom prompt names follow MCP naming conventions.

        Args:
            v: Custom prompt name to validate.

        Returns:
            The validated custom name or None.
        """
        if v is None:
            return v
        return SecurityValidator.validate_name(v, "Prompt name")

    @field_validator("display_name")
    @classmethod
    def validate_display_name(cls, v: Optional[str]) -> Optional[str]:
        """Ensure display names render safely in UI.

        Args:
            v: Display name to validate.

        Returns:
            The validated display name or None.
        """
        if v is None:
            return v
        return SecurityValidator.sanitize_display_text(v, "Prompt display name")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import PromptUpdate
            >>> PromptUpdate.validate_description('A safe description')
            'A safe description'
            >>> PromptUpdate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = PromptUpdate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("template")
    @classmethod
    def validate_template(cls, v: str) -> str:
        """Validate template content for safe display

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_template(v)

    @field_validator("arguments")
    @classmethod
    def validate_arguments(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Ensure JSON structure is valid and within complexity limits

        Args:
            v (dict): Value to validate

        Returns:
            dict: Value if validated as safe
        """
        SecurityValidator.validate_json_depth(v)
        return v


class PromptRead(BaseModelWithConfigDict):
    """Schema for reading prompt information.

    Includes all prompt fields plus:
    - Database ID
    - Creation/update timestamps
    - Active status
    - Metrics: Aggregated metrics for the prompt invocations.
    """

    id: str = Field(description="Unique ID of the prompt")
    name: str
    original_name: str
    custom_name: str
    custom_name_slug: str
    display_name: Optional[str] = Field(None, description="Display name for the prompt (shown in UI)")
    gateway_id: Optional[str] = Field(None, description="ID of the gateway for the prompt")
    gateway_slug: Optional[str] = None
    description: Optional[str]
    template: str
    arguments: List[PromptArgument]
    created_at: datetime
    updated_at: datetime
    # is_active: bool
    enabled: bool
    tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the prompt")
    metrics: Optional[PromptMetrics] = Field(None, description="Prompt metrics (may be None in list operations)")

    # Comprehensive metadata for audit tracking
    created_by: Optional[str] = Field(None, description="Username who created this entity")
    created_from_ip: Optional[str] = Field(None, description="IP address of creator")
    created_via: Optional[str] = Field(None, description="Creation method: ui|api|import|federation")
    created_user_agent: Optional[str] = Field(None, description="User agent of creation request")

    modified_by: Optional[str] = Field(None, description="Username who last modified this entity")
    modified_from_ip: Optional[str] = Field(None, description="IP address of last modifier")
    modified_via: Optional[str] = Field(None, description="Modification method")
    modified_user_agent: Optional[str] = Field(None, description="User agent of modification request")

    import_batch_id: Optional[str] = Field(None, description="UUID of bulk import batch")
    federation_source: Optional[str] = Field(None, description="Source gateway for federated entities")
    version: Optional[int] = Field(1, description="Entity version for change tracking")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="ID of the team that owns this resource")
    team: Optional[str] = Field(None, description="Name of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Email of the user who owns this resource")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Visibility level: private, team, or public")

    # MCP protocol fields
    title: Optional[str] = Field(None, max_length=255, description="Human-readable title for the prompt")
    meta: Optional[Dict[str, Any]] = Field(None, alias="_meta", description="Optional metadata for protocol extension")

    _normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))


class PromptInvocation(BaseModelWithConfigDict):
    """Schema for prompt invocation requests.

    Contains:
    - Prompt name to use
    - Arguments for template rendering
    """

    name: str = Field(..., description="Name of prompt to use")
    arguments: Dict[str, str] = Field(default_factory=dict, description="Arguments for template rendering")


# --- Global Config Schemas ---
class GlobalConfigUpdate(BaseModel):
    """Schema for updating global configuration.

    Attributes:
        passthrough_headers (Optional[List[str]]): List of headers allowed to be passed through globally
    """

    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through globally")


class GlobalConfigRead(BaseModel):
    """Schema for reading global configuration.

    Attributes:
        passthrough_headers (Optional[List[str]]): List of headers allowed to be passed through globally
    """

    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through globally")


# --- Gateway Schemas ---


# --- Transport Type ---
class TransportType(str, Enum):
    """
    Enumeration of supported transport mechanisms for communication between components.

    Attributes:
        SSE (str): Server-Sent Events transport.
        HTTP (str): Standard HTTP-based transport.
        STDIO (str): Standard input/output transport.
        STREAMABLEHTTP (str): HTTP transport with streaming.
    """

    SSE = "SSE"
    HTTP = "HTTP"
    STDIO = "STDIO"
    STREAMABLEHTTP = "STREAMABLEHTTP"


class GatewayCreate(BaseModelWithConfigDict):
    """
    Schema for creating a new gateway.

    Attributes:
        model_config (ConfigDict): Configuration for the model.
        name (str): Unique name for the gateway.
        url (Union[str, AnyHttpUrl]): Gateway endpoint URL.
        description (Optional[str]): Optional description of the gateway.
        transport (str): Transport used by the MCP server, default is "SSE".
        auth_type (Optional[str]): Type of authentication (basic, bearer, authheaders, or none).
        auth_username (Optional[str]): Username for basic authentication.
        auth_password (Optional[str]): Password for basic authentication.
        auth_token (Optional[str]): Token for bearer authentication.
        auth_header_key (Optional[str]): Key for custom headers authentication.
        auth_header_value (Optional[str]): Value for custom headers authentication.
        auth_headers (Optional[List[Dict[str, str]]]): List of custom headers for authentication.
        auth_value (Optional[str]): Alias for authentication value, used for better access post-validation.
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    name: str = Field(..., description="Unique name for the gateway")
    url: str = Field(..., description="Gateway endpoint URL")
    description: Optional[str] = Field(None, description="Gateway description")
    transport: str = Field(default="SSE", description="Transport used by MCP server: SSE or STREAMABLEHTTP")
    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through from client to target")

    # Authorizations
    auth_type: Optional[str] = Field(None, description="Type of authentication: basic, bearer, authheaders, oauth, query_param, or none")
    # Fields for various types of authentication
    auth_username: Optional[str] = Field(None, description="Username for basic authentication")
    auth_password: Optional[str] = Field(None, description="Password for basic authentication")
    auth_token: Optional[str] = Field(None, description="Token for bearer authentication")
    auth_header_key: Optional[str] = Field(None, description="Key for custom headers authentication")
    auth_header_value: Optional[str] = Field(None, description="Value for custom headers authentication")
    auth_headers: Optional[List[Dict[str, str]]] = Field(None, description="List of custom headers for authentication")

    # OAuth 2.0 configuration
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")

    # Query Parameter Authentication (INSECURE)
    auth_query_param_key: Optional[str] = Field(
        None,
        description="Query parameter name for authentication (e.g., 'api_key', 'tavilyApiKey')",
        pattern=r"^[a-zA-Z_][a-zA-Z0-9_\-]*$",
    )
    auth_query_param_value: Optional[SecretStr] = Field(
        None,
        description="Query parameter value (API key). Stored encrypted.",
    )

    # Adding `auth_value` as an alias for better access post-validation
    auth_value: Optional[str] = Field(None, validate_default=True)

    # One time auth - do not store the auth in gateway flag
    one_time_auth: Optional[bool] = Field(default=False, description="The authentication should be used only once and not stored in the gateway")

    tags: Optional[List[Union[str, Dict[str, str]]]] = Field(default_factory=list, description="Tags for categorizing the gateway")

    # Team scoping fields for resource organization
    team_id: Optional[str] = Field(None, description="Team ID this gateway belongs to")
    owner_email: Optional[str] = Field(None, description="Email of the gateway owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Gateway visibility: private, team, or public")

    # CA certificate
    ca_certificate: Optional[str] = Field(None, description="Custom CA certificate for TLS verification")
    ca_certificate_sig: Optional[str] = Field(None, description="Signature of the custom CA certificate for integrity verification")
    signing_algorithm: Optional[str] = Field("ed25519", description="Algorithm used for signing the CA certificate")

    # mTLS client certificate/key
    client_cert: Optional[str] = Field(None, description="Client TLS certificate for mTLS authentication")
    client_key: Optional[str] = Field(None, description="Client TLS key for mTLS authentication")

    # Per-gateway refresh configuration
    refresh_interval_seconds: Optional[int] = Field(None, ge=60, description="Per-gateway refresh interval in seconds (minimum 60); uses global default if not set")

    # Gateway mode configuration
    gateway_mode: str = Field(default="cache", description="Gateway mode: 'cache' (database caching, default) or 'direct_proxy' (pass-through mode with no caching)", pattern="^(cache|direct_proxy)$")

    # Per-gateway identity propagation configuration
    identity_propagation: Optional[Dict[str, Any]] = Field(None, description="Per-gateway identity propagation config: {enabled, mode, headers_prefix, sign_claims, allowed_attributes}")

    @field_validator("gateway_mode", mode="before")
    @classmethod
    def default_gateway_mode(cls, v: Optional[str]) -> str:
        """Default gateway_mode to 'cache' when None is provided.

        Args:
            v: Gateway mode value (may be None).

        Returns:
            The validated gateway mode string, defaulting to 'cache'.
        """
        return v if v is not None else "cache"

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate gateway name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Gateway name")

    @field_validator("url")
    @classmethod
    def validate_url(cls, v: str) -> str:
        """Validate gateway URL

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return validate_core_url(v, "Gateway URL")

    @field_validator("oauth_config", mode="before")
    @classmethod
    def validate_oauth_config(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        """Validate URL-bearing OAuth configuration entries."""
        return _validate_oauth_config_urls(v)

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import GatewayCreate
            >>> GatewayCreate.validate_description('A safe description')
            'A safe description'
            >>> GatewayCreate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ToolCreate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("auth_value", mode="before")
    @classmethod
    def create_auth_value(cls, v, info):
        """
        This validator will run before the model is fully instantiated (mode="before")
        It will process the auth fields based on auth_type and generate auth_value.

        Args:
            v: Input url
            info: ValidationInfo containing auth_type

        Returns:
            str: Auth value
        """
        data = info.data
        auth_type = data.get("auth_type")

        if (auth_type is None) or (auth_type == ""):
            return v  # If no auth_type is provided, no need to create auth_value

        # Process the auth fields and generate auth_value based on auth_type
        auth_value = cls._process_auth_fields(info)
        return auth_value

    @field_validator("transport")
    @classmethod
    def validate_transport(cls, v: str) -> str:
        """
        Validates that the given transport value is one of the supported TransportType values.

        Args:
            v (str): The transport value to validate.

        Returns:
            str: The validated transport value if it is valid.

        Raises:
            ValueError: If the provided value is not a valid transport type.

        Valid transport types are defined in the TransportType enum:
            - SSE
            - HTTP
            - STDIO
            - STREAMABLEHTTP
        """
        allowed = [t.value for t in TransportType.__members__.values()]
        if v not in allowed:
            raise ValueError(f"Invalid transport type: {v}. Must be one of: {', '.join(allowed)}")
        return v

    @staticmethod
    def _process_auth_fields(info: ValidationInfo) -> Optional[str]:
        """
        Processes the input authentication fields and returns the correct auth_value.
        This method is called based on the selected auth_type.

        Args:
            info: ValidationInfo containing auth fields

        Returns:
            Encoded auth string or None

        Raises:
            ValueError: If auth_type is invalid
        """
        data = info.data
        auth_type = data.get("auth_type")

        if auth_type == "basic":
            # For basic authentication, both username and password must be present
            username = data.get("auth_username")
            password = data.get("auth_password")

            if not username or not password:
                raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")

            creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()
            return encode_auth({"Authorization": f"Basic {creds}"})

        if auth_type == "bearer":
            # For bearer authentication, only token is required
            token = data.get("auth_token")

            if not token:
                raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")

            return encode_auth({"Authorization": f"Bearer {token}"})

        if auth_type == "oauth":
            # For OAuth authentication, we don't encode anything here
            # The OAuth configuration is handled separately in the oauth_config field
            # This method is only called for traditional auth types
            return None

        if auth_type == "authheaders":
            # Support both new multi-headers format and legacy single header format
            auth_headers = data.get("auth_headers")
            if auth_headers and isinstance(auth_headers, list):
                # New multi-headers format with enhanced validation
                header_dict = {}
                duplicate_keys = set()

                for header in auth_headers:
                    if not isinstance(header, dict):
                        continue

                    key = header.get("key")
                    value = header.get("value", "")

                    # Skip headers without keys
                    if not key:
                        continue

                    # Track duplicate keys (last value wins)
                    if key in header_dict:
                        duplicate_keys.add(key)

                    # Validate header key format (basic HTTP header validation)
                    if not all(c.isalnum() or c in "-_" for c in key.replace(" ", "")):
                        raise ValueError(f"Invalid header key format: '{key}'. Header keys should contain only alphanumeric characters, hyphens, and underscores.")

                    # Store header (empty values are allowed)
                    header_dict[key] = value

                # Ensure at least one valid header
                if not header_dict:
                    raise ValueError("For 'authheaders' auth, at least one valid header with a key must be provided.")

                # Warn about duplicate keys (optional - could log this instead)
                if duplicate_keys:
                    logger.warning(f"Duplicate header keys detected (last value used): {', '.join(duplicate_keys)}")

                # Check for excessive headers (prevent abuse)
                if len(header_dict) > 100:
                    raise ValueError("Maximum of 100 headers allowed per gateway.")

                return encode_auth(header_dict)

            # Legacy single header format (backward compatibility)
            header_key = data.get("auth_header_key")
            header_value = data.get("auth_header_value")

            if not header_key or not header_value:
                raise ValueError("For 'authheaders' auth, either 'auth_headers' list or both 'auth_header_key' and 'auth_header_value' must be provided.")

            return encode_auth({header_key: header_value})

        if auth_type == "one_time_auth":
            return None  # No auth_value needed for one-time auth

        if auth_type == "query_param":
            # Query param auth doesn't use auth_value field
            # Validation is handled by model_validator
            return None

        raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, oauth, authheaders, or query_param.")

    @model_validator(mode="after")
    def validate_query_param_auth(self) -> "GatewayCreate":
        """Validate query parameter authentication configuration.

        Returns:
            GatewayCreate: The validated instance.

        Raises:
            ValueError: If query param auth is disabled or host is not in allowlist.
        """
        if self.auth_type != "query_param":
            return self

        # Check feature flag
        if not settings.insecure_allow_queryparam_auth:
            raise ValueError("Query parameter authentication is disabled. " + "Set INSECURE_ALLOW_QUERYPARAM_AUTH=true to enable. " + "WARNING: API keys in URLs may appear in proxy logs.")

        # Check required fields
        if not self.auth_query_param_key:
            raise ValueError("auth_query_param_key is required when auth_type is 'query_param'")
        if not self.auth_query_param_value:
            raise ValueError("auth_query_param_value is required when auth_type is 'query_param'")

        # Check host allowlist (if configured)
        if settings.insecure_queryparam_auth_allowed_hosts:
            parsed = urlparse(str(self.url))
            # Extract hostname properly (handles IPv6, ports, userinfo)
            hostname = parsed.hostname or ""
            hostname = hostname.lower()

            if hostname not in settings.insecure_queryparam_auth_allowed_hosts:
                allowed = ", ".join(settings.insecure_queryparam_auth_allowed_hosts)
                raise ValueError(f"Host '{hostname}' is not in the allowed hosts for query parameter auth. Allowed hosts: {allowed}")

        return self


class GatewayUpdate(BaseModelWithConfigDict):
    """Schema for updating an existing federation gateway.

    Similar to GatewayCreate but all fields are optional to allow partial updates.
    """

    name: Optional[str] = Field(None, description="Unique name for the gateway")
    url: Optional[str] = Field(None, description="Gateway endpoint URL")
    description: Optional[str] = Field(None, description="Gateway description")
    transport: Optional[str] = Field(None, description="Transport used by MCP server: SSE or STREAMABLEHTTP")

    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through from client to target")

    # Authorizations
    auth_type: Optional[str] = Field(None, description="auth_type: basic, bearer, authheaders or None")
    auth_username: Optional[str] = Field(None, description="username for basic authentication")
    auth_password: Optional[str] = Field(None, description="password for basic authentication")
    auth_token: Optional[str] = Field(None, description="token for bearer authentication")
    auth_header_key: Optional[str] = Field(None, description="key for custom headers authentication")
    auth_header_value: Optional[str] = Field(None, description="value for custom headers authentication")
    auth_headers: Optional[List[Dict[str, str]]] = Field(None, description="List of custom headers for authentication")

    # Adding `auth_value` as an alias for better access post-validation
    auth_value: Optional[str] = Field(None, validate_default=True)

    # OAuth 2.0 configuration
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")

    # Query Parameter Authentication (INSECURE)
    auth_query_param_key: Optional[str] = Field(
        None,
        description="Query parameter name for authentication",
        pattern=r"^[a-zA-Z_][a-zA-Z0-9_\-]*$",
    )
    auth_query_param_value: Optional[SecretStr] = Field(
        None,
        description="Query parameter value (API key)",
    )

    # One time auth - do not store the auth in gateway flag
    one_time_auth: Optional[bool] = Field(default=False, description="The authentication should be used only once and not stored in the gateway")

    tags: Optional[List[Union[str, Dict[str, str]]]] = Field(None, description="Tags for categorizing the gateway")

    # Team scoping fields for resource organization
    team_id: Optional[str] = Field(None, description="Team ID this gateway belongs to")
    owner_email: Optional[str] = Field(None, description="Email of the gateway owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(None, description="Gateway visibility: private, team, or public")

    # Per-gateway refresh configuration
    refresh_interval_seconds: Optional[int] = Field(None, ge=60, description="Per-gateway refresh interval in seconds (minimum 60); uses global default if not set")

    # Gateway mode configuration
    gateway_mode: Optional[str] = Field(None, description="Gateway mode: 'cache' (database caching, default) or 'direct_proxy' (pass-through mode with no caching)", pattern="^(cache|direct_proxy)$")

    # CA certificate configuration for custom TLS trust
    ca_certificate: Optional[str] = Field(None, description="Custom CA certificate for TLS verification")
    ca_certificate_sig: Optional[str] = Field(None, description="Signature of the custom CA certificate")
    signing_algorithm: Optional[str] = Field(None, description="Algorithm used for signing the CA certificate")

    # mTLS client TLS certificate and key
    client_cert: Optional[str] = Field(None, description="Client TLS certificate for mTLS gateway authentication")
    client_key: Optional[str] = Field(None, description="Client TLS key for mTLS gateway authentication")

    # Per-gateway identity propagation configuration
    identity_propagation: Optional[Dict[str, Any]] = Field(None, description="Per-gateway identity propagation config: {enabled, mode, headers_prefix, sign_claims, allowed_attributes}")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("name", mode="before")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate gateway name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Gateway name")

    @field_validator("url", mode="before")
    @classmethod
    def validate_url(cls, v: str) -> str:
        """Validate gateway URL

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return validate_core_url(v, "Gateway URL")

    @field_validator("oauth_config", mode="before")
    @classmethod
    def validate_oauth_config(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        """Validate URL-bearing OAuth configuration entries."""
        return _validate_oauth_config_urls(v)

    @field_validator("description", mode="before")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import GatewayUpdate
            >>> GatewayUpdate.validate_description('A safe description')
            'A safe description'
            >>> GatewayUpdate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ToolCreate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("auth_value", mode="before")
    @classmethod
    def create_auth_value(cls, v, info):
        """
        This validator will run before the model is fully instantiated (mode="before")
        It will process the auth fields based on auth_type and generate auth_value.

        Args:
            v: Input URL
            info: ValidationInfo containing auth_type

        Returns:
            str: Auth value or URL
        """
        data = info.data
        auth_type = data.get("auth_type")

        if (auth_type is None) or (auth_type == ""):
            return v  # If no auth_type is provided, no need to create auth_value

        # Process the auth fields and generate auth_value based on auth_type
        auth_value = cls._process_auth_fields(info)
        return auth_value

    @staticmethod
    def _process_auth_fields(info: ValidationInfo) -> Optional[str]:
        """
        Processes the input authentication fields and returns the correct auth_value.
        This method is called based on the selected auth_type.

        Args:
            info: ValidationInfo containing auth fields

        Returns:
            Encoded auth string or None

        Raises:
            ValueError: If auth type is invalid
        """

        data = info.data
        auth_type = data.get("auth_type")

        if auth_type == "basic":
            # For basic authentication, both username and password must be present
            username = data.get("auth_username")
            password = data.get("auth_password")
            if not username or not password:
                raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")

            creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()
            return encode_auth({"Authorization": f"Basic {creds}"})

        if auth_type == "bearer":
            # For bearer authentication, only token is required
            token = data.get("auth_token")

            if not token:
                raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")

            return encode_auth({"Authorization": f"Bearer {token}"})

        if auth_type == "oauth":
            # For OAuth authentication, we don't encode anything here
            # The OAuth configuration is handled separately in the oauth_config field
            # This method is only called for traditional auth types
            return None

        if auth_type == "authheaders":
            # Support both new multi-headers format and legacy single header format
            auth_headers = data.get("auth_headers")
            if auth_headers and isinstance(auth_headers, list):
                # New multi-headers format with enhanced validation
                header_dict = {}
                duplicate_keys = set()

                for header in auth_headers:
                    if not isinstance(header, dict):
                        continue

                    key = header.get("key")
                    value = header.get("value", "")

                    # Skip headers without keys
                    if not key:
                        continue

                    # Track duplicate keys (last value wins)
                    if key in header_dict:
                        duplicate_keys.add(key)

                    # Validate header key format (basic HTTP header validation)
                    if not all(c.isalnum() or c in "-_" for c in key.replace(" ", "")):
                        raise ValueError(f"Invalid header key format: '{key}'. Header keys should contain only alphanumeric characters, hyphens, and underscores.")

                    # Store header (empty values are allowed)
                    header_dict[key] = value

                # Ensure at least one valid header
                if not header_dict:
                    raise ValueError("For 'authheaders' auth, at least one valid header with a key must be provided.")

                # Warn about duplicate keys (optional - could log this instead)
                if duplicate_keys:
                    logger.warning(f"Duplicate header keys detected (last value used): {', '.join(duplicate_keys)}")

                # Check for excessive headers (prevent abuse)
                if len(header_dict) > 100:
                    raise ValueError("Maximum of 100 headers allowed per gateway.")

                return encode_auth(header_dict)

            # Legacy single header format (backward compatibility)
            header_key = data.get("auth_header_key")
            header_value = data.get("auth_header_value")

            if not header_key or not header_value:
                raise ValueError("For 'authheaders' auth, either 'auth_headers' list or both 'auth_header_key' and 'auth_header_value' must be provided.")

            return encode_auth({header_key: header_value})

        if auth_type == "one_time_auth":
            return None  # No auth_value needed for one-time auth

        if auth_type == "query_param":
            # Query param auth doesn't use auth_value field
            # Validation is handled by model_validator
            return None

        raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, oauth, authheaders, or query_param.")

    @model_validator(mode="after")
    def validate_query_param_auth(self) -> "GatewayUpdate":
        """Validate query parameter authentication configuration.

        NOTE: This only runs when auth_type is explicitly set to "query_param".
        Service-layer enforcement in update_gateway() handles the case where
        auth_type is omitted but the existing gateway uses query_param auth.

        Returns:
            GatewayUpdate: The validated instance.

        Raises:
            ValueError: If required fields are missing when setting query_param auth.
        """
        if self.auth_type == "query_param":
            # Validate fields are provided when explicitly setting query_param auth
            # Feature flag/allowlist check happens in service layer (has access to existing gateway)
            if not self.auth_query_param_key:
                raise ValueError("auth_query_param_key is required when setting auth_type to 'query_param'")
            if not self.auth_query_param_value:
                raise ValueError("auth_query_param_value is required when setting auth_type to 'query_param'")

        return self


# ---------------------------------------------------------------------------
# OAuth config masking helper (used by GatewayRead.masked / A2AAgentRead.masked)
# ---------------------------------------------------------------------------
_SENSITIVE_OAUTH_KEYS = OAUTH_SENSITIVE_KEYS


def _mask_oauth_config(oauth_config: Any) -> Any:
    """Recursively mask sensitive keys inside an ``oauth_config`` dict.

    Args:
        oauth_config: The oauth_config value to mask (dict, list, or scalar).

    Returns:
        The masked copy with sensitive values replaced.
    """
    if isinstance(oauth_config, dict):
        out: Dict[str, Any] = {}
        for k, v in oauth_config.items():
            if isinstance(k, str) and k.lower() in _SENSITIVE_OAUTH_KEYS:
                out[k] = settings.masked_auth_value if v else v
            else:
                out[k] = _mask_oauth_config(v)
        return out
    if isinstance(oauth_config, list):
        return [_mask_oauth_config(x) for x in oauth_config]
    return oauth_config


class GatewayRead(BaseModelWithConfigDict):
    """Schema for reading gateway information.

    Includes all gateway fields plus:
    - Database ID
    - Capabilities dictionary
    - Creation/update timestamps
    - enabled status
    - reachable status
    - Last seen timestamp
    - Authentication type: basic, bearer, authheaders, oauth
    - Authentication value: username/password or token or custom headers
    - OAuth configuration for OAuth 2.0 authentication

    Auto Populated fields:
    - Authentication username: for basic auth
    - Authentication password: for basic auth
    - Authentication token: for bearer auth
    - Authentication header key: for authheaders auth
    - Authentication header value: for authheaders auth
    """

    id: Optional[str] = Field(None, description="Unique ID of the gateway")
    name: str = Field(..., description="Unique name for the gateway")
    url: str = Field(..., description="Gateway endpoint URL")
    description: Optional[str] = Field(None, description="Gateway description")
    transport: str = Field(default="SSE", description="Transport used by MCP server: SSE or STREAMABLEHTTP")
    capabilities: Dict[str, Any] = Field(default_factory=dict, description="Gateway capabilities")
    created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Creation timestamp")
    updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Last update timestamp")
    enabled: bool = Field(default=True, description="Is the gateway enabled?")
    reachable: bool = Field(default=True, description="Is the gateway reachable/online?")

    last_seen: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), description="Last seen timestamp")

    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through from client to target")
    ca_certificate: Optional[str] = Field(default=None, description="Custom CA certificate for TLS verification")
    ca_certificate_sig: Optional[str] = Field(default=None, description="Signature of the custom CA certificate")
    signing_algorithm: Optional[str] = Field(default="ed25519", description="Algorithm used for signing the CA certificate")
    client_cert: Optional[str] = Field(default=None, description="Client TLS certificate for mTLS authentication")
    client_key: Optional[str] = Field(default=None, description="Client TLS key for mTLS authentication")
    # Authorizations
    auth_type: Optional[str] = Field(None, description="auth_type: basic, bearer, authheaders, oauth, query_param, or None")
    auth_value: Optional[str] = Field(None, description="auth value: username/password or token or custom headers")
    auth_headers: Optional[List[Dict[str, str]]] = Field(default=None, description="List of custom headers for authentication")
    auth_headers_unmasked: Optional[List[Dict[str, str]]] = Field(default=None, description="Unmasked custom headers for administrative views")

    # OAuth 2.0 configuration
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")

    # Query Parameter Authentication (masked for security)
    auth_query_param_key: Optional[str] = Field(
        None,
        description="Query parameter name for authentication",
    )
    auth_query_param_value_masked: Optional[str] = Field(
        None,
        description="Masked indicator if query param auth is configured",
    )

    # auth_value will populate the following fields
    auth_username: Optional[str] = Field(None, description="username for basic authentication")
    auth_password: Optional[str] = Field(None, description="password for basic authentication")
    auth_token: Optional[str] = Field(None, description="token for bearer authentication")
    auth_header_key: Optional[str] = Field(None, description="key for custom headers authentication")
    auth_header_value: Optional[str] = Field(None, description="vallue for custom headers authentication")
    tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the gateway")

    auth_password_unmasked: Optional[str] = Field(default=None, description="Unmasked password for basic authentication")
    auth_token_unmasked: Optional[str] = Field(default=None, description="Unmasked bearer token for authentication")
    auth_header_value_unmasked: Optional[str] = Field(default=None, description="Unmasked single custom header value")

    # Team scoping fields for resource organization
    team_id: Optional[str] = Field(None, description="Team ID this gateway belongs to")
    team: Optional[str] = Field(None, description="Name of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Email of the gateway owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Gateway visibility: private, team, or public")

    # Comprehensive metadata for audit tracking
    created_by: Optional[str] = Field(None, description="Username who created this entity")
    created_from_ip: Optional[str] = Field(None, description="IP address of creator")
    created_via: Optional[str] = Field(None, description="Creation method: ui|api|import|federation")
    created_user_agent: Optional[str] = Field(None, description="User agent of creation request")

    modified_by: Optional[str] = Field(None, description="Username who last modified this entity")
    modified_from_ip: Optional[str] = Field(None, description="IP address of last modifier")
    modified_via: Optional[str] = Field(None, description="Modification method")
    modified_user_agent: Optional[str] = Field(None, description="User agent of modification request")

    import_batch_id: Optional[str] = Field(None, description="UUID of bulk import batch")
    federation_source: Optional[str] = Field(None, description="Source gateway for federated entities")
    version: Optional[int] = Field(1, description="Entity version for change tracking")

    slug: Optional[str] = Field(None, description="Slug for gateway endpoint URL")

    # Per-gateway refresh configuration
    refresh_interval_seconds: Optional[int] = Field(None, description="Per-gateway refresh interval in seconds")
    last_refresh_at: Optional[datetime] = Field(None, description="Timestamp of last successful refresh")

    # Gateway mode configuration
    gateway_mode: str = Field(default="cache", description="Gateway mode: 'cache' (database caching, default) or 'direct_proxy' (pass-through mode with no caching)")

    # Per-gateway identity propagation configuration
    identity_propagation: Optional[Dict[str, Any]] = Field(None, description="Per-gateway identity propagation config")

    _normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))

    # Tool count (populated from the tools relationship; 0 when not loaded)
    tool_count: int = Field(default=0, description="Number of tools registered for this gateway")

    # Tools skipped during gateway import due to validation errors (transient, not persisted)
    skipped_tools: List[str] = Field(default_factory=list, description="Tools skipped during gateway import due to validation errors")

    @model_validator(mode="before")
    @classmethod
    def _mask_query_param_auth(cls, data: Any) -> Any:
        """Mask query param auth value when constructing from DB model.

        This extracts auth_query_params from the raw data (DB model or dict)
        and populates the masked fields for display.

        Args:
            data: The raw data (dict or ORM model) to process.

        Returns:
            Any: The processed data with masked query param values.
        """
        # Handle dict input
        if isinstance(data, dict):
            auth_query_params = data.get("auth_query_params")
            if auth_query_params and isinstance(auth_query_params, dict):
                # Extract the param key name and set masked value
                first_key = next(iter(auth_query_params.keys()), None)
                if first_key:
                    data["auth_query_param_key"] = first_key
                    data["auth_query_param_value_masked"] = settings.masked_auth_value
        # Handle ORM model input (has auth_query_params attribute)
        elif hasattr(data, "auth_query_params"):
            auth_query_params = getattr(data, "auth_query_params", None)
            if auth_query_params and isinstance(auth_query_params, dict):
                # Convert ORM to dict for modification, preserving all attributes
                # Start with table columns
                data_dict = {c.name: getattr(data, c.name) for c in data.__table__.columns}
                # Preserve dynamically added attributes like 'team' (from relationships)
                for attr in ["team"]:
                    if hasattr(data, attr):
                        data_dict[attr] = getattr(data, attr)
                first_key = next(iter(auth_query_params.keys()), None)
                if first_key:
                    data_dict["auth_query_param_key"] = first_key
                    data_dict["auth_query_param_value_masked"] = settings.masked_auth_value
                return data_dict
        return data

    # This will be the main method to automatically populate fields
    @model_validator(mode="after")
    def _populate_auth(self) -> Self:
        """Populate authentication fields based on auth_type and encoded auth_value.

        This post-validation method decodes the stored authentication value and
        populates the appropriate authentication fields (username/password, token,
        or custom headers) based on the authentication type. It ensures the
        authentication data is properly formatted and accessible through individual
        fields for display purposes.

        The method handles three authentication types:
        - basic: Extracts username and password from Authorization header
        - bearer: Extracts token from Bearer Authorization header
        - authheaders: Extracts custom header key/value pair

        Returns:
            Self: The instance with populated authentication fields:
                - For basic: auth_username and auth_password
                - For bearer: auth_token
                - For authheaders: auth_header_key and auth_header_value

        Raises:
            ValueError: If the authentication data is malformed:
                    - Basic auth missing username or password
                    - Bearer auth missing or improperly formatted Authorization header
                    - Custom headers not exactly one key/value pair

        Examples:
            >>> # Basic auth example
            >>> string_bytes = "admin:secret".encode("utf-8")
            >>> encoded_auth = base64.urlsafe_b64encode(string_bytes).decode("utf-8")
            >>> values = GatewayRead.model_construct(
            ...     auth_type="basic",
            ...     auth_value=encode_auth({"Authorization": f"Basic {encoded_auth}"})
            ... )
            >>> values = GatewayRead._populate_auth(values)
            >>> values.auth_username
            'admin'
            >>> values.auth_password
            'secret'

            >>> # Bearer auth example
            >>> values = GatewayRead.model_construct(
            ...     auth_type="bearer",
            ...     auth_value=encode_auth({"Authorization": "Bearer mytoken123"})
            ... )
            >>> values = GatewayRead._populate_auth(values)
            >>> values.auth_token
            'mytoken123'

            >>> # Custom headers example
            >>> values = GatewayRead.model_construct(
            ...     auth_type='authheaders',
            ...     auth_value=encode_auth({"X-API-Key": "abc123"})
            ... )
            >>> values = GatewayRead._populate_auth(values)
            >>> values.auth_header_key
            'X-API-Key'
            >>> values.auth_header_value
            'abc123'
        """
        auth_type = self.auth_type
        auth_value_encoded = self.auth_value

        # Skip validation logic if masked value
        if auth_value_encoded == settings.masked_auth_value:
            return self

        # Handle OAuth authentication (no auth_value to decode)
        if auth_type == "oauth":
            # OAuth gateways don't have traditional auth_value to decode
            # They use oauth_config instead
            return self

        if auth_type == "one_time_auth":
            # One-time auth gateways don't store auth_value
            return self

        if auth_type == "query_param":
            # Query param auth is handled by the before validator
            # (auth_query_params from DB model is processed there)
            return self

        # If no encoded value is present, nothing to populate
        if not auth_value_encoded:
            return self

        auth_value = decode_auth(auth_value_encoded)
        if auth_type == "basic":
            auth = auth_value.get("Authorization")
            if not (isinstance(auth, str) and auth.startswith("Basic ")):
                raise ValueError("basic auth requires an Authorization header of the form 'Basic <base64>'")
            auth = auth.removeprefix("Basic ")
            u, p = base64.urlsafe_b64decode(auth).decode("utf-8").split(":")
            if not u or not p:
                raise ValueError("basic auth requires both username and password")
            self.auth_username, self.auth_password = u, p
            self.auth_password_unmasked = p

        elif auth_type == "bearer":
            auth = auth_value.get("Authorization")
            if not (isinstance(auth, str) and auth.startswith("Bearer ")):
                raise ValueError("bearer auth requires an Authorization header of the form 'Bearer <token>'")
            self.auth_token = auth.removeprefix("Bearer ")
            self.auth_token_unmasked = self.auth_token

        elif auth_type == "authheaders":
            # For backward compatibility, populate first header in key/value fields
            if not isinstance(auth_value, dict) or len(auth_value) == 0:
                raise ValueError("authheaders requires at least one key/value pair")
            self.auth_headers = [{"key": str(key), "value": "" if value is None else str(value)} for key, value in auth_value.items()]
            self.auth_headers_unmasked = [{"key": str(key), "value": "" if value is None else str(value)} for key, value in auth_value.items()]
            k, v = next(iter(auth_value.items()))
            self.auth_header_key, self.auth_header_value = k, v
            self.auth_header_value_unmasked = v

        return self

    def masked(self) -> "GatewayRead":
        """
        Return a masked version of the model instance with sensitive authentication fields hidden.

        This method creates a dictionary representation of the model data and replaces sensitive fields
        such as `auth_value`, `auth_password`, `auth_token`, and `auth_header_value` with a masked
        placeholder value defined in `settings.masked_auth_value`. Masking is only applied if the fields
        are present and not already masked.

        Args:
            None

        Returns:
            GatewayRead: A new instance of the GatewayRead model with sensitive authentication-related fields
            masked to prevent exposure of sensitive information.

        Notes:
            - The `auth_value` field is only masked if it exists and its value is different from the masking
            placeholder.
            - Other sensitive fields (`auth_password`, `auth_token`, `auth_header_value`) are masked if present.
            - Fields not related to authentication remain unmodified.
        """
        masked_data = self.model_dump()

        # Only mask if auth_value is present and not already masked
        if masked_data.get("auth_value") and masked_data["auth_value"] != settings.masked_auth_value:
            masked_data["auth_value"] = settings.masked_auth_value

        masked_data["auth_password"] = settings.masked_auth_value if masked_data.get("auth_password") else None
        masked_data["auth_token"] = settings.masked_auth_value if masked_data.get("auth_token") else None
        masked_data["auth_header_value"] = settings.masked_auth_value if masked_data.get("auth_header_value") else None
        if masked_data.get("auth_headers"):
            masked_data["auth_headers"] = [
                {
                    "key": header.get("key"),
                    "value": settings.masked_auth_value if header.get("value") else header.get("value"),
                }
                for header in masked_data["auth_headers"]
            ]

        # Mask sensitive keys inside oauth_config (e.g. password, client_secret)
        if masked_data.get("oauth_config"):
            masked_data["oauth_config"] = _mask_oauth_config(masked_data["oauth_config"])

        # SECURITY: Never expose unmasked credentials in API responses
        masked_data["auth_password_unmasked"] = None
        masked_data["auth_token_unmasked"] = None
        masked_data["auth_header_value_unmasked"] = None
        masked_data["auth_headers_unmasked"] = None
        # SECURITY: Mask mTLS client private key
        masked_data["client_key"] = settings.masked_auth_value if masked_data.get("client_key") else None
        return GatewayRead.model_validate(masked_data)


class GatewayRefreshResponse(BaseModelWithConfigDict):
    """Response schema for manual gateway refresh API.

    Contains counts of added, updated, and removed items for tools, resources, and prompts,
    along with any validation errors encountered during the refresh operation.
    """

    gateway_id: str = Field(..., description="ID of the refreshed gateway")
    success: bool = Field(default=True, description="Whether the refresh operation was successful")
    error: Optional[str] = Field(None, description="Error message if the refresh failed")
    tools_added: int = Field(default=0, description="Number of tools added")
    tools_updated: int = Field(default=0, description="Number of tools updated")
    tools_removed: int = Field(default=0, description="Number of tools removed")
    resources_added: int = Field(default=0, description="Number of resources added")
    resources_updated: int = Field(default=0, description="Number of resources updated")
    resources_removed: int = Field(default=0, description="Number of resources removed")
    prompts_added: int = Field(default=0, description="Number of prompts added")
    prompts_updated: int = Field(default=0, description="Number of prompts updated")
    prompts_removed: int = Field(default=0, description="Number of prompts removed")
    validation_errors: List[str] = Field(default_factory=list, description="List of validation errors encountered")
    duration_ms: float = Field(..., description="Duration of the refresh operation in milliseconds")
    refreshed_at: datetime = Field(..., description="Timestamp when the refresh completed")


class FederatedTool(BaseModelWithConfigDict):
    """Schema for tools provided by federated gateways.

    Contains:
    - Tool definition
    - Source gateway information
    """

    tool: MCPTool
    gateway_id: str
    gateway_name: str
    gateway_url: str


class FederatedResource(BaseModelWithConfigDict):
    """Schema for resources from federated gateways.

    Contains:
    - Resource definition
    - Source gateway information
    """

    resource: MCPResource
    gateway_id: str
    gateway_name: str
    gateway_url: str


class FederatedPrompt(BaseModelWithConfigDict):
    """Schema for prompts from federated gateways.

    Contains:
    - Prompt definition
    - Source gateway information
    """

    prompt: MCPPrompt
    gateway_id: str
    gateway_name: str
    gateway_url: str


# --- RPC Schemas ---
class RPCRequest(BaseModel):
    """MCP-compliant RPC request validation"""

    model_config = ConfigDict(hide_input_in_errors=True)

    jsonrpc: Literal["2.0"]
    method: str
    params: Optional[Dict[str, Any]] = None
    id: Optional[Union[int, str]] = None

    @field_validator("method")
    @classmethod
    def validate_method(cls, v: str) -> str:
        """Ensure method names follow MCP format

        Args:
            v (str): Value to validate

        Returns:
            str: Value if determined as safe

        Raises:
            ValueError: When value is not safe
        """
        SecurityValidator.validate_no_xss(v, "RPC method name")
        # Runtime pattern matching (not precompiled to allow test monkeypatching)
        if not re.match(settings.validation_tool_method_pattern, v):
            raise ValueError("Invalid method name format")
        if len(v) > settings.validation_max_method_length:
            raise ValueError("Method name too long")
        return v

    @field_validator("params")
    @classmethod
    def validate_params(cls, v: Optional[Union[Dict, List]]) -> Optional[Union[Dict, List]]:
        """Validate RPC parameters

        Args:
            v (Union[dict, list]): Value to validate

        Returns:
            Union[dict, list]: Value if determined as safe

        Raises:
            ValueError: When value is not safe
        """
        if v is None:
            return v

        # Check size limits (MCP recommends max 256KB for params)
        param_size = len(orjson.dumps(v))
        if param_size > settings.validation_max_rpc_param_size:
            raise ValueError(f"Parameters exceed maximum size of {settings.validation_max_rpc_param_size} bytes")

        # Check depth
        SecurityValidator.validate_json_depth(v)
        return v


class RPCResponse(BaseModelWithConfigDict):
    """Schema for JSON-RPC 2.0 responses.

    Contains:
    - Protocol version
    - Result or error
    - Request ID
    """

    jsonrpc: Literal["2.0"]
    result: Optional[Any] = None
    error: Optional[Dict[str, Any]] = None
    id: Optional[Union[int, str]] = None


# --- Event and Admin Schemas ---


class EventMessage(BaseModelWithConfigDict):
    """Schema for SSE event messages.

    Includes:
    - Event type
    - Event data payload
    - Event timestamp
    """

    type: str = Field(..., description="Event type (tool_added, resource_updated, etc)")
    data: Dict[str, Any] = Field(..., description="Event payload")
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

    @field_serializer("timestamp")
    def serialize_timestamp(self, dt: datetime) -> str:
        """
        Serialize the `timestamp` field as an ISO 8601 string with UTC timezone.

        Converts the given datetime to UTC and returns it in ISO 8601 format,
        replacing the "+00:00" suffix with "Z" to indicate UTC explicitly.

        Args:
            dt (datetime): The datetime object to serialize.

        Returns:
            str: ISO 8601 formatted string in UTC, ending with 'Z'.
        """
        return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


class AdminToolCreate(BaseModelWithConfigDict):
    """Schema for creating tools via admin UI.

    Handles:
    - Basic tool information
    - JSON string inputs for headers/schema
    """

    name: str
    url: str
    description: Optional[str] = None
    integration_type: str = "MCP"
    headers: Optional[str] = None  # JSON string
    input_schema: Optional[str] = None  # JSON string

    @field_validator("headers", "input_schema")
    @classmethod
    def validate_json(cls, v: Optional[str]) -> Optional[Dict[str, Any]]:
        """
        Validate and parse JSON string inputs.

        Args:
            v: Input string

        Returns:
            dict: Output JSON version of v

        Raises:
            ValueError: When unable to convert to JSON
        """
        if not v:
            return None
        try:
            return orjson.loads(v)
        except orjson.JSONDecodeError:
            raise ValueError("Invalid JSON")


class AdminGatewayCreate(BaseModelWithConfigDict):
    """Schema for creating gateways via admin UI.

    Captures:
    - Gateway name
    - Endpoint URL
    - Optional description
    """

    name: str
    url: str
    description: Optional[str] = None


# --- New Schemas for Status Toggle Operations ---


class StatusToggleRequest(BaseModelWithConfigDict):
    """Request schema for toggling active status."""

    activate: bool = Field(..., description="Whether to activate (true) or deactivate (false) the item")


class StatusToggleResponse(BaseModelWithConfigDict):
    """Response schema for status toggle operations."""

    id: int
    name: str
    is_active: bool
    message: str = Field(..., description="Success message")


# --- Optional Filter Parameters for Listing Operations ---


class ListFilters(BaseModelWithConfigDict):
    """Filtering options for list operations."""

    include_inactive: bool = Field(False, description="Whether to include inactive items in the results")


# --- Server Schemas ---


class ServerCreate(BaseModel):
    """
    Schema for creating a new server.

    Attributes:
        model_config (ConfigDict): Configuration for the model, such as stripping whitespace from strings.
        name (str): The server's name.
        description (Optional[str]): Optional description of the server.
        icon (Optional[str]): Optional URL for the server's icon.
        associated_tools (Optional[List[str]]): Optional list of associated tool IDs.
        associated_resources (Optional[List[str]]): Optional list of associated resource IDs.
        associated_prompts (Optional[List[str]]): Optional list of associated prompt IDs.
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    id: Optional[str] = Field(None, description="Custom UUID for the server (if not provided, one will be generated)")
    name: str = Field(..., description="The server's name")
    description: Optional[str] = Field(None, description="Server description")
    icon: Optional[str] = Field(None, description="URL for the server's icon")
    tags: Optional[List[str]] = Field(default_factory=list, description="Tags for categorizing the server")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("id")
    @classmethod
    def validate_id(cls, v: Optional[str]) -> Optional[str]:
        """Validate server ID/UUID format

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe

        Raises:
            ValueError: When displayName contains unsafe content or exceeds length limits

        Examples:
            >>> from mcpgateway.schemas import ServerCreate
            >>> ServerCreate.validate_id('550e8400-e29b-41d4-a716-446655440000')
            '550e8400e29b41d4a716446655440000'
            >>> ServerCreate.validate_id('invalid-uuid')
            Traceback (most recent call last):
                ...
            ValueError: ...
        """
        if v is None:
            return v
        return SecurityValidator.validate_uuid(v, "Server ID")

    associated_tools: Optional[List[str]] = Field(None, description="Comma-separated tool IDs")
    associated_resources: Optional[List[str]] = Field(None, description="Comma-separated resource IDs")
    associated_prompts: Optional[List[str]] = Field(None, description="Comma-separated prompt IDs")
    associated_a2a_agents: Optional[List[str]] = Field(None, description="Comma-separated A2A agent IDs")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the server owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Visibility level: private, team, or public")

    # OAuth 2.0 configuration for RFC 9728 Protected Resource Metadata
    oauth_enabled: bool = Field(False, description="Enable OAuth 2.0 for MCP client authentication")
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration (authorization_server, scopes_supported, etc.)")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate server name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Server name")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import ServerCreate
            >>> ServerCreate.validate_description('A safe description')
            'A safe description'
            >>> ServerCreate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ServerCreate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("icon")
    @classmethod
    def validate_icon(cls, v: Optional[str]) -> Optional[str]:
        """Validate icon URL

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        if v is None or v == "":
            return v
        return validate_core_url(v, "Icon URL")

    @field_validator("associated_tools", "associated_resources", "associated_prompts", "associated_a2a_agents", mode="before")
    @classmethod
    def split_comma_separated(cls, v: Any, info: ValidationInfo) -> Any:
        """Split comma-separated string and validate UUID format.

        Args:
            v: Input string or list of IDs.
            info: Pydantic validation info providing the field name.

        Returns:
            List of validated, normalized UUID strings, or the original value.
        """
        return _validate_association_ids(v, info.field_name)

    @field_validator("team_id")
    @classmethod
    def validate_team_id(cls, v: Optional[str]) -> Optional[str]:
        """Validate team ID format.

        Args:
            v: Team ID to validate

        Returns:
            Validated team ID
        """
        if v is not None:
            return SecurityValidator.validate_uuid(v, "team_id")
        return v


class ServerUpdate(BaseModelWithConfigDict):
    """Schema for updating an existing server.

    All fields are optional to allow partial updates.
    """

    id: Optional[str] = Field(None, description="Custom UUID for the server")
    name: Optional[str] = Field(None, description="The server's name")
    description: Optional[str] = Field(None, description="Server description")
    icon: Optional[str] = Field(None, description="URL for the server's icon")
    tags: Optional[List[str]] = Field(None, description="Tags for categorizing the server")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the server owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(None, description="Visibility level: private, team, or public")

    # OAuth 2.0 configuration for RFC 9728 Protected Resource Metadata
    oauth_enabled: Optional[bool] = Field(None, description="Enable OAuth 2.0 for MCP client authentication")
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration (authorization_server, scopes_supported, etc.)")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("id")
    @classmethod
    def validate_id(cls, v: Optional[str]) -> Optional[str]:
        """Validate server ID/UUID format

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe

        Raises:
            ValueError: When displayName contains unsafe content or exceeds length limits

        Examples:
            >>> from mcpgateway.schemas import ServerUpdate
            >>> ServerUpdate.validate_id('550e8400-e29b-41d4-a716-446655440000')
            '550e8400e29b41d4a716446655440000'
            >>> ServerUpdate.validate_id('invalid-uuid')
            Traceback (most recent call last):
                ...
            ValueError: ...
        """
        if v is None:
            return v
        return SecurityValidator.validate_uuid(v, "Server ID")

    associated_tools: Optional[List[str]] = Field(None, description="Comma-separated tool IDs")
    associated_resources: Optional[List[str]] = Field(None, description="Comma-separated resource IDs")
    associated_prompts: Optional[List[str]] = Field(None, description="Comma-separated prompt IDs")
    associated_a2a_agents: Optional[List[str]] = Field(None, description="Comma-separated A2A agent IDs")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate server name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Server name")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import ServerUpdate
            >>> ServerUpdate.validate_description('A safe description')
            'A safe description'
            >>> ServerUpdate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = ServerUpdate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("icon")
    @classmethod
    def validate_icon(cls, v: Optional[str]) -> Optional[str]:
        """Validate icon URL

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        if v is None or v == "":
            return v
        return validate_core_url(v, "Icon URL")

    @field_validator("associated_tools", "associated_resources", "associated_prompts", "associated_a2a_agents", mode="before")
    @classmethod
    def split_comma_separated(cls, v: Any, info: ValidationInfo) -> Any:
        """Split comma-separated string and validate UUID format.

        Args:
            v: Input string or list of IDs.
            info: Pydantic validation info providing the field name.

        Returns:
            List of validated, normalized UUID strings, or the original value.
        """
        return _validate_association_ids(v, info.field_name)


class ServerRead(BaseModelWithConfigDict):
    """Schema for reading server information.

    Includes all server fields plus:
    - Database ID
    - Associated tool, resource, and prompt IDs
    - Creation/update timestamps
    - Active status
    - Metrics: Aggregated metrics for the server invocations.
    """

    id: str
    name: str
    description: Optional[str]
    icon: Optional[str]
    created_at: datetime
    updated_at: datetime
    # is_active: bool
    enabled: bool
    associated_tools: List[str] = []
    associated_tool_ids: List[str] = []
    associated_resources: List[str] = []
    associated_prompts: List[str] = []
    associated_a2a_agents: List[str] = []
    metrics: Optional[ServerMetrics] = Field(None, description="Server metrics (may be None in list operations)")
    tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the server")

    # Comprehensive metadata for audit tracking
    created_by: Optional[str] = Field(None, description="Username who created this entity")
    created_from_ip: Optional[str] = Field(None, description="IP address of creator")
    created_via: Optional[str] = Field(None, description="Creation method: ui|api|import|federation")
    created_user_agent: Optional[str] = Field(None, description="User agent of creation request")

    modified_by: Optional[str] = Field(None, description="Username who last modified this entity")
    modified_from_ip: Optional[str] = Field(None, description="IP address of last modifier")
    modified_via: Optional[str] = Field(None, description="Modification method")
    modified_user_agent: Optional[str] = Field(None, description="User agent of modification request")

    import_batch_id: Optional[str] = Field(None, description="UUID of bulk import batch")
    federation_source: Optional[str] = Field(None, description="Source gateway for federated entities")
    version: Optional[int] = Field(1, description="Entity version for change tracking")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="ID of the team that owns this resource")
    team: Optional[str] = Field(None, description="Name of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Email of the user who owns this resource")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Visibility level: private, team, or public")

    # OAuth 2.0 configuration for RFC 9728 Protected Resource Metadata
    oauth_enabled: bool = Field(False, description="Whether OAuth 2.0 is enabled for MCP client authentication")
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration (authorization_server, scopes_supported, etc.)")

    _normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))

    @model_validator(mode="before")
    @classmethod
    def populate_associated_ids(cls, values):
        """
        Pre-validation method that converts associated objects to their 'id'.

        This method checks 'associated_tools', 'associated_resources', and
        'associated_prompts' in the input and replaces each object with its `id`
        if present.

        Args:
            values (dict): The input values.

        Returns:
            dict: Updated values with object ids, or the original values if no
            changes are made.
        """
        # Normalize to a mutable dict
        if isinstance(values, dict):
            data = dict(values)
        else:
            try:
                data = dict(vars(values))
            except Exception:
                return values

        if data.get("associated_tools"):
            data["associated_tools"] = [getattr(tool, "id", tool) for tool in data["associated_tools"]]
        if data.get("associated_resources"):
            data["associated_resources"] = [getattr(res, "id", res) for res in data["associated_resources"]]
        if data.get("associated_prompts"):
            data["associated_prompts"] = [getattr(prompt, "id", prompt) for prompt in data["associated_prompts"]]
        if data.get("associated_a2a_agents"):
            data["associated_a2a_agents"] = [getattr(agent, "id", agent) for agent in data["associated_a2a_agents"]]
        return data

    def masked(self) -> "ServerRead":
        """Return a masked model with oauth_config secrets redacted.

        Returns:
            ServerRead: Masked server model.
        """
        masked_data = self.model_dump()
        if masked_data.get("oauth_config"):
            masked_data["oauth_config"] = _mask_oauth_config(masked_data["oauth_config"])
        return ServerRead.model_validate(masked_data)


class GatewayTestRequest(BaseModelWithConfigDict):
    """Schema for testing gateway connectivity.

    Includes the HTTP method, base URL, path, optional headers, body, and content type.
    """

    method: str = Field(..., description="HTTP method to test (GET, POST, etc.)")
    base_url: AnyHttpUrl = Field(..., description="Base URL of the gateway to test")
    path: str = Field(..., description="Path to append to the base URL")
    headers: Optional[Dict[str, str]] = Field(None, description="Optional headers for the request")
    body: Optional[Union[str, Dict[str, Any]]] = Field(None, description="Optional body for the request, can be a string or JSON object")
    content_type: Optional[str] = Field("application/json", description="Content type for the request body")


class GatewayTestResponse(BaseModelWithConfigDict):
    """Schema for the response from a gateway test request.

    Contains:
    - HTTP status code
    - Latency in milliseconds
    - Optional response body, which can be a string or JSON object
    """

    status_code: int = Field(..., description="HTTP status code returned by the gateway")
    latency_ms: int = Field(..., description="Latency of the request in milliseconds")
    body: Optional[Union[str, Dict[str, Any]]] = Field(None, description="Response body, can be a string or JSON object")


class TaggedEntity(BaseModelWithConfigDict):
    """A simplified representation of an entity that has a tag."""

    id: str = Field(..., description="The entity's ID")
    name: str = Field(..., description="The entity's name")
    type: str = Field(..., description="The entity type (tool, resource, prompt, server, gateway)")
    description: Optional[str] = Field(None, description="The entity's description")


class TagStats(BaseModelWithConfigDict):
    """Statistics for a single tag across all entity types."""

    tools: int = Field(default=0, description="Number of tools with this tag")
    resources: int = Field(default=0, description="Number of resources with this tag")
    prompts: int = Field(default=0, description="Number of prompts with this tag")
    servers: int = Field(default=0, description="Number of servers with this tag")
    gateways: int = Field(default=0, description="Number of gateways with this tag")
    total: int = Field(default=0, description="Total occurrences of this tag")


class TagInfo(BaseModelWithConfigDict):
    """Information about a single tag."""

    name: str = Field(..., description="The tag name")
    stats: TagStats = Field(..., description="Statistics for this tag")
    entities: Optional[List[TaggedEntity]] = Field(default_factory=list, description="Entities that have this tag")


class TopPerformer(BaseModelWithConfigDict):
    """Schema for representing top-performing entities with performance metrics.

    Used to encapsulate metrics for entities such as prompts, resources, servers, or tools,
    including execution count, average response time, success rate, and last execution timestamp.

    Attributes:
        id (Union[str, int]): Unique identifier for the entity.
        name (str): Name of the entity (e.g., prompt name, resource URI, server name, or tool name).
        execution_count (int): Total number of executions for the entity.
        avg_response_time (Optional[float]): Average response time in seconds, or None if no metrics.
        success_rate (Optional[float]): Success rate percentage, or None if no metrics.
        last_execution (Optional[datetime]): Timestamp of the last execution, or None if no metrics.
    """

    id: Union[str, int] = Field(..., description="Entity ID")
    name: str = Field(..., description="Entity name")
    execution_count: int = Field(..., description="Number of executions")
    avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
    success_rate: Optional[float] = Field(None, description="Success rate percentage")
    last_execution: Optional[datetime] = Field(None, description="Timestamp of last execution")


# --- A2A Agent Schemas ---


class A2AAgentCreate(BaseModel):
    """
    Schema for creating a new A2A (Agent-to-Agent) compatible agent.

    Attributes:
        model_config (ConfigDict): Configuration for the model.
        name (str): Unique name for the agent.
        description (Optional[str]): Optional description of the agent.
        endpoint_url (str): URL endpoint for the agent.
        agent_type (str): Type of agent (e.g., "openai", "anthropic", "custom").
        protocol_version (str): A2A protocol version supported.
        capabilities (Dict[str, Any]): Agent capabilities and features.
        config (Dict[str, Any]): Agent-specific configuration parameters.
        auth_type (Optional[str]): Type of authentication ("api_key", "oauth", "bearer", etc.).
        auth_username (Optional[str]): Username for basic authentication.
        auth_password (Optional[str]): Password for basic authentication.
        auth_token (Optional[str]): Token for bearer authentication.
        auth_header_key (Optional[str]): Key for custom headers authentication.
        auth_header_value (Optional[str]): Value for custom headers authentication.
        auth_headers (Optional[List[Dict[str, str]]]): List of custom headers for authentication.
        auth_value (Optional[str]): Alias for authentication value, used for better access post-validation.
        tags (List[str]): Tags for categorizing the agent.
        team_id (Optional[str]): Team ID for resource organization.
        visibility (str): Visibility level ("private", "team", "public").
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    name: str = Field(..., description="Unique name for the agent")
    slug: Optional[str] = Field(None, description="Optional slug for the agent (auto-generated if not provided)")
    description: Optional[str] = Field(None, description="Agent description")
    endpoint_url: str = Field(..., description="URL endpoint for the agent")
    agent_type: str = Field(default="generic", description="Type of agent (e.g., 'openai', 'anthropic', 'custom')")
    protocol_version: str = Field(default="1.0", description="A2A protocol version supported")
    capabilities: Dict[str, Any] = Field(default_factory=dict, description="Agent capabilities and features")
    config: Dict[str, Any] = Field(default_factory=dict, description="Agent-specific configuration parameters")
    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through from client to target")
    # Authorizations
    auth_type: Optional[str] = Field(None, description="Type of authentication: basic, bearer, authheaders, oauth, query_param, or none")
    # Fields for various types of authentication
    auth_username: Optional[str] = Field(None, description="Username for basic authentication")
    auth_password: Optional[str] = Field(None, description="Password for basic authentication")
    auth_token: Optional[str] = Field(None, description="Token for bearer authentication")
    auth_header_key: Optional[str] = Field(None, description="Key for custom headers authentication")
    auth_header_value: Optional[str] = Field(None, description="Value for custom headers authentication")
    auth_headers: Optional[List[Dict[str, str]]] = Field(None, description="List of custom headers for authentication")

    # OAuth 2.0 configuration
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")

    # Query Parameter Authentication (CWE-598 security concern - use only when required by upstream)
    auth_query_param_key: Optional[str] = Field(
        None,
        description="Query parameter name for authentication (e.g., 'tavilyApiKey')",
    )
    auth_query_param_value: Optional[SecretStr] = Field(
        None,
        description="Query parameter value (API key) - will be encrypted at rest",
    )

    # Adding `auth_value` as an alias for better access post-validation
    auth_value: Optional[str] = Field(None, validate_default=True)
    tags: List[str] = Field(default_factory=list, description="Tags for categorizing the agent")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the agent owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Visibility level: private, team, or public")

    # UAID (Universal Agent ID) generation fields
    generate_uaid: bool = Field(default=False, description="Generate UAID (Universal Agent ID) instead of UUID for zero-config cross-gateway routing")
    uaid_registry: Optional[str] = Field(default="context-forge", description="Registry name for UAID generation (e.g., 'context-forge')")
    uaid_protocol: Optional[str] = Field(default="a2a", description="Protocol for UAID (a2a, mcp, rest, grpc)")
    uaid_skills: Optional[list[int]] = Field(default_factory=list, description="Skill IDs for UAID hash generation (deterministic identity)")
    version: Optional[str] = Field(default="1.0.0", description="Agent version for UAID generation")
    uaid_native_id_override: Optional[str] = Field(None, description="Override nativeId in UAID for cross-gateway routing (defaults to endpoint_url if not provided)")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> List[str]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings
        """
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate agent name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "A2A Agent name")

    @field_validator("endpoint_url")
    @classmethod
    def validate_endpoint_url(cls, v: str) -> str:
        """Validate agent endpoint URL

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return validate_core_url(v, "Agent endpoint URL")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import A2AAgentCreate
            >>> A2AAgentCreate.validate_description('A safe description')
            'A safe description'
            >>> A2AAgentCreate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = A2AAgentCreate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("capabilities", "config")
    @classmethod
    def validate_json_fields(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Validate JSON structure depth

        Args:
            v (dict): Value to validate

        Returns:
            dict: Value if validated as safe
        """
        SecurityValidator.validate_json_depth(v)
        return v

    @field_validator("team_id")
    @classmethod
    def validate_team_id(cls, v: Optional[str]) -> Optional[str]:
        """Validate team ID format.

        Args:
            v: Team ID to validate

        Returns:
            Validated team ID
        """
        if v is not None:
            return SecurityValidator.validate_uuid(v, "team_id")
        return v

    @field_validator("auth_value", mode="before")
    @classmethod
    def create_auth_value(cls, v, info):
        """
        This validator will run before the model is fully instantiated (mode="before")
        It will process the auth fields based on auth_type and generate auth_value.

        Args:
            v: Input url
            info: ValidationInfo containing auth_type

        Returns:
            str: Auth value
        """
        data = info.data
        auth_type = data.get("auth_type")

        if (auth_type is None) or (auth_type == ""):
            return v  # If no auth_type is provided, no need to create auth_value

        # Process the auth fields and generate auth_value based on auth_type
        auth_value = cls._process_auth_fields(info)
        return auth_value

    @staticmethod
    def _process_auth_fields(info: ValidationInfo) -> Optional[str]:
        """
        Processes the input authentication fields and returns the correct auth_value.
        This method is called based on the selected auth_type.

        Args:
            info: ValidationInfo containing auth fields

        Returns:
            Encoded auth string or None

        Raises:
            ValueError: If auth_type is invalid
        """
        data = info.data
        auth_type = data.get("auth_type")

        if auth_type == "basic":
            # For basic authentication, both username and password must be present
            username = data.get("auth_username")
            password = data.get("auth_password")

            if not username or not password:
                raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")

            creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()
            return encode_auth({"Authorization": f"Basic {creds}"})

        if auth_type == "bearer":
            # For bearer authentication, only token is required
            token = data.get("auth_token")

            if not token:
                raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")

            return encode_auth({"Authorization": f"Bearer {token}"})

        if auth_type == "oauth":
            # For OAuth authentication, we don't encode anything here
            # The OAuth configuration is handled separately in the oauth_config field
            # This method is only called for traditional auth types
            return None

        if auth_type == "authheaders":
            # Support both new multi-headers format and legacy single header format
            auth_headers = data.get("auth_headers")
            if auth_headers and isinstance(auth_headers, list):
                # New multi-headers format with enhanced validation
                header_dict = {}
                duplicate_keys = set()

                for header in auth_headers:
                    if not isinstance(header, dict):
                        continue

                    key = header.get("key")
                    value = header.get("value", "")

                    # Skip headers without keys
                    if not key:
                        continue

                    # Track duplicate keys (last value wins)
                    if key in header_dict:
                        duplicate_keys.add(key)

                    # Validate header key format (basic HTTP header validation)
                    if not all(c.isalnum() or c in "-_" for c in key.replace(" ", "")):
                        raise ValueError(f"Invalid header key format: '{key}'. Header keys should contain only alphanumeric characters, hyphens, and underscores.")

                    # Store header (empty values are allowed)
                    header_dict[key] = value

                # Ensure at least one valid header
                if not header_dict:
                    raise ValueError("For 'authheaders' auth, at least one valid header with a key must be provided.")

                # Warn about duplicate keys (optional - could log this instead)
                if duplicate_keys:
                    logger.warning(f"Duplicate header keys detected (last value used): {', '.join(duplicate_keys)}")

                # Check for excessive headers (prevent abuse)
                if len(header_dict) > 100:
                    raise ValueError("Maximum of 100 headers allowed per gateway.")

                return encode_auth(header_dict)

            # Legacy single header format (backward compatibility)
            header_key = data.get("auth_header_key")
            header_value = data.get("auth_header_value")

            if not header_key or not header_value:
                raise ValueError("For 'authheaders' auth, either 'auth_headers' list or both 'auth_header_key' and 'auth_header_value' must be provided.")

            return encode_auth({header_key: header_value})

        if auth_type == "one_time_auth":
            # One-time auth does not require encoding here
            return None

        if auth_type == "query_param":
            # Query param auth doesn't use auth_value field
            # Validation is handled by model_validator
            return None

        raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, oauth, authheaders, or query_param.")

    @model_validator(mode="after")
    def validate_query_param_auth(self) -> "A2AAgentCreate":
        """Validate query parameter authentication configuration.

        Returns:
            A2AAgentCreate: The validated instance.

        Raises:
            ValueError: If query param auth is disabled or host is not in allowlist.
        """
        if self.auth_type != "query_param":
            return self

        # Check feature flag
        if not settings.insecure_allow_queryparam_auth:
            raise ValueError("Query parameter authentication is disabled. " + "Set INSECURE_ALLOW_QUERYPARAM_AUTH=true to enable. " + "WARNING: API keys in URLs may appear in proxy logs.")

        # Check required fields
        if not self.auth_query_param_key:
            raise ValueError("auth_query_param_key is required when auth_type is 'query_param'")
        if not self.auth_query_param_value:
            raise ValueError("auth_query_param_value is required when auth_type is 'query_param'")

        # Check host allowlist (if configured)
        if settings.insecure_queryparam_auth_allowed_hosts:
            parsed = urlparse(str(self.endpoint_url))
            # Extract hostname properly (handles IPv6, ports, userinfo)
            hostname = parsed.hostname or parsed.netloc.split("@")[-1].split(":")[0]
            hostname_lower = hostname.lower()

            if hostname_lower not in settings.insecure_queryparam_auth_allowed_hosts:
                allowed = ", ".join(settings.insecure_queryparam_auth_allowed_hosts)
                raise ValueError(f"Host '{hostname}' is not in the allowed hosts for query parameter auth. Allowed hosts: {allowed}")

        return self


class A2AAgentUpdate(BaseModelWithConfigDict):
    """Schema for updating an existing A2A agent.

    Similar to A2AAgentCreate but all fields are optional to allow partial updates.
    """

    name: Optional[str] = Field(None, description="Unique name for the agent")
    description: Optional[str] = Field(None, description="Agent description")
    endpoint_url: Optional[str] = Field(None, description="URL endpoint for the agent")
    agent_type: Optional[str] = Field(None, description="Type of agent")
    protocol_version: Optional[str] = Field(None, description="A2A protocol version supported")
    capabilities: Optional[Dict[str, Any]] = Field(None, description="Agent capabilities and features")
    config: Optional[Dict[str, Any]] = Field(None, description="Agent-specific configuration parameters")
    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through from client to target")
    auth_type: Optional[str] = Field(None, description="Type of authentication")
    auth_username: Optional[str] = Field(None, description="username for basic authentication")
    auth_password: Optional[str] = Field(None, description="password for basic authentication")
    auth_token: Optional[str] = Field(None, description="token for bearer authentication")
    auth_header_key: Optional[str] = Field(None, description="key for custom headers authentication")
    auth_header_value: Optional[str] = Field(None, description="value for custom headers authentication")
    auth_headers: Optional[List[Dict[str, str]]] = Field(None, description="List of custom headers for authentication")

    # Adding `auth_value` as an alias for better access post-validation
    auth_value: Optional[str] = Field(None, validate_default=True)

    # OAuth 2.0 configuration
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")

    # Query Parameter Authentication (CWE-598 security concern - use only when required by upstream)
    auth_query_param_key: Optional[str] = Field(
        None,
        description="Query parameter name for authentication (e.g., 'tavilyApiKey')",
    )
    auth_query_param_value: Optional[SecretStr] = Field(
        None,
        description="Query parameter value (API key) - will be encrypted at rest",
    )

    tags: Optional[List[str]] = Field(None, description="Tags for categorizing the agent")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="Team ID for resource organization")
    owner_email: Optional[str] = Field(None, description="Email of the agent owner")
    visibility: Optional[Literal["private", "team", "public"]] = Field(None, description="Visibility level: private, team, or public")

    # UAID (Universal Agent ID) generation fields - allow adding UAID to agents that don't have one
    generate_uaid: Optional[bool] = Field(default=False, description="Generate UAID if agent doesn't already have one (UAID is immutable once set)")
    uaid_registry: Optional[str] = Field(default=None, description="Registry name for UAID generation (e.g., 'context-forge')")
    uaid_protocol: Optional[str] = Field(default=None, description="Protocol for UAID (a2a, mcp, rest, grpc)")
    version: Optional[str] = Field(default=None, description="Agent version for UAID generation")
    uaid_native_id_override: Optional[str] = Field(None, description="Override nativeId in UAID for cross-gateway routing (defaults to endpoint_url if not provided)")

    @field_validator("tags")
    @classmethod
    def validate_tags(cls, v: Optional[List[str]]) -> Optional[List[str]]:
        """Validate and normalize tags.

        Args:
            v: Optional list of tag strings to validate

        Returns:
            List of validated tag strings or None if input is None
        """
        if v is None:
            return None
        return validate_tags_field(v)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate agent name

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "A2A Agent name")

    @field_validator("endpoint_url")
    @classmethod
    def validate_endpoint_url(cls, v: str) -> str:
        """Validate agent endpoint URL

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return validate_core_url(v, "Agent endpoint URL")

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Ensure descriptions display safely, truncate if too long

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe and truncated if too long

        Raises:
            ValueError: When value is unsafe

        Examples:
            >>> from mcpgateway.schemas import A2AAgentUpdate
            >>> A2AAgentUpdate.validate_description('A safe description')
            'A safe description'
            >>> A2AAgentUpdate.validate_description(None) # Test None case
            >>> long_desc = 'x' * SecurityValidator.MAX_DESCRIPTION_LENGTH
            >>> truncated = A2AAgentUpdate.validate_description(long_desc)
            >>> len(truncated) - SecurityValidator.MAX_DESCRIPTION_LENGTH
            0
            >>> truncated == long_desc[:SecurityValidator.MAX_DESCRIPTION_LENGTH]
            True
        """
        if v is None:
            return v
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            # Truncate the description to the maximum allowed length
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")

    @field_validator("capabilities", "config")
    @classmethod
    def validate_json_fields(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        """Validate JSON structure depth

        Args:
            v (dict): Value to validate

        Returns:
            dict: Value if validated as safe
        """
        if v is None:
            return v
        SecurityValidator.validate_json_depth(v)
        return v

    @field_validator("team_id")
    @classmethod
    def validate_team_id(cls, v: Optional[str]) -> Optional[str]:
        """Validate team ID format.

        Args:
            v: Team ID to validate

        Returns:
            Validated team ID
        """
        if v is not None:
            return SecurityValidator.validate_uuid(v, "team_id")
        return v

    @field_validator("auth_value", mode="before")
    @classmethod
    def create_auth_value(cls, v, info):
        """
        This validator will run before the model is fully instantiated (mode="before")
        It will process the auth fields based on auth_type and generate auth_value.

        Args:
            v: Input URL
            info: ValidationInfo containing auth_type

        Returns:
            str: Auth value or URL
        """
        data = info.data
        auth_type = data.get("auth_type")

        if (auth_type is None) or (auth_type == ""):
            return v  # If no auth_type is provided, no need to create auth_value

        # Process the auth fields and generate auth_value based on auth_type
        auth_value = cls._process_auth_fields(info)
        return auth_value

    @staticmethod
    def _process_auth_fields(info: ValidationInfo) -> Optional[str]:
        """
        Processes the input authentication fields and returns the correct auth_value.
        This method is called based on the selected auth_type.

        Args:
            info: ValidationInfo containing auth fields

        Returns:
            Encoded auth string or None

        Raises:
            ValueError: If auth type is invalid
        """

        data = info.data
        auth_type = data.get("auth_type")

        if auth_type == "basic":
            # For basic authentication, both username and password must be present
            username = data.get("auth_username")
            password = data.get("auth_password")
            if not username or not password:
                raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")

            creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()
            return encode_auth({"Authorization": f"Basic {creds}"})

        if auth_type == "bearer":
            # For bearer authentication, only token is required
            token = data.get("auth_token")

            if not token:
                raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")

            return encode_auth({"Authorization": f"Bearer {token}"})

        if auth_type == "oauth":
            # For OAuth authentication, we don't encode anything here
            # The OAuth configuration is handled separately in the oauth_config field
            # This method is only called for traditional auth types
            return None

        if auth_type == "authheaders":
            # Support both new multi-headers format and legacy single header format
            auth_headers = data.get("auth_headers")
            if auth_headers and isinstance(auth_headers, list):
                # New multi-headers format with enhanced validation
                header_dict = {}
                duplicate_keys = set()

                for header in auth_headers:
                    if not isinstance(header, dict):
                        continue

                    key = header.get("key")
                    value = header.get("value", "")

                    # Skip headers without keys
                    if not key:
                        continue

                    # Track duplicate keys (last value wins)
                    if key in header_dict:
                        duplicate_keys.add(key)

                    # Validate header key format (basic HTTP header validation)
                    if not all(c.isalnum() or c in "-_" for c in key.replace(" ", "")):
                        raise ValueError(f"Invalid header key format: '{key}'. Header keys should contain only alphanumeric characters, hyphens, and underscores.")

                    # Store header (empty values are allowed)
                    header_dict[key] = value

                # Ensure at least one valid header
                if not header_dict:
                    raise ValueError("For 'authheaders' auth, at least one valid header with a key must be provided.")

                # Warn about duplicate keys (optional - could log this instead)
                if duplicate_keys:
                    logger.warning(f"Duplicate header keys detected (last value used): {', '.join(duplicate_keys)}")

                # Check for excessive headers (prevent abuse)
                if len(header_dict) > 100:
                    raise ValueError("Maximum of 100 headers allowed per gateway.")

                return encode_auth(header_dict)

            # Legacy single header format (backward compatibility)
            header_key = data.get("auth_header_key")
            header_value = data.get("auth_header_value")

            if not header_key or not header_value:
                raise ValueError("For 'authheaders' auth, either 'auth_headers' list or both 'auth_header_key' and 'auth_header_value' must be provided.")

            return encode_auth({header_key: header_value})

        if auth_type == "one_time_auth":
            # One-time auth does not require encoding here
            return None

        if auth_type == "query_param":
            # Query param auth doesn't use auth_value field
            # Validation is handled by model_validator
            return None

        raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, oauth, authheaders, or query_param.")

    @model_validator(mode="after")
    def validate_query_param_auth(self) -> "A2AAgentUpdate":
        """Validate query parameter authentication configuration.

        NOTE: This only runs when auth_type is explicitly set to "query_param".
        Service-layer enforcement handles the case where auth_type is omitted
        but the existing agent uses query_param auth.

        Returns:
            A2AAgentUpdate: The validated instance.

        Raises:
            ValueError: If required fields are missing when setting query_param auth.
        """
        if self.auth_type == "query_param":
            # Validate fields are provided when explicitly setting query_param auth
            # Feature flag/allowlist check happens in service layer (has access to existing agent)
            if not self.auth_query_param_key:
                raise ValueError("auth_query_param_key is required when setting auth_type to 'query_param'")
            if not self.auth_query_param_value:
                raise ValueError("auth_query_param_value is required when setting auth_type to 'query_param'")

        return self


class A2AAgentRead(BaseModelWithConfigDict):
    """Schema for reading A2A agent information.

    Includes all agent fields plus:
    - Database ID
    - Slug
    - Creation/update timestamps
    - Enabled/reachable status
    - Metrics
    - Authentication type: basic, bearer, authheaders, oauth, query_param
    - Authentication value: username/password or token or custom headers
    - OAuth configuration for OAuth 2.0 authentication
    - Query parameter authentication (key name and masked value)

    Auto Populated fields:
    - Authentication username: for basic auth
    - Authentication password: for basic auth
    - Authentication token: for bearer auth
    - Authentication header key: for authheaders auth
    - Authentication header value: for authheaders auth
    - Query param key: for query_param auth
    - Query param value (masked): for query_param auth
    """

    id: Optional[str] = Field(None, description="Unique ID of the a2a agent")
    name: str = Field(..., description="Unique name for the a2a agent")
    slug: Optional[str] = Field(None, description="Slug for a2a agent endpoint URL")
    description: Optional[str] = Field(None, description="a2a agent description")
    endpoint_url: str = Field(..., description="a2a agent endpoint URL")
    agent_type: str
    protocol_version: str
    capabilities: Dict[str, Any]
    config: Dict[str, Any]
    enabled: bool
    reachable: bool
    created_at: datetime
    updated_at: datetime
    last_interaction: Optional[datetime]
    tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the agent")
    metrics: Optional[A2AAgentMetrics] = Field(None, description="Agent metrics (may be None in list operations)")
    passthrough_headers: Optional[List[str]] = Field(default=None, description="List of headers allowed to be passed through from client to target")
    # Authorizations
    auth_type: Optional[str] = Field(None, description="auth_type: basic, bearer, authheaders, oauth, query_param, or None")
    auth_value: Optional[str] = Field(None, description="auth value: username/password or token or custom headers")

    # OAuth 2.0 configuration
    oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")

    # auth_value will populate the following fields
    auth_username: Optional[str] = Field(None, description="username for basic authentication")
    auth_password: Optional[str] = Field(None, description="password for basic authentication")
    auth_token: Optional[str] = Field(None, description="token for bearer authentication")
    auth_header_key: Optional[str] = Field(None, description="key for custom headers authentication")
    auth_header_value: Optional[str] = Field(None, description="vallue for custom headers authentication")
    auth_headers: Optional[List[Dict[str, str]]] = Field(None, description="List of custom headers for authentication")

    # Query Parameter Authentication (masked for security)
    auth_query_param_key: Optional[str] = Field(
        None,
        description="Query parameter name for authentication",
    )
    auth_query_param_value_masked: Optional[str] = Field(
        None,
        description="Masked query parameter value (actual value is encrypted at rest)",
    )

    # Comprehensive metadata for audit tracking
    created_by: Optional[str] = Field(None, description="Username who created this entity")
    created_from_ip: Optional[str] = Field(None, description="IP address of creator")
    created_via: Optional[str] = Field(None, description="Creation method: ui|api|import|federation")
    created_user_agent: Optional[str] = Field(None, description="User agent of creation request")

    modified_by: Optional[str] = Field(None, description="Username who last modified this entity")
    modified_from_ip: Optional[str] = Field(None, description="IP address of last modifier")
    modified_via: Optional[str] = Field(None, description="Modification method")
    modified_user_agent: Optional[str] = Field(None, description="User agent of modification request")

    import_batch_id: Optional[str] = Field(None, description="UUID of bulk import batch")
    federation_source: Optional[str] = Field(None, description="Source gateway for federated entities")
    version: Optional[int] = Field(1, description="Entity version for change tracking")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="ID of the team that owns this resource")
    team: Optional[str] = Field(None, description="Name of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Email of the user who owns this resource")
    visibility: Optional[Literal["private", "team", "public"]] = Field(default="public", description="Visibility level: private, team, or public")

    # UAID (Universal Agent ID) fields
    uaid: Optional[str] = Field(None, description="Full UAID string (if UAID-based agent)")
    uaid_registry: Optional[str] = Field(None, description="Registry name from UAID")
    uaid_proto: Optional[str] = Field(None, description="Protocol from UAID (a2a, mcp, rest, grpc)")
    uaid_native_id: Optional[str] = Field(None, description="Native endpoint from UAID for cross-gateway routing")

    _normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))

    @model_validator(mode="before")
    @classmethod
    def _mask_query_param_auth(cls, data: Any) -> Any:
        """Mask query param auth value when constructing from DB model.

        This extracts auth_query_params from the raw data (DB model or dict)
        and populates the masked fields for display.

        Args:
            data: The raw data (dict or ORM model) to process.

        Returns:
            Any: The processed data with masked query param values.
        """
        # Handle dict input
        if isinstance(data, dict):
            auth_query_params = data.get("auth_query_params")
            if auth_query_params and isinstance(auth_query_params, dict):
                # Extract the param key name and set masked value
                first_key = next(iter(auth_query_params.keys()), None)
                if first_key:
                    data["auth_query_param_key"] = first_key
                    data["auth_query_param_value_masked"] = settings.masked_auth_value
        # Handle ORM model input (has auth_query_params attribute)
        elif hasattr(data, "auth_query_params"):
            auth_query_params = getattr(data, "auth_query_params", None)
            if auth_query_params and isinstance(auth_query_params, dict):
                # Convert ORM to dict for modification, preserving all attributes
                # Start with table columns
                data_dict = {c.name: getattr(data, c.name) for c in data.__table__.columns}
                # Preserve dynamically added attributes like 'team' (from relationships)
                for attr in ["team"]:
                    if hasattr(data, attr):
                        data_dict[attr] = getattr(data, attr)
                first_key = next(iter(auth_query_params.keys()), None)
                if first_key:
                    data_dict["auth_query_param_key"] = first_key
                    data_dict["auth_query_param_value_masked"] = settings.masked_auth_value
                return data_dict
        return data

    # This will be the main method to automatically populate fields
    @model_validator(mode="after")
    def _populate_auth(self) -> Self:
        """Populate authentication fields based on auth_type and encoded auth_value.

        This post-validation method decodes the stored authentication value and
        populates the appropriate authentication fields (username/password, token,
        or custom headers) based on the authentication type. It ensures the
        authentication data is properly formatted and accessible through individual
        fields for display purposes.

        The method handles three authentication types:
        - basic: Extracts username and password from Authorization header
        - bearer: Extracts token from Bearer Authorization header
        - authheaders: Extracts custom header key/value pair

        Returns:
            Self: The instance with populated authentication fields:
                - For basic: auth_username and auth_password
                - For bearer: auth_token
                - For authheaders: auth_header_key and auth_header_value

        Raises:
            ValueError: If the authentication data is malformed:
                    - Basic auth missing username or password
                    - Bearer auth missing or improperly formatted Authorization header
                    - Custom headers not exactly one key/value pair

        Examples:
            >>> # Basic auth example
            >>> string_bytes = "admin:secret".encode("utf-8")
            >>> encoded_auth = base64.urlsafe_b64encode(string_bytes).decode("utf-8")
            >>> values = GatewayRead.model_construct(
            ...     auth_type="basic",
            ...     auth_value=encode_auth({"Authorization": f"Basic {encoded_auth}"})
            ... )
            >>> values = A2AAgentRead._populate_auth(values)
            >>> values.auth_username
            'admin'
            >>> values.auth_password
            'secret'

            >>> # Bearer auth example
            >>> values = A2AAgentRead.model_construct(
            ...     auth_type="bearer",
            ...     auth_value=encode_auth({"Authorization": "Bearer mytoken123"})
            ... )
            >>> values = A2AAgentRead._populate_auth(values)
            >>> values.auth_token
            'mytoken123'

            >>> # Custom headers example
            >>> values = A2AAgentRead.model_construct(
            ...     auth_type='authheaders',
            ...     auth_value=encode_auth({"X-API-Key": "abc123"})
            ... )
            >>> values = A2AAgentRead._populate_auth(values)
            >>> values.auth_header_key
            'X-API-Key'
            >>> values.auth_header_value
            'abc123'
        """
        auth_type = self.auth_type
        auth_value_encoded = self.auth_value
        # Skip validation logic if masked value
        if auth_value_encoded == settings.masked_auth_value:
            return self

        # Handle OAuth authentication (no auth_value to decode)
        if auth_type == "oauth":
            # OAuth gateways don't have traditional auth_value to decode
            # They use oauth_config instead
            return self

        if auth_type == "one_time_auth":
            return self

        if auth_type == "query_param":
            # Query param auth is handled by the before validator
            # (auth_query_params from DB model is processed there)
            return self

        # If no encoded value is present, nothing to populate
        if not auth_value_encoded:
            return self

        auth_value = decode_auth(auth_value_encoded)
        if auth_type == "basic":
            auth = auth_value.get("Authorization")
            if not (isinstance(auth, str) and auth.startswith("Basic ")):
                raise ValueError("basic auth requires an Authorization header of the form 'Basic <base64>'")
            auth = auth.removeprefix("Basic ")
            u, p = base64.urlsafe_b64decode(auth).decode("utf-8").split(":")
            if not u or not p:
                raise ValueError("basic auth requires both username and password")
            self.auth_username, self.auth_password = u, p

        elif auth_type == "bearer":
            auth = auth_value.get("Authorization")
            if not (isinstance(auth, str) and auth.startswith("Bearer ")):
                raise ValueError("bearer auth requires an Authorization header of the form 'Bearer <token>'")
            self.auth_token = auth.removeprefix("Bearer ")

        elif auth_type == "authheaders":
            # For backward compatibility, populate first header in key/value fields
            if not isinstance(auth_value, dict) or len(auth_value) == 0:
                raise ValueError("authheaders requires at least one key/value pair")
            # Populate auth_headers list for multi-header support
            self.auth_headers = [{"key": str(key), "value": "" if value is None else str(value)} for key, value in auth_value.items()]
            # Maintain backward compatibility with single header fields
            k, v = next(iter(auth_value.items()))
            self.auth_header_key, self.auth_header_value = k, v
        return self

    def masked(self) -> "A2AAgentRead":
        """
        Return a masked version of the model instance with sensitive authentication fields hidden.

        This method creates a dictionary representation of the model data and replaces sensitive fields
        such as `auth_value`, `auth_password`, `auth_token`, and `auth_header_value` with a masked
        placeholder value defined in `settings.masked_auth_value`. Masking is only applied if the fields
        are present and not already masked.

        Args:
            None

        Returns:
            A2AAgentRead: A new instance of the A2AAgentRead model with sensitive authentication-related fields
            masked to prevent exposure of sensitive information.

        Notes:
            - The `auth_value` field is only masked if it exists and its value is different from the masking
            placeholder.
            - Other sensitive fields (`auth_password`, `auth_token`, `auth_header_value`) are masked if present.
            - Fields not related to authentication remain unmodified.
        """
        masked_data = self.model_dump()

        # Only mask if auth_value is present and not already masked
        if masked_data.get("auth_value") and masked_data["auth_value"] != settings.masked_auth_value:
            masked_data["auth_value"] = settings.masked_auth_value

        masked_data["auth_password"] = settings.masked_auth_value if masked_data.get("auth_password") else None
        masked_data["auth_token"] = settings.masked_auth_value if masked_data.get("auth_token") else None
        masked_data["auth_header_value"] = settings.masked_auth_value if masked_data.get("auth_header_value") else None
        if masked_data.get("auth_headers"):
            masked_data["auth_headers"] = [
                {
                    "key": header.get("key"),
                    "value": settings.masked_auth_value if header.get("value") else header.get("value"),
                }
                for header in masked_data["auth_headers"]
            ]

        # Mask sensitive keys inside oauth_config (e.g. password, client_secret)
        if masked_data.get("oauth_config"):
            masked_data["oauth_config"] = _mask_oauth_config(masked_data["oauth_config"])

        return A2AAgentRead.model_validate(masked_data)


class A2AAgentInvocation(BaseModelWithConfigDict):
    """Schema for A2A agent invocation requests.

    Contains:
    - Agent name or ID to invoke
    - Parameters for the agent interaction
    - Interaction type (query, execute, etc.)
    """

    agent_name: str = Field(..., description="Name of the A2A agent to invoke")
    parameters: Dict[str, Any] = Field(default_factory=dict, description="Parameters for agent interaction")
    interaction_type: str = Field(default="query", description="Type of interaction (query, execute, etc.)")

    @field_validator("agent_name")
    @classmethod
    def validate_agent_name(cls, v: str) -> str:
        """Ensure agent names follow naming conventions

        Args:
            v (str): Value to validate

        Returns:
            str: Value if validated as safe
        """
        return SecurityValidator.validate_name(v, "Agent name")

    @field_validator("parameters")
    @classmethod
    def validate_parameters(cls, v: Dict[str, Any]) -> Dict[str, Any]:
        """Validate parameters structure depth to prevent DoS attacks.

        Args:
            v (dict): Parameters dictionary to validate

        Returns:
            dict: The validated parameters if within depth limits

        Raises:
            ValueError: If the parameters exceed the maximum allowed depth
        """
        SecurityValidator.validate_json_depth(v)
        return v


# ---------------------------------------------------------------------------
# A2A Task Schemas
# ---------------------------------------------------------------------------


class A2ATaskState(str, Enum):
    """Finite state machine for A2A task lifecycle per the A2A v1 spec.

    Terminal states (``completed``, ``canceled``, ``failed``, ``rejected``)
    are the only states for which ``completed_at`` should be set; the
    model-validators on :class:`A2ATaskCreate` and :class:`A2ATaskUpdate`
    enforce this invariant.
    """

    SUBMITTED = "submitted"
    WORKING = "working"
    INPUT_REQUIRED = "input-required"
    AUTH_REQUIRED = "auth-required"
    COMPLETED = "completed"
    CANCELED = "canceled"
    FAILED = "failed"
    REJECTED = "rejected"

    @classmethod
    def terminal(cls) -> "frozenset[A2ATaskState]":
        """Return the set of terminal states."""
        return frozenset({cls.COMPLETED, cls.CANCELED, cls.FAILED, cls.REJECTED})

    def is_terminal(self) -> bool:
        """Whether this state is a terminal state."""
        return self in self.terminal()


class A2ATaskCreate(BaseModel):
    """Schema for recording a new A2A task state."""

    a2a_agent_id: str
    task_id: str
    context_id: Optional[str] = None
    state: A2ATaskState = A2ATaskState.SUBMITTED
    payload: Optional[Dict[str, Any]] = None
    latest_message: Optional[Dict[str, Any]] = None
    last_error: Optional[str] = None


class A2ATaskRead(BaseModel):
    """Schema for reading A2A task state."""

    model_config = ConfigDict(from_attributes=True)

    id: str
    a2a_agent_id: str
    task_id: str
    context_id: Optional[str] = None
    state: str
    payload: Optional[Dict[str, Any]] = None
    latest_message: Optional[Dict[str, Any]] = None
    last_error: Optional[str] = None
    created_at: datetime
    updated_at: datetime
    completed_at: Optional[datetime] = None


class A2ATaskUpdate(BaseModel):
    """Schema for updating A2A task state.

    The validator rejects ``completed_at`` paired with a non-terminal
    ``state``.  The reverse (terminal ``state`` without ``completed_at``)
    is **not** enforced here — the service layer (e.g. ``cancel_task``)
    stamps ``completed_at`` itself when transitioning into a terminal
    state, so callers may legitimately omit the timestamp.
    """

    state: Optional[A2ATaskState] = None
    payload: Optional[Dict[str, Any]] = None
    latest_message: Optional[Dict[str, Any]] = None
    last_error: Optional[str] = None
    completed_at: Optional[datetime] = None

    @model_validator(mode="after")
    def _enforce_completed_at_iff_terminal(self) -> "A2ATaskUpdate":
        """Reject ``completed_at`` paired with a non-terminal state."""
        if self.completed_at is not None and self.state is not None and not self.state.is_terminal():
            raise ValueError(f"completed_at is only valid with a terminal state (got state={self.state.value!r})")
        return self


# ---------------------------------------------------------------------------
# Server Interface Schemas
# ---------------------------------------------------------------------------


class ServerInterfaceCreate(BaseModel):
    """Schema for creating a server interface."""

    server_id: str
    protocol: str
    binding: str
    version: Optional[str] = None
    tenant: Optional[str] = None
    enabled: bool = True
    config: Optional[Dict[str, Any]] = None


class ServerInterfaceRead(BaseModel):
    """Schema for reading a server interface."""

    model_config = ConfigDict(from_attributes=True)

    id: str
    server_id: str
    protocol: str
    binding: str
    version: Optional[str] = None
    tenant: Optional[str] = None
    enabled: bool
    config: Optional[Dict[str, Any]] = None
    created_at: datetime
    updated_at: datetime


# ---------------------------------------------------------------------------
# A2A Push Notification Config Schemas
# ---------------------------------------------------------------------------


class A2APushNotificationConfigCreate(BaseModel):
    """Schema for creating a push notification webhook configuration."""

    a2a_agent_id: str
    task_id: str
    webhook_url: str
    auth_token: Optional[str] = None
    events: Optional[List[str]] = None
    enabled: bool = True

    @field_validator("webhook_url")
    @classmethod
    def validate_webhook_url(cls, v: str) -> str:
        """Validate webhook URL for scheme, SSRF, and dangerous patterns."""
        return validate_core_url(v, "Webhook URL")


class A2APushNotificationConfigRead(BaseModel):
    """Schema for reading a push notification webhook configuration."""

    model_config = ConfigDict(from_attributes=True)

    id: str
    a2a_agent_id: str
    task_id: str
    webhook_url: str
    auth_token: Optional[str] = Field(default=None, exclude=True)
    events: Optional[List[str]] = None
    enabled: bool
    created_at: datetime
    updated_at: datetime


class A2ATaskEventCreate(BaseModel):
    """Schema for creating a task event log entry."""

    a2a_agent_id: Optional[str] = None
    task_id: str
    event_id: str
    sequence: int
    event_type: str
    payload: Optional[Dict[str, Any]] = None


class A2ATaskEventRead(BaseModel):
    """Schema for reading a task event log entry."""

    model_config = ConfigDict(from_attributes=True)

    id: str
    a2a_agent_id: Optional[str] = None
    task_id: str
    event_id: str
    sequence: int
    event_type: str
    payload: Optional[Dict[str, Any]] = None
    created_at: datetime


# ---------------------------------------------------------------------------
# Email-Based Authentication Schemas
# ---------------------------------------------------------------------------


class EmailLoginRequest(BaseModel):
    """Request schema for email login.

    Attributes:
        email: User's email address
        password: User's password

    Examples:
        >>> request = EmailLoginRequest(email="user@example.com", password="secret123")
        >>> request.email
        'user@example.com'
        >>> request.password
        'secret123'
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    email: EmailStr = Field(..., description="User's email address")
    password: str = Field(..., min_length=1, description="User's password")


class PublicRegistrationRequest(BaseModel):
    """Public self-registration request — minimal fields, password required.

    Extra fields are rejected (extra="forbid") so clients cannot submit
    admin-only fields like is_admin or is_active.

    Attributes:
        email: User's email address
        password: User's password (required, min 8 chars)
        full_name: Optional full name for display

    Examples:
        >>> request = PublicRegistrationRequest(
        ...     email="new@example.com",
        ...     password="secure123",
        ...     full_name="New User"
        ... )
        >>> request.email
        'new@example.com'
        >>> request.full_name
        'New User'
    """

    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")

    email: EmailStr = Field(..., description="User's email address")
    password: str = Field(..., min_length=8, description="User's password")
    full_name: Optional[str] = Field(None, max_length=255, description="User's full name")


class AdminCreateUserRequest(BaseModel):
    """Admin user creation request — all fields, password required.

    Attributes:
        email: User's email address
        password: User's password (required, min 8 chars)
        full_name: Optional full name for display
        is_admin: Whether user should have admin privileges (default: False)
        is_active: Whether user account is active (default: True)
        password_change_required: Whether user must change password on next login (default: False)

    Examples:
        >>> request = AdminCreateUserRequest(
        ...     email="new@example.com",
        ...     password="secure123",
        ...     full_name="New User"
        ... )
        >>> request.email
        'new@example.com'
        >>> request.full_name
        'New User'
        >>> request.is_admin
        False
        >>> request.is_active
        True
        >>> request.password_change_required
        False
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    email: EmailStr = Field(..., description="User's email address")
    password: str = Field(..., min_length=8, description="User's password")
    full_name: Optional[str] = Field(None, max_length=255, description="User's full name")
    is_admin: bool = Field(False, description="Grant admin privileges to user")
    is_active: bool = Field(True, description="Whether user account is active")
    password_change_required: bool = Field(False, description="Whether user must change password on next login")


# Deprecated alias — use AdminCreateUserRequest or PublicRegistrationRequest instead
EmailRegistrationRequest = AdminCreateUserRequest


class ChangePasswordRequest(BaseModel):
    """Request schema for password change.

    Attributes:
        old_password: Current password for verification
        new_password: New password to set

    Examples:
        >>> request = ChangePasswordRequest(
        ...     old_password="old_secret",
        ...     new_password="new_secure_password"
        ... )
        >>> request.old_password
        'old_secret'
        >>> request.new_password
        'new_secure_password'
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    old_password: str = Field(..., min_length=1, description="Current password")
    new_password: str = Field(..., min_length=8, description="New password")

    @field_validator("new_password")
    @classmethod
    def validate_new_password(cls, v: str) -> str:
        """Validate new password meets minimum requirements.

        Args:
            v: New password string to validate

        Returns:
            str: Validated new password

        Raises:
            ValueError: If new password doesn't meet requirements
        """
        if len(v) < 8:
            raise ValueError("New password must be at least 8 characters long")
        return v


class ForgotPasswordRequest(BaseModel):
    """Request schema for forgot-password flow."""

    model_config = ConfigDict(str_strip_whitespace=True)

    email: EmailStr = Field(..., description="Email address for password reset")


class ResetPasswordRequest(BaseModel):
    """Request schema for completing password reset."""

    model_config = ConfigDict(str_strip_whitespace=True)

    new_password: str = Field(..., min_length=8, description="New password to set")
    confirm_password: str = Field(..., min_length=8, description="Password confirmation")

    @model_validator(mode="after")
    def validate_password_match(self):
        """Ensure password and confirmation are identical.

        Returns:
            ResetPasswordRequest: Validated request instance.

        Raises:
            ValueError: If the password and confirmation do not match.
        """
        if self.new_password != self.confirm_password:
            raise ValueError("Passwords do not match")
        return self


class PasswordResetTokenValidationResponse(BaseModel):
    """Response schema for reset-token validation."""

    valid: bool = Field(..., description="Whether token is currently valid")
    message: str = Field(..., description="Validation status message")
    expires_at: Optional[datetime] = Field(None, description="Token expiration timestamp when valid")


class EmailUserResponse(BaseModel):
    """Response schema for user information.

    Attributes:
        email: User's email address
        full_name: User's full name
        is_admin: Whether user has admin privileges
        is_active: Whether account is active
        auth_provider: Authentication provider used
        created_at: Account creation timestamp
        last_login: Last successful login timestamp
        email_verified: Whether email is verified
        password_change_required: Whether user must change password on next login

    Examples:
        >>> user = EmailUserResponse(
        ...     email="user@example.com",
        ...     full_name="Test User",
        ...     is_admin=False,
        ...     is_active=True,
        ...     auth_provider="local",
        ...     created_at=datetime.now(),
        ...     last_login=None,
        ...     email_verified=False
        ... )
        >>> user.email
        'user@example.com'
        >>> user.is_admin
        False
    """

    model_config = ConfigDict(from_attributes=True)

    email: str = Field(..., description="User's email address")
    full_name: Optional[str] = Field(None, description="User's full name")
    is_admin: bool = Field(..., description="Whether user has admin privileges")
    is_active: bool = Field(..., description="Whether account is active")
    auth_provider: str = Field(..., description="Authentication provider")
    created_at: datetime = Field(..., description="Account creation timestamp")
    last_login: Optional[datetime] = Field(None, description="Last successful login")
    email_verified: bool = Field(False, description="Whether email is verified")
    password_change_required: bool = Field(False, description="Whether user must change password on next login")
    failed_login_attempts: int = Field(0, description="Current failed login attempts counter")
    locked_until: Optional[datetime] = Field(None, description="Account lock expiration timestamp")
    is_locked: bool = Field(False, description="Whether the account is currently locked")

    @classmethod
    def from_email_user(cls, user) -> "EmailUserResponse":
        """Create response from EmailUser model.

        Args:
            user: EmailUser model instance

        Returns:
            EmailUserResponse: Response schema instance
        """
        is_locked = user.is_account_locked()
        locked_until_raw = getattr(user, "locked_until", None)
        locked_until = locked_until_raw if isinstance(locked_until_raw, datetime) else None
        failed_attempts_raw = getattr(user, "failed_login_attempts", 0)
        try:
            failed_attempts = int(failed_attempts_raw or 0)
        except (TypeError, ValueError):
            failed_attempts = 0
        return cls(
            email=user.email,
            full_name=user.full_name,
            is_admin=user.is_admin,
            is_active=user.is_active,
            auth_provider=user.auth_provider,
            created_at=user.created_at,
            last_login=user.last_login,
            email_verified=user.is_email_verified(),
            password_change_required=user.password_change_required,
            failed_login_attempts=failed_attempts,
            locked_until=locked_until,
            is_locked=is_locked,
        )


class AuthenticationResponse(BaseModel):
    """Response schema for successful authentication.

    Attributes:
        access_token: JWT token for API access
        token_type: Type of token (always 'bearer')
        expires_in: Token expiration time in seconds
        user: User information

    Examples:
        >>> from datetime import datetime
        >>> response = AuthenticationResponse(
        ...     access_token="jwt.token.here",
        ...     token_type="bearer",
        ...     expires_in=3600,
        ...     user=EmailUserResponse(
        ...         email="user@example.com",
        ...         full_name="Test User",
        ...         is_admin=False,
        ...         is_active=True,
        ...         auth_provider="local",
        ...         created_at=datetime.now(),
        ...         last_login=None,
        ...         email_verified=False
        ...     )
        ... )
        >>> response.token_type
        'bearer'
        >>> response.user.email
        'user@example.com'
    """

    access_token: str = Field(..., description="JWT access token")
    token_type: str = Field(default="bearer", description="Token type")
    expires_in: int = Field(..., description="Token expiration in seconds")
    user: EmailUserResponse = Field(..., description="User information")


class AuthEventResponse(BaseModel):
    """Response schema for authentication events.

    Attributes:
        id: Event ID
        timestamp: Event timestamp
        user_email: User's email address
        event_type: Type of authentication event
        success: Whether the event was successful
        ip_address: Client IP address
        failure_reason: Reason for failure (if applicable)

    Examples:
        >>> from datetime import datetime
        >>> event = AuthEventResponse(
        ...     id=1,
        ...     timestamp=datetime.now(),
        ...     user_email="user@example.com",
        ...     event_type="login",
        ...     success=True,
        ...     ip_address="192.168.1.1",
        ...     failure_reason=None
        ... )
        >>> event.event_type
        'login'
        >>> event.success
        True
    """

    model_config = ConfigDict(from_attributes=True)

    id: int = Field(..., description="Event ID")
    timestamp: datetime = Field(..., description="Event timestamp")
    user_email: Optional[str] = Field(None, description="User's email address")
    event_type: str = Field(..., description="Type of authentication event")
    success: bool = Field(..., description="Whether the event was successful")
    ip_address: Optional[str] = Field(None, description="Client IP address")
    failure_reason: Optional[str] = Field(None, description="Reason for failure")


class UserListResponse(BaseModel):
    """Response schema for user list.

    Attributes:
        users: List of users
        total_count: Total number of users
        limit: Request limit
        offset: Request offset

    Examples:
        >>> user_list = UserListResponse(
        ...     users=[],
        ...     total_count=0,
        ...     limit=10,
        ...     offset=0
        ... )
        >>> user_list.total_count
        0
        >>> len(user_list.users)
        0
    """

    users: list[EmailUserResponse] = Field(..., description="List of users")
    total_count: int = Field(..., description="Total number of users")
    limit: int = Field(..., description="Request limit")
    offset: int = Field(..., description="Request offset")


class AdminUserUpdateRequest(BaseModel):
    """Request schema for admin user updates.

    Attributes:
        full_name: User's full name
        is_admin: Whether user has admin privileges
        is_active: Whether account is active
        password_change_required: Whether user must change password on next login
        password: New password (admin can reset without old password)

    Examples:
        >>> request = AdminUserUpdateRequest(
        ...     full_name="Updated Name",
        ...     is_admin=True,
        ...     is_active=True
        ... )
        >>> request.full_name
        'Updated Name'
        >>> request.is_admin
        True
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    full_name: Optional[str] = Field(None, max_length=255, description="User's full name")
    is_admin: Optional[bool] = Field(None, description="Whether user has admin privileges")
    is_active: Optional[bool] = Field(None, description="Whether account is active")
    email_verified: Optional[bool] = Field(None, description="Whether user's email is verified")
    password_change_required: Optional[bool] = Field(None, description="Whether user must change password on next login")
    password: Optional[str] = Field(None, min_length=8, description="New password (admin reset)")


class ErrorResponse(BaseModel):
    """Standard error response schema.

    Attributes:
        error: Error type
        message: Human-readable error message
        details: Additional error details

    Examples:
        >>> error = ErrorResponse(
        ...     error="authentication_failed",
        ...     message="Invalid email or password",
        ...     details=None
        ... )
        >>> error.error
        'authentication_failed'
        >>> error.message
        'Invalid email or password'
    """

    error: str = Field(..., description="Error type")
    message: str = Field(..., description="Human-readable error message")
    details: Optional[dict] = Field(None, description="Additional error details")


class SuccessResponse(BaseModel):
    """Standard success response schema.

    Attributes:
        success: Whether operation was successful
        message: Human-readable success message

    Examples:
        >>> response = SuccessResponse(
        ...     success=True,
        ...     message="Password changed successfully"
        ... )
        >>> response.success
        True
        >>> response.message
        'Password changed successfully'
    """

    success: bool = Field(True, description="Operation success status")
    message: str = Field(..., description="Human-readable success message")


# ---------------------------------------------------------------------------
# Team Management Schemas
# ---------------------------------------------------------------------------


class TeamCreateRequest(BaseModel):
    """Schema for creating a new team.

    Attributes:
        name: Team display name
        slug: URL-friendly team identifier (optional, auto-generated if not provided)
        description: Team description
        visibility: Team visibility level
        max_members: Maximum number of members allowed

    Examples:
        >>> request = TeamCreateRequest(
        ...     name="Engineering Team",
        ...     description="Software development team"
        ... )
        >>> request.name
        'Engineering Team'
        >>> request.visibility
        'private'
        >>> request.slug is None
        True
        >>>
        >>> # Test with all fields
        >>> full_request = TeamCreateRequest(
        ...     name="DevOps Team",
        ...     slug="devops-team",
        ...     description="Infrastructure and deployment team",
        ...     visibility="public",
        ...     max_members=50
        ... )
        >>> full_request.slug
        'devops-team'
        >>> full_request.max_members
        50
        >>> full_request.visibility
        'public'
        >>>
        >>> # Test validation
        >>> try:
        ...     TeamCreateRequest(name="   ", description="test")
        ... except ValueError as e:
        ...     "empty" in str(e).lower()
        True
        >>>
        >>> # Test slug validation
        >>> try:
        ...     TeamCreateRequest(name="Test", slug="Invalid_Slug")
        ... except ValueError:
        ...     True
        True
        >>>
        >>> # Test valid slug patterns
        >>> valid_slug = TeamCreateRequest(name="Test", slug="valid-slug-123")
        >>> valid_slug.slug
        'valid-slug-123'
    """

    name: str = Field(..., min_length=1, max_length=255, description="Team display name")
    slug: Optional[str] = Field(None, min_length=2, max_length=255, pattern="^[a-z0-9-]+$", description="URL-friendly team identifier")
    description: Optional[str] = Field(None, max_length=1000, description="Team description")
    visibility: Literal["private", "public"] = Field("private", description="Team visibility level")
    max_members: Optional[int] = Field(default=None, ge=1, description="Maximum number of team members. If omitted, the team inherits the global MAX_MEMBERS_PER_TEAM setting at check time.")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate team name.

        Args:
            v: Team name to validate

        Returns:
            str: Validated and stripped team name

        Raises:
            ValueError: If team name is empty or contains invalid characters
        """
        if not v.strip():
            raise ValueError("Team name cannot be empty")
        v = v.strip()
        # Strict validation: only alphanumeric, underscore, period, dash, and spaces
        if not re.match(settings.validation_name_pattern, v):
            raise ValueError("Team name can only contain letters, numbers, spaces, underscores, periods, and dashes")
        SecurityValidator.validate_no_xss(v, "Team name")
        if re.search(SecurityValidator.DANGEROUS_JS_PATTERN, v, re.IGNORECASE):
            raise ValueError("Team name contains script patterns that may cause security issues")
        return v

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Validate team description for XSS.

        Args:
            v: Team description to validate

        Returns:
            Optional[str]: Validated description or None

        Raises:
            ValueError: If description contains dangerous patterns
        """
        if v is not None:
            v = v.strip()
            if v:
                SecurityValidator.validate_no_xss(v, "Team description")
                if re.search(SecurityValidator.DANGEROUS_JS_PATTERN, v, re.IGNORECASE):
                    raise ValueError("Team description contains script patterns that may cause security issues")
        return v if v else None

    @field_validator("slug")
    @classmethod
    def validate_slug(cls, v: Optional[str]) -> Optional[str]:
        """Validate team slug.

        Args:
            v: Team slug to validate

        Returns:
            Optional[str]: Validated and formatted slug or None

        Raises:
            ValueError: If slug format is invalid
        """
        if v is None:
            return v
        v = v.strip().lower()
        # Uses precompiled regex for slug validation
        if not _SLUG_RE.match(v):
            raise ValueError("Slug must contain only lowercase letters, numbers, and hyphens")
        if v.startswith("-") or v.endswith("-"):
            raise ValueError("Slug cannot start or end with hyphens")
        return v


class TeamUpdateRequest(BaseModel):
    """Schema for updating a team.

    Attributes:
        name: Team display name
        description: Team description
        visibility: Team visibility level
        max_members: Maximum number of members allowed

    Examples:
        >>> request = TeamUpdateRequest(
        ...     name="Updated Engineering Team",
        ...     description="Updated description"
        ... )
        >>> request.name
        'Updated Engineering Team'
    """

    name: Optional[str] = Field(None, min_length=1, max_length=255, description="Team display name")
    description: Optional[str] = Field(None, max_length=1000, description="Team description")
    visibility: Optional[Literal["private", "public"]] = Field(None, description="Team visibility level")
    max_members: Optional[int] = Field(
        default=None, ge=1, description="Maximum number of team members. Set to null to clear a per-team override and revert to the global MAX_MEMBERS_PER_TEAM setting."
    )

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: Optional[str]) -> Optional[str]:
        """Validate team name.

        Args:
            v: Team name to validate

        Returns:
            Optional[str]: Validated and stripped team name or None

        Raises:
            ValueError: If team name is empty or contains invalid characters
        """
        if v is not None:
            if not v.strip():
                raise ValueError("Team name cannot be empty")
            v = v.strip()
            # Strict validation: only alphanumeric, underscore, period, dash, and spaces
            if not re.match(settings.validation_name_pattern, v):
                raise ValueError("Team name can only contain letters, numbers, spaces, underscores, periods, and dashes")
            SecurityValidator.validate_no_xss(v, "Team name")
            if re.search(SecurityValidator.DANGEROUS_JS_PATTERN, v, re.IGNORECASE):
                raise ValueError("Team name contains script patterns that may cause security issues")
            return v
        return v

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Validate team description for XSS.

        Args:
            v: Team description to validate

        Returns:
            Optional[str]: Validated description or None

        Raises:
            ValueError: If description contains dangerous patterns
        """
        if v is not None:
            v = v.strip()
            if v:
                SecurityValidator.validate_no_xss(v, "Team description")
                if re.search(SecurityValidator.DANGEROUS_JS_PATTERN, v, re.IGNORECASE):
                    raise ValueError("Team description contains script patterns that may cause security issues")
        return v if v else None


class TeamResponse(BaseModel):
    """Schema for team response data.

    Attributes:
        id: Team UUID
        name: Team display name
        slug: URL-friendly team identifier
        description: Team description
        created_by: Email of team creator
        is_personal: Whether this is a personal team
        visibility: Team visibility level
        max_members: Maximum number of members allowed
        member_count: Current number of team members
        created_at: Team creation timestamp
        updated_at: Last update timestamp
        is_active: Whether the team is active

    Examples:
        >>> team = TeamResponse(
        ...     id="team-123",
        ...     name="Engineering Team",
        ...     slug="engineering-team",
        ...     created_by="admin@example.com",
        ...     is_personal=False,
        ...     visibility="private",
        ...     member_count=5,
        ...     created_at=datetime.now(timezone.utc),
        ...     updated_at=datetime.now(timezone.utc),
        ...     is_active=True
        ... )
        >>> team.name
        'Engineering Team'
    """

    id: str = Field(..., description="Team UUID")
    name: str = Field(..., description="Team display name")
    slug: str = Field(..., description="URL-friendly team identifier")
    description: Optional[str] = Field(None, description="Team description")
    created_by: str = Field(..., description="Email of team creator")
    is_personal: bool = Field(..., description="Whether this is a personal team")
    visibility: Optional[Literal["private", "public"]] = Field(..., description="Team visibility level")
    max_members: Optional[int] = Field(None, description="Per-team member limit override. Null means the team uses the global MAX_MEMBERS_PER_TEAM setting.")
    member_count: int = Field(..., description="Current number of team members")
    created_at: datetime = Field(..., description="Team creation timestamp")
    updated_at: datetime = Field(..., description="Last update timestamp")
    is_active: bool = Field(..., description="Whether the team is active")


class TeamMemberResponse(BaseModel):
    """Schema for team member response data.

    Attributes:
        id: Member UUID
        team_id: Team UUID
        user_email: Member email address
        role: Member role in the team
        joined_at: When the member joined
        invited_by: Email of user who invited this member
        is_active: Whether the membership is active
        grant_source: Origin of the grant (e.g., 'sso', 'manual', 'bootstrap', 'auto')

    Examples:
        >>> member = TeamMemberResponse(
        ...     id="member-123",
        ...     team_id="team-123",
        ...     user_email="user@example.com",
        ...     role="member",
        ...     joined_at=datetime.now(timezone.utc),
        ...     is_active=True
        ... )
        >>> member.role
        'member'
    """

    model_config = ConfigDict(from_attributes=True)

    id: str = Field(..., description="Member UUID")
    team_id: str = Field(..., description="Team UUID")
    user_email: str = Field(..., description="Member email address")
    role: str = Field(..., description="Member role in the team")
    joined_at: datetime = Field(..., description="When the member joined")
    invited_by: Optional[str] = Field(None, description="Email of user who invited this member")
    is_active: bool = Field(..., description="Whether the membership is active")
    grant_source: Optional[str] = Field(None, description="Origin of the grant (e.g., 'sso', 'manual', 'bootstrap', 'auto')")


class PaginatedTeamMembersResponse(BaseModel):
    """Schema for paginated team member list response.

    Attributes:
        members: List of team members
        next_cursor: Optional cursor for next page of results

    Examples:
        >>> member1 = TeamMemberResponse(
        ...     id="member-1",
        ...     team_id="team-123",
        ...     user_email="user1@example.com",
        ...     role="member",
        ...     joined_at=datetime.now(timezone.utc),
        ...     is_active=True
        ... )
        >>> member2 = TeamMemberResponse(
        ...     id="member-2",
        ...     team_id="team-123",
        ...     user_email="user2@example.com",
        ...     role="member",
        ...     joined_at=datetime.now(timezone.utc),
        ...     is_active=True
        ... )
        >>> response = PaginatedTeamMembersResponse(
        ...     members=[member1, member2],
        ...     nextCursor="cursor-token-123"
        ... )
        >>> len(response.members)
        2
    """

    members: List[TeamMemberResponse] = Field(..., description="List of team members")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for next page of results")


class TeamInviteRequest(BaseModel):
    """Schema for inviting users to a team.

    Attributes:
        email: Email address of user to invite
        role: Role to assign to the user

    Examples:
        >>> invite = TeamInviteRequest(
        ...     email="newuser@example.com",
        ...     role="member"
        ... )
        >>> invite.email
        'newuser@example.com'
    """

    email: EmailStr = Field(..., description="Email address of user to invite")
    role: Literal["owner", "member"] = Field("member", description="Role to assign to the user")


class TeamInvitationResponse(BaseModel):
    """Schema for team invitation response data.

    Attributes:
        id: Invitation UUID
        team_id: Team UUID
        team_name: Team display name
        email: Email address of invited user
        role: Role the user will have when they accept
        invited_by: Email of user who sent the invitation
        invited_at: When the invitation was sent
        expires_at: When the invitation expires
        token: Invitation token
        is_active: Whether the invitation is active
        is_expired: Whether the invitation has expired

    Examples:
        >>> invitation = TeamInvitationResponse(
        ...     id="invite-123",
        ...     team_id="team-123",
        ...     team_name="Engineering Team",
        ...     email="newuser@example.com",
        ...     role="member",
        ...     invited_by="admin@example.com",
        ...     invited_at=datetime.now(timezone.utc),
        ...     expires_at=datetime.now(timezone.utc),
        ...     token="invitation-token",
        ...     is_active=True,
        ...     is_expired=False
        ... )
        >>> invitation.role
        'member'
    """

    id: str = Field(..., description="Invitation UUID")
    team_id: str = Field(..., description="Team UUID")
    team_name: str = Field(..., description="Team display name")
    email: str = Field(..., description="Email address of invited user")
    role: str = Field(..., description="Role the user will have when they accept")
    invited_by: str = Field(..., description="Email of user who sent the invitation")
    invited_at: datetime = Field(..., description="When the invitation was sent")
    expires_at: datetime = Field(..., description="When the invitation expires")
    token: str = Field(..., description="Invitation token")
    is_active: bool = Field(..., description="Whether the invitation is active")
    is_expired: bool = Field(..., description="Whether the invitation has expired")


class TeamMemberAddRequest(BaseModel):
    """Schema for adding a team member.

    Attributes:
        email: Email address of user to be added to the team
        role: New role for the team member
    """

    email: EmailStr = Field(..., description="Email address of user to be added to the team")
    role: Literal["owner", "member"] = Field(..., description="New role for the team member")


class TeamMemberUpdateRequest(BaseModel):
    """Schema for updating a team member's role.

    Attributes:
        role: New role for the team member

    Examples:
        >>> update = TeamMemberUpdateRequest(role="member")
        >>> update.role
        'member'
    """

    role: Literal["owner", "member"] = Field(..., description="New role for the team member")


class TeamListResponse(BaseModel):
    """Schema for team list response.

    Attributes:
        teams: List of teams
        total: Total number of teams

    Examples:
        >>> response = TeamListResponse(teams=[], total=0)
        >>> response.total
        0
    """

    teams: List[TeamResponse] = Field(..., description="List of teams")
    total: int = Field(..., description="Total number of teams")


class TeamDiscoveryResponse(BaseModel):
    """Schema for public team discovery response.

    Provides limited metadata about public teams for discovery purposes.

    Attributes:
        id: Team ID
        name: Team name
        description: Team description
        member_count: Number of members
        created_at: Team creation timestamp
        is_joinable: Whether the current user can join this team
    """

    id: str = Field(..., description="Team ID")
    name: str = Field(..., description="Team name")
    description: Optional[str] = Field(None, description="Team description")
    member_count: int = Field(..., description="Number of team members")
    created_at: datetime = Field(..., description="Team creation timestamp")
    is_joinable: bool = Field(..., description="Whether the current user can join this team")


class TeamJoinRequest(BaseModel):
    """Schema for requesting to join a public team.

    Attributes:
        message: Optional message to team owners
    """

    message: Optional[str] = Field(None, description="Optional message to team owners", max_length=500)


class TeamJoinRequestResponse(BaseModel):
    """Schema for team join request response.

    Attributes:
        id: Join request ID
        team_id: Target team ID
        team_name: Target team name
        user_email: Requesting user email
        message: Request message
        status: Request status (pending, approved, rejected)
        requested_at: Request timestamp
        expires_at: Request expiration timestamp
    """

    id: str = Field(..., description="Join request ID")
    team_id: str = Field(..., description="Target team ID")
    team_name: str = Field(..., description="Target team name")
    user_email: str = Field(..., description="Requesting user email")
    message: Optional[str] = Field(None, description="Request message")
    status: str = Field(..., description="Request status")
    requested_at: datetime = Field(..., description="Request timestamp")
    expires_at: datetime = Field(..., description="Request expiration")


# API Token Management Schemas


class TokenScopeRequest(BaseModel):
    """Schema for token scoping configuration.

    Attributes:
        server_id: Optional server ID limitation
        permissions: List of permission scopes
        ip_restrictions: List of IP address/CIDR restrictions
        time_restrictions: Time-based access limitations
        usage_limits: Rate limiting and quota settings

    Examples:
        >>> scope = TokenScopeRequest(
        ...     server_id="server-123",
        ...     permissions=["tools.read", "resources.read"],
        ...     ip_restrictions=["192.168.1.0/24"]
        ... )
        >>> scope.server_id
        'server-123'
    """

    server_id: Optional[str] = Field(None, description="Limit token to specific server")
    permissions: List[str] = Field(default_factory=list, description="Permission scopes")
    ip_restrictions: List[str] = Field(default_factory=list, description="IP address restrictions")
    time_restrictions: Dict[str, Any] = Field(default_factory=dict, description="Time-based restrictions")
    usage_limits: Dict[str, Any] = Field(default_factory=dict, description="Usage limits and quotas")

    @field_validator("ip_restrictions")
    @classmethod
    def validate_ip_restrictions(cls, v: List[str]) -> List[str]:
        """Validate IP addresses and CIDR notation.

        Args:
            v: List of IP address or CIDR strings to validate.

        Returns:
            List of validated IP/CIDR strings with whitespace stripped.

        Raises:
            ValueError: If any IP address or CIDR notation is invalid.

        Examples:
            >>> TokenScopeRequest.validate_ip_restrictions(["192.168.1.0/24"])
            ['192.168.1.0/24']
            >>> TokenScopeRequest.validate_ip_restrictions(["10.0.0.1"])
            ['10.0.0.1']
        """
        # Standard
        import ipaddress  # pylint: disable=import-outside-toplevel

        if not v:
            return v

        validated = []
        for ip_str in v:
            ip_str = ip_str.strip()
            if not ip_str:
                continue
            try:
                # Try parsing as network (CIDR notation)
                if "/" in ip_str:
                    ipaddress.ip_network(ip_str, strict=False)
                else:
                    # Try parsing as single IP address
                    ipaddress.ip_address(ip_str)
                validated.append(ip_str)
            except ValueError as e:
                raise ValueError(f"Invalid IP address or CIDR notation '{ip_str}': {e}") from e
        return validated

    @field_validator("permissions")
    @classmethod
    def validate_permissions(cls, v: List[str]) -> List[str]:
        """Validate permission scope format.

        Permissions must be in format 'resource.action' or wildcard '*'.

        Args:
            v: List of permission strings to validate.

        Returns:
            List of validated permission strings with whitespace stripped.

        Raises:
            ValueError: If any permission does not match 'resource.action' format or '*'.

        Examples:
            >>> TokenScopeRequest.validate_permissions(["tools.read", "resources.write"])
            ['tools.read', 'resources.write']
            >>> TokenScopeRequest.validate_permissions(["*"])
            ['*']
        """
        if not v:
            return v

        # Permission pattern: resource.action (alphanumeric with underscores)
        permission_pattern = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*$")

        validated = []
        for perm in v:
            perm = perm.strip()
            if not perm:
                continue
            # Allow wildcard
            if perm == "*":
                validated.append(perm)
                continue
            if not permission_pattern.match(perm):
                raise ValueError(f"Invalid permission format '{perm}'. Use 'resource.action' format (e.g., 'tools.read') or '*' for full access")
            validated.append(perm)
        return validated


class TokenCreateRequest(BaseModel):
    """Schema for creating a new API token.

    Attributes:
        name: Human-readable token name
        description: Optional token description
        expires_in_days: Optional expiry in days
        scope: Optional token scoping configuration
        tags: Optional organizational tags
        team_id: Optional team ID for team-scoped tokens
        is_active: Token active status (defaults to True)
        user_email: Optional email of user to create token for (admin only)

    Examples:
        >>> request = TokenCreateRequest(
        ...     name="Production Access",
        ...     description="Read-only production access",
        ...     expires_in_days=30,
        ...     tags=["production", "readonly"]
        ... )
        >>> request.name
        'Production Access'
    """

    name: str = Field(..., description="Human-readable token name", min_length=1, max_length=255)
    description: Optional[str] = Field(None, description="Token description", max_length=1000)
    expires_in_days: Optional[int] = Field(default=None, ge=1, description="Expiry in days (must be >= 1 if specified)")
    scope: Optional[TokenScopeRequest] = Field(None, description="Token scoping configuration")
    tags: List[str] = Field(default_factory=list, description="Organizational tags")
    team_id: Optional[str] = Field(None, description="Team ID for team-scoped tokens")
    is_active: bool = Field(default=True, description="Token active status")
    user_email: Optional[EmailStr] = Field(None, description="Email of user to create token for (admin only)")


class TokenUpdateRequest(BaseModel):
    """Schema for updating an existing API token.

    Attributes:
        name: New token name
        description: New token description
        scope: New token scoping configuration
        tags: New organizational tags
        is_active: New token active status

    Examples:
        >>> request = TokenUpdateRequest(
        ...     name="Updated Token Name",
        ...     description="Updated description"
        ... )
        >>> request.name
        'Updated Token Name'
    """

    name: Optional[str] = Field(None, description="New token name", min_length=1, max_length=255)
    description: Optional[str] = Field(None, description="New token description", max_length=1000)
    scope: Optional[TokenScopeRequest] = Field(None, description="New token scoping configuration")
    tags: Optional[List[str]] = Field(None, description="New organizational tags")
    is_active: Optional[bool] = Field(None, description="New token active status")


class TokenResponse(BaseModel):
    """Schema for API token response.

    Attributes:
        id: Token ID
        name: Token name
        description: Token description
        server_id: Server scope limitation
        resource_scopes: Permission scopes
        ip_restrictions: IP restrictions
        time_restrictions: Time-based restrictions
        usage_limits: Usage limits
        created_at: Creation timestamp
        expires_at: Expiry timestamp
        last_used: Last usage timestamp
        is_active: Active status
        tags: Organizational tags

    Examples:
        >>> from datetime import datetime
        >>> token = TokenResponse(
        ...     id="token-123",
        ...     name="Test Token",
        ...     description="Test description",
        ...     user_email="test@example.com",
        ...     server_id=None,
        ...     resource_scopes=["tools.read"],
        ...     ip_restrictions=[],
        ...     time_restrictions={},
        ...     usage_limits={},
        ...     created_at=datetime.now(),
        ...     expires_at=None,
        ...     last_used=None,
        ...     is_active=True,
        ...     tags=[]
        ... )
        >>> token.name
        'Test Token'
    """

    model_config = ConfigDict(from_attributes=True)

    id: str = Field(..., description="Token ID")
    name: str = Field(..., description="Token name")
    description: Optional[str] = Field(None, description="Token description")
    user_email: str = Field(..., description="Token owner's email")
    team_id: Optional[str] = Field(None, description="Team ID for team-scoped tokens")
    server_id: Optional[str] = Field(None, description="Server scope limitation")
    resource_scopes: List[str] = Field(..., description="Permission scopes")
    ip_restrictions: List[str] = Field(..., description="IP restrictions")
    time_restrictions: Dict[str, Any] = Field(..., description="Time-based restrictions")
    usage_limits: Dict[str, Any] = Field(..., description="Usage limits")
    created_at: datetime = Field(..., description="Creation timestamp")
    expires_at: Optional[datetime] = Field(None, description="Expiry timestamp")
    last_used: Optional[datetime] = Field(None, description="Last usage timestamp")
    is_active: bool = Field(..., description="Active status")
    is_revoked: bool = Field(False, description="Whether token is revoked")
    revoked_at: Optional[datetime] = Field(None, description="Revocation timestamp")
    revoked_by: Optional[str] = Field(None, description="Email of user who revoked token")
    revocation_reason: Optional[str] = Field(None, description="Reason for revocation")
    tags: List[str] = Field(..., description="Organizational tags")


class TokenCreateResponse(BaseModel):
    """Schema for token creation response.

    Attributes:
        token: Token information
        access_token: The actual token string (only returned on creation)

    Examples:
        >>> from datetime import datetime
        >>> token_info = TokenResponse(
        ...     id="token-123", name="Test Token", description=None,
        ...     user_email="test@example.com", server_id=None, resource_scopes=[], ip_restrictions=[],
        ...     time_restrictions={}, usage_limits={}, created_at=datetime.now(),
        ...     expires_at=None, last_used=None, is_active=True, tags=[]
        ... )
        >>> response = TokenCreateResponse(
        ...     token=token_info,
        ...     access_token="abc123xyz"
        ... )
        >>> response.access_token
        'abc123xyz'
    """

    token: TokenResponse = Field(..., description="Token information")
    access_token: str = Field(..., description="The actual token string")


class TokenListResponse(BaseModel):
    """Schema for token list response.

    Attributes:
        tokens: List of tokens
        total: Total number of tokens
        limit: Request limit
        offset: Request offset

    Examples:
        >>> response = TokenListResponse(
        ...     tokens=[],
        ...     total=0,
        ...     limit=10,
        ...     offset=0
        ... )
        >>> response.total
        0
    """

    tokens: List[TokenResponse] = Field(..., description="List of tokens")
    total: int = Field(..., description="Total number of tokens")
    limit: int = Field(..., description="Request limit")
    offset: int = Field(..., description="Request offset")


class TokenRevokeRequest(BaseModel):
    """Schema for token revocation.

    Attributes:
        reason: Optional reason for revocation

    Examples:
        >>> request = TokenRevokeRequest(reason="Security incident")
        >>> request.reason
        'Security incident'
    """

    reason: Optional[str] = Field(None, description="Reason for revocation", max_length=255)


class TokenUsageStatsResponse(BaseModel):
    """Schema for token usage statistics.

    Attributes:
        period_days: Number of days analyzed
        total_requests: Total number of requests
        successful_requests: Number of successful requests
        blocked_requests: Number of blocked requests
        success_rate: Success rate percentage
        average_response_time_ms: Average response time
        top_endpoints: Most accessed endpoints

    Examples:
        >>> stats = TokenUsageStatsResponse(
        ...     period_days=30,
        ...     total_requests=100,
        ...     successful_requests=95,
        ...     blocked_requests=5,
        ...     success_rate=0.95,
        ...     average_response_time_ms=150.5,
        ...     top_endpoints=[("/tools", 50), ("/resources", 30)]
        ... )
        >>> stats.success_rate
        0.95
    """

    period_days: int = Field(..., description="Number of days analyzed")
    total_requests: int = Field(..., description="Total number of requests")
    successful_requests: int = Field(..., description="Number of successful requests")
    blocked_requests: int = Field(..., description="Number of blocked requests")
    success_rate: float = Field(..., description="Success rate (0-1)")
    average_response_time_ms: float = Field(..., description="Average response time in milliseconds")
    top_endpoints: List[tuple[str, int]] = Field(..., description="Most accessed endpoints with counts")


# ===== RBAC Schemas =====


class RoleCreateRequest(BaseModel):
    """Schema for creating a new role.

    Attributes:
        name: Unique role name
        description: Role description
        scope: Role scope (global, team, personal)
        permissions: List of permission strings
        inherits_from: Optional parent role ID
        is_system_role: Whether this is a system role

    Examples:
        >>> request = RoleCreateRequest(
        ...     name="team_admin",
        ...     description="Team administrator with member management",
        ...     scope="team",
        ...     permissions=["teams.manage_members", "resources.create"]
        ... )
        >>> request.name
        'team_admin'
    """

    name: str = Field(..., description="Unique role name", max_length=255)
    description: Optional[str] = Field(None, description="Role description")
    scope: str = Field(..., description="Role scope", pattern="^(global|team|personal)$")
    permissions: List[str] = Field(..., description="List of permission strings")
    inherits_from: Optional[str] = Field(None, description="Parent role ID for inheritance")
    is_system_role: Optional[bool] = Field(False, description="Whether this is a system role")


class RoleUpdateRequest(BaseModel):
    """Schema for updating an existing role.

    Attributes:
        name: Optional new name
        description: Optional new description
        permissions: Optional new permissions list
        inherits_from: Optional new parent role
        is_active: Optional active status

    Examples:
        >>> request = RoleUpdateRequest(
        ...     description="Updated role description",
        ...     permissions=["new.permission"]
        ... )
        >>> request.description
        'Updated role description'
    """

    name: Optional[str] = Field(None, description="Role name", max_length=255)
    description: Optional[str] = Field(None, description="Role description")
    permissions: Optional[List[str]] = Field(None, description="List of permission strings")
    inherits_from: Optional[str] = Field(None, description="Parent role ID for inheritance")
    is_active: Optional[bool] = Field(None, description="Whether role is active")


class RoleResponse(BaseModel):
    """Schema for role response.

    Attributes:
        id: Role identifier
        name: Role name
        description: Role description
        scope: Role scope
        permissions: List of permissions
        effective_permissions: All permissions including inherited
        inherits_from: Parent role ID
        created_by: Creator email
        is_system_role: Whether system role
        is_active: Whether role is active
        created_at: Creation timestamp
        updated_at: Update timestamp

    Examples:
        >>> role = RoleResponse(
        ...     id="role-123",
        ...     name="admin",
        ...     scope="global",
        ...     permissions=["*"],
        ...     effective_permissions=["*"],
        ...     created_by="admin@example.com",
        ...     is_system_role=True,
        ...     is_active=True,
        ...     created_at=datetime.now(),
        ...     updated_at=datetime.now()
        ... )
        >>> role.name
        'admin'
    """

    model_config = ConfigDict(from_attributes=True)

    id: str = Field(..., description="Role identifier")
    name: str = Field(..., description="Role name")
    description: Optional[str] = Field(None, description="Role description")
    scope: str = Field(..., description="Role scope")
    permissions: List[str] = Field(..., description="Direct permissions")
    effective_permissions: Optional[List[str]] = Field(None, description="All permissions including inherited")
    inherits_from: Optional[str] = Field(None, description="Parent role ID")
    created_by: str = Field(..., description="Creator email")
    is_system_role: bool = Field(..., description="Whether system role")
    is_active: bool = Field(..., description="Whether role is active")
    created_at: datetime = Field(..., description="Creation timestamp")
    updated_at: datetime = Field(..., description="Update timestamp")


class UserRoleAssignRequest(BaseModel):
    """Schema for assigning a role to a user.

    Attributes:
        role_id: Role to assign
        scope: Assignment scope
        scope_id: Team ID if team-scoped
        expires_at: Optional expiration timestamp

    Examples:
        >>> request = UserRoleAssignRequest(
        ...     role_id="role-123",
        ...     scope="team",
        ...     scope_id="team-456"
        ... )
        >>> request.scope
        'team'
    """

    role_id: str = Field(..., description="Role ID to assign")
    scope: str = Field(..., description="Assignment scope", pattern="^(global|team|personal)$")
    scope_id: Optional[str] = Field(None, description="Team ID if team-scoped")
    expires_at: Optional[datetime] = Field(None, description="Optional expiration timestamp")


class UserRoleResponse(BaseModel):
    """Schema for user role assignment response.

    Attributes:
        id: Assignment identifier
        user_email: User email
        role_id: Role identifier
        role_name: Role name for convenience
        scope: Assignment scope
        scope_id: Team ID if applicable
        granted_by: Who granted the role
        granted_at: When role was granted
        expires_at: Optional expiration
        is_active: Whether assignment is active

    Examples:
        >>> user_role = UserRoleResponse(
        ...     id="assignment-123",
        ...     user_email="user@example.com",
        ...     role_id="role-456",
        ...     role_name="team_admin",
        ...     scope="team",
        ...     scope_id="team-789",
        ...     granted_by="admin@example.com",
        ...     granted_at=datetime.now(),
        ...     is_active=True
        ... )
        >>> user_role.scope
        'team'
    """

    model_config = ConfigDict(from_attributes=True)

    id: str = Field(..., description="Assignment identifier")
    user_email: str = Field(..., description="User email")
    role_id: str = Field(..., description="Role identifier")
    role_name: Optional[str] = Field(None, description="Role name for convenience")
    scope: str = Field(..., description="Assignment scope")
    scope_id: Optional[str] = Field(None, description="Team ID if applicable")
    granted_by: str = Field(..., description="Who granted the role")
    granted_at: datetime = Field(..., description="When role was granted")
    expires_at: Optional[datetime] = Field(None, description="Optional expiration")
    is_active: bool = Field(..., description="Whether assignment is active")
    grant_source: Optional[str] = Field(None, description="Origin of the grant (e.g., 'sso', 'manual', 'bootstrap', 'auto')")


class PermissionCheckRequest(BaseModel):
    """Schema for permission check request.

    Attributes:
        user_email: User to check
        permission: Permission to verify
        resource_type: Optional resource type
        resource_id: Optional resource ID
        team_id: Optional team context

    Examples:
        >>> request = PermissionCheckRequest(
        ...     user_email="user@example.com",
        ...     permission="tools.create",
        ...     resource_type="tools"
        ... )
        >>> request.permission
        'tools.create'
    """

    user_email: str = Field(..., description="User email to check")
    permission: str = Field(..., description="Permission to verify")
    resource_type: Optional[str] = Field(None, description="Resource type")
    resource_id: Optional[str] = Field(None, description="Resource ID")
    team_id: Optional[str] = Field(None, description="Team context")


class PermissionCheckResponse(BaseModel):
    """Schema for permission check response.

    Attributes:
        user_email: User checked
        permission: Permission checked
        granted: Whether permission was granted
        checked_at: When check was performed
        checked_by: Who performed the check

    Examples:
        >>> response = PermissionCheckResponse(
        ...     user_email="user@example.com",
        ...     permission="tools.create",
        ...     granted=True,
        ...     checked_at=datetime.now(),
        ...     checked_by="admin@example.com"
        ... )
        >>> response.granted
        True
    """

    user_email: str = Field(..., description="User email checked")
    permission: str = Field(..., description="Permission checked")
    granted: bool = Field(..., description="Whether permission was granted")
    checked_at: datetime = Field(..., description="When check was performed")
    checked_by: str = Field(..., description="Who performed the check")


class PermissionListResponse(BaseModel):
    """Schema for available permissions list.

    Attributes:
        all_permissions: List of all available permissions
        permissions_by_resource: Permissions grouped by resource type
        total_count: Total number of permissions

    Examples:
        >>> response = PermissionListResponse(
        ...     all_permissions=["users.create", "tools.read"],
        ...     permissions_by_resource={"users": ["users.create"], "tools": ["tools.read"]},
        ...     total_count=2
        ... )
        >>> response.total_count
        2
    """

    all_permissions: List[str] = Field(..., description="All available permissions")
    permissions_by_resource: Dict[str, List[str]] = Field(..., description="Permissions by resource type")
    total_count: int = Field(..., description="Total number of permissions")


# ==============================================================================
# SSO Authentication Schemas
# ==============================================================================


class SSOProviderResponse(BaseModelWithConfigDict):
    """Response schema for SSO provider information.

    Attributes:
        id: Provider identifier (e.g., 'github', 'google')
        name: Provider name
        display_name: Human-readable display name
        provider_type: Type of provider ('oauth2', 'oidc')
        is_enabled: Whether provider is currently enabled
        authorization_url: OAuth authorization URL (optional)

    Examples:
        >>> provider = SSOProviderResponse(
        ...     id="github",
        ...     name="github",
        ...     display_name="GitHub",
        ...     provider_type="oauth2",
        ...     is_enabled=True
        ... )
        >>> provider.id
        'github'
    """

    id: str = Field(..., description="Provider identifier")
    name: str = Field(..., description="Provider name")
    display_name: str = Field(..., description="Human-readable display name")
    provider_type: Optional[str] = Field(None, description="Provider type (oauth2, oidc)")
    is_enabled: Optional[bool] = Field(None, description="Whether provider is enabled")
    authorization_url: Optional[str] = Field(None, description="OAuth authorization URL")
    jwks_uri: Optional[str] = Field(None, description="OIDC JWKS endpoint for token signature verification")


class SSOLoginResponse(BaseModelWithConfigDict):
    """Response schema for SSO login initiation.

    Attributes:
        authorization_url: URL to redirect user for authentication
        state: CSRF state parameter for validation

    Examples:
        >>> login = SSOLoginResponse(
        ...     authorization_url="https://github.com/login/oauth/authorize?...",
        ...     state="csrf-token-123"
        ... )
        >>> "github.com" in login.authorization_url
        True
    """

    authorization_url: str = Field(..., description="OAuth authorization URL")
    state: str = Field(..., description="CSRF state parameter")


class SSOCallbackResponse(BaseModelWithConfigDict):
    """Response schema for SSO authentication callback.

    Attributes:
        access_token: JWT access token for authenticated user
        token_type: Token type (always 'bearer')
        expires_in: Token expiration time in seconds
        user: User information from SSO provider

    Examples:
        >>> callback = SSOCallbackResponse(
        ...     access_token="jwt.token.here",
        ...     token_type="bearer",
        ...     expires_in=3600,
        ...     user={"email": "user@example.com", "full_name": "User"}
        ... )
        >>> callback.token_type
        'bearer'
    """

    access_token: str = Field(..., description="JWT access token")
    token_type: str = Field(default="bearer", description="Token type")
    expires_in: int = Field(..., description="Token expiration in seconds")
    user: Dict[str, Any] = Field(..., description="User information")


# gRPC Service schemas


class GrpcServiceCreate(BaseModel):
    """Schema for creating a new gRPC service."""

    name: str = Field(..., min_length=1, max_length=255, description="Unique name for the gRPC service")
    target: str = Field(..., description="gRPC server target address (host:port)")
    description: Optional[str] = Field(None, description="Description of the gRPC service")
    reflection_enabled: bool = Field(default=True, description="Enable gRPC server reflection")
    tls_enabled: bool = Field(default=False, description="Enable TLS for gRPC connection")
    tls_cert_path: Optional[str] = Field(None, description="Path to TLS certificate file")
    tls_key_path: Optional[str] = Field(None, description="Path to TLS key file")
    grpc_metadata: Dict[str, str] = Field(default_factory=dict, description="gRPC metadata headers")
    tags: List[str] = Field(default_factory=list, description="Tags for categorization")

    # Team scoping fields
    team_id: Optional[str] = Field(None, description="ID of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Email of the user who owns this resource")
    visibility: Literal["private", "team", "public"] = Field(default="public", description="Visibility level: private, team, or public")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Validate service name.

        Args:
            v: Service name to validate

        Returns:
            Validated service name
        """
        return SecurityValidator.validate_name(v, "gRPC service name")

    @field_validator("target")
    @classmethod
    def validate_target(cls, v: str) -> str:
        """Validate target address format (host:port).

        Args:
            v: Target address to validate

        Returns:
            Validated target address

        Raises:
            ValueError: If target is not in host:port format
        """
        if not v or ":" not in v:
            raise ValueError("Target must be in host:port format")
        return v

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Validate description.

        Args:
            v: Description to validate

        Returns:
            Validated and sanitized description
        """
        if v is None:
            return None
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")


class GrpcServiceUpdate(BaseModel):
    """Schema for updating an existing gRPC service."""

    name: Optional[str] = Field(None, min_length=1, max_length=255, description="Service name")
    target: Optional[str] = Field(None, description="gRPC server target address")
    description: Optional[str] = Field(None, description="Service description")
    reflection_enabled: Optional[bool] = Field(None, description="Enable server reflection")
    tls_enabled: Optional[bool] = Field(None, description="Enable TLS")
    tls_cert_path: Optional[str] = Field(None, description="TLS certificate path")
    tls_key_path: Optional[str] = Field(None, description="TLS key path")
    grpc_metadata: Optional[Dict[str, str]] = Field(None, description="gRPC metadata headers")
    tags: Optional[List[str]] = Field(None, description="Service tags")
    visibility: Optional[Literal["private", "team", "public"]] = Field(None, description="Visibility level: private, team, or public")

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: Optional[str]) -> Optional[str]:
        """Validate service name.

        Args:
            v: Service name to validate

        Returns:
            Validated service name or None
        """
        if v is None:
            return None
        return SecurityValidator.validate_name(v, "gRPC service name")

    @field_validator("target")
    @classmethod
    def validate_target(cls, v: Optional[str]) -> Optional[str]:
        """Validate target address.

        Args:
            v: Target address to validate

        Returns:
            Validated target address or None

        Raises:
            ValueError: If target is not in host:port format
        """
        if v is None:
            return None
        if ":" not in v:
            raise ValueError("Target must be in host:port format")
        return v

    @field_validator("description")
    @classmethod
    def validate_description(cls, v: Optional[str]) -> Optional[str]:
        """Validate description.

        Args:
            v: Description to validate

        Returns:
            Validated and sanitized description
        """
        if v is None:
            return None
        if len(v) > SecurityValidator.MAX_DESCRIPTION_LENGTH:
            truncated = v[: SecurityValidator.MAX_DESCRIPTION_LENGTH]
            logger.info(f"Description too long, truncated to {SecurityValidator.MAX_DESCRIPTION_LENGTH} characters.")
            return SecurityValidator.sanitize_display_text(truncated, "Description")
        return SecurityValidator.sanitize_display_text(v, "Description")


class GrpcServiceRead(BaseModel):
    """Schema for reading gRPC service information."""

    model_config = ConfigDict(from_attributes=True)

    id: str = Field(..., description="Unique service identifier")
    name: str = Field(..., description="Service name")
    slug: str = Field(..., description="URL-safe slug")
    target: str = Field(..., description="gRPC server target (host:port)")
    description: Optional[str] = Field(None, description="Service description")

    # Configuration
    reflection_enabled: bool = Field(..., description="Reflection enabled")
    tls_enabled: bool = Field(..., description="TLS enabled")
    tls_cert_path: Optional[str] = Field(None, description="TLS certificate path")
    tls_key_path: Optional[str] = Field(None, description="TLS key path")
    grpc_metadata: Dict[str, str] = Field(default_factory=dict, description="gRPC metadata")

    # Status
    enabled: bool = Field(..., description="Service enabled")
    reachable: bool = Field(..., description="Service reachable")

    # Discovery
    service_count: int = Field(default=0, description="Number of gRPC services discovered")
    method_count: int = Field(default=0, description="Number of methods discovered")
    discovered_services: Dict[str, Any] = Field(default_factory=dict, description="Discovered service descriptors")
    last_reflection: Optional[datetime] = Field(None, description="Last reflection timestamp")

    # Tags
    tags: List[str] = Field(default_factory=list, description="Service tags")

    # Timestamps
    created_at: datetime = Field(..., description="Creation timestamp")
    updated_at: datetime = Field(..., description="Last update timestamp")

    # Team scoping
    team_id: Optional[str] = Field(None, description="Team ID")
    team: Optional[str] = Field(None, description="Name of the team that owns this resource")
    owner_email: Optional[str] = Field(None, description="Owner email")
    visibility: Literal["private", "team", "public"] = Field(default="public", description="Visibility level: private, team, or public")

    _normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))


# Plugin-related schemas


class PluginSummary(BaseModel):
    """Summary information for a plugin in list views."""

    name: str = Field(..., description="Unique plugin name")
    description: str = Field("", description="Plugin description")
    author: str = Field("Unknown", description="Plugin author")
    version: str = Field("0.0.0", description="Plugin version")
    mode: str = Field(..., description="Plugin mode: enforce, permissive, or disabled")
    priority: int = Field(..., description="Plugin execution priority (lower = higher priority)")
    hooks: List[str] = Field(default_factory=list, description="Hook points where plugin executes")
    tags: List[str] = Field(default_factory=list, description="Plugin tags for categorization")
    status: str = Field(..., description="Plugin status: enabled or disabled")
    config_summary: Dict[str, Any] = Field(default_factory=dict, description="Summary of plugin configuration")


class PluginDetail(PluginSummary):
    """Detailed plugin information including full configuration."""

    kind: str = Field("", description="Plugin type or class")
    namespace: Optional[str] = Field(None, description="Plugin namespace")
    conditions: List[Any] = Field(default_factory=list, description="Conditions for plugin execution")
    config: Dict[str, Any] = Field(default_factory=dict, description="Full plugin configuration")
    manifest: Optional[Dict[str, Any]] = Field(None, description="Plugin manifest information")


class PluginListResponse(BaseModel):
    """Response for plugin list endpoint."""

    plugins_globally_enabled: bool = Field(True, description="Whether the plugin subsystem is globally enabled at runtime")
    plugins: List[PluginSummary] = Field(..., description="List of plugins")
    total: int = Field(..., description="Total number of plugins")
    enabled_count: int = Field(0, description="Number of enabled plugins")
    disabled_count: int = Field(0, description="Number of disabled plugins")


class PluginToggleRequest(BaseModel):
    """Request body for ``PUT /admin/plugins`` — toggles the plugin subsystem globally."""

    enabled: bool = Field(..., description="Activate plugins when true, deactivate when false")


class PluginToggleResponse(BaseModel):
    """Response body for the global plugin toggle endpoint."""

    plugins_enabled: bool = Field(..., description="Effective plugin subsystem state after the toggle")
    redis_persisted: bool = Field(..., description="True when the shared Redis toggle accepted the write")


class PluginModeUpdateRequest(BaseModel):
    """Request body for ``PUT /admin/plugins/{name}`` — drives the Pydantic enum validation."""

    # Mirrors PluginMode.value; importing the enum here would create a cycle
    # (schemas -> plugins.framework -> services -> schemas), so use a Literal.
    mode: Literal[
        "enforce",
        "enforce_ignore_error",
        "permissive",
        "disabled",
        "sequential",
        "concurrent",
        "transform",
        "audit",
        "fire_and_forget",
    ] = Field(..., description="Plugin mode: enforce, enforce_ignore_error, permissive, disabled, or cpex native modes")


class PluginModeUpdateResponse(BaseModel):
    """Response body for the per-plugin mode update endpoint."""

    plugin: str = Field(..., description="Plugin name whose mode was updated")
    mode: str = Field(..., description="New plugin mode")
    redis_persisted: bool = Field(..., description="True when the shared Redis override accepted the write")


class PluginStatsResponse(BaseModel):
    """Response for plugin statistics endpoint."""

    total_plugins: int = Field(..., description="Total number of plugins")
    enabled_plugins: int = Field(..., description="Number of enabled plugins")
    disabled_plugins: int = Field(..., description="Number of disabled plugins")
    plugins_by_hook: Dict[str, int] = Field(default_factory=dict, description="Plugin count by hook type")
    plugins_by_mode: Dict[str, int] = Field(default_factory=dict, description="Plugin count by mode")


# MCP Server Catalog Schemas


class CatalogServer(BaseModel):
    """Schema for a catalog server entry."""

    id: str = Field(..., description="Unique identifier for the catalog server")
    name: str = Field(..., description="Display name of the server")
    category: str = Field(..., description="Server category (e.g., Project Management, Software Development)")
    url: str = Field(..., description="Server endpoint URL")
    auth_type: str = Field(..., description="Authentication type (e.g., OAuth2.1, API Key, Open)")
    provider: str = Field(..., description="Provider/vendor name")
    description: str = Field(..., description="Server description")
    requires_api_key: bool = Field(default=False, description="Whether API key is required")
    secure: bool = Field(default=False, description="Whether additional security is required")
    tags: List[str] = Field(default_factory=list, description="Tags for categorization")
    transport: Optional[str] = Field(None, description="Transport type: SSE, STREAMABLEHTTP, or WEBSOCKET")
    logo_url: Optional[str] = Field(None, description="URL to server logo/icon")
    documentation_url: Optional[str] = Field(None, description="URL to server documentation")
    is_registered: bool = Field(default=False, description="Whether server is already registered")
    is_available: bool = Field(default=True, description="Whether server is currently available")
    requires_oauth_config: bool = Field(default=False, description="Whether server is registered but needs OAuth configuration")


class CatalogServerRegisterRequest(BaseModel):
    """Request to register a catalog server."""

    server_id: str = Field(..., description="Catalog server ID to register")
    name: Optional[str] = Field(None, description="Optional custom name for the server")
    api_key: Optional[str] = Field(None, description="API key if required")
    oauth_credentials: Optional[Dict[str, Any]] = Field(None, description="OAuth credentials if required")


class CatalogServerRegisterResponse(BaseModel):
    """Response after registering a catalog server."""

    success: bool = Field(..., description="Whether registration was successful")
    server_id: str = Field(..., description="ID of the registered server in the system")
    message: str = Field(..., description="Status message")
    error: Optional[str] = Field(None, description="Error message if registration failed")
    oauth_required: bool = Field(False, description="Whether OAuth configuration is required before activation")


class CatalogServerStatusRequest(BaseModel):
    """Request to check catalog server status."""

    server_id: str = Field(..., description="Catalog server ID to check")


class CatalogServerStatusResponse(BaseModel):
    """Response for catalog server status check."""

    server_id: str = Field(..., description="Catalog server ID")
    is_available: bool = Field(..., description="Whether server is reachable")
    is_registered: bool = Field(..., description="Whether server is registered")
    last_checked: Optional[datetime] = Field(None, description="Last health check timestamp")
    response_time_ms: Optional[float] = Field(None, description="Response time in milliseconds")
    error: Optional[str] = Field(None, description="Error message if check failed")


class CatalogListRequest(BaseModel):
    """Request to list catalog servers."""

    category: Optional[str] = Field(None, description="Filter by category")
    auth_type: Optional[str] = Field(None, description="Filter by auth type")
    provider: Optional[str] = Field(None, description="Filter by provider")
    search: Optional[str] = Field(None, description="Search term for name/description")
    tags: Optional[List[str]] = Field(None, description="Filter by tags")
    show_registered_only: bool = Field(default=False, description="Show only registered servers")
    show_available_only: bool = Field(default=True, description="Show only available servers")
    limit: int = Field(default=100, description="Maximum number of results")
    offset: int = Field(default=0, description="Offset for pagination")


class CatalogListResponse(BaseModel):
    """Response containing catalog servers."""

    servers: List[CatalogServer] = Field(..., description="List of catalog servers")
    total: int = Field(..., description="Total number of matching servers")
    categories: List[str] = Field(..., description="Available categories")
    auth_types: List[str] = Field(..., description="Available auth types")
    providers: List[str] = Field(..., description="Available providers")
    all_tags: List[str] = Field(default_factory=list, description="All available tags")


class CatalogBulkRegisterRequest(BaseModel):
    """Request to register multiple catalog servers."""

    server_ids: List[str] = Field(..., description="List of catalog server IDs to register")
    skip_errors: bool = Field(default=True, description="Continue on error")


class CatalogBulkRegisterResponse(BaseModel):
    """Response after bulk registration."""

    successful: List[str] = Field(..., description="Successfully registered server IDs")
    failed: List[Dict[str, str]] = Field(..., description="Failed registrations with error messages")
    total_attempted: int = Field(..., description="Total servers attempted")
    total_successful: int = Field(..., description="Total successful registrations")


# ===================================
# Pagination Schemas
# ===================================


class PaginationMeta(BaseModel):
    """Pagination metadata.

    Attributes:
        page: Current page number (1-indexed)
        per_page: Items per page
        total_items: Total number of items across all pages
        total_pages: Total number of pages
        has_next: Whether there is a next page
        has_prev: Whether there is a previous page
        next_cursor: Cursor for next page (cursor-based only)
        prev_cursor: Cursor for previous page (cursor-based only)

    Examples:
        >>> meta = PaginationMeta(
        ...     page=2,
        ...     per_page=50,
        ...     total_items=250,
        ...     total_pages=5,
        ...     has_next=True,
        ...     has_prev=True
        ... )
        >>> meta.page
        2
        >>> meta.total_pages
        5
    """

    page: int = Field(..., description="Current page number (1-indexed)", ge=1)
    per_page: int = Field(..., description="Items per page", ge=1)
    total_items: int = Field(..., description="Total number of items", ge=0)
    total_pages: int = Field(..., description="Total number of pages", ge=0)
    has_next: bool = Field(..., description="Whether there is a next page")
    has_prev: bool = Field(..., description="Whether there is a previous page")
    page_items: Optional[int] = Field(None, description="Actual number of items on current page (after conversion failures)", ge=0)
    next_cursor: Optional[str] = Field(None, description="Cursor for next page (cursor-based only)")
    prev_cursor: Optional[str] = Field(None, description="Cursor for previous page (cursor-based only)")


class PaginationLinks(BaseModel):
    """Pagination navigation links.

    Attributes:
        self: Current page URL
        first: First page URL
        last: Last page URL
        next: Next page URL (None if no next page)
        prev: Previous page URL (None if no previous page)

    Examples:
        >>> links = PaginationLinks(
        ...     self="/admin/tools?page=2&per_page=50",
        ...     first="/admin/tools?page=1&per_page=50",
        ...     last="/admin/tools?page=5&per_page=50",
        ...     next="/admin/tools?page=3&per_page=50",
        ...     prev="/admin/tools?page=1&per_page=50"
        ... )
        >>> links.self
        '/admin/tools?page=2&per_page=50'
    """

    self: str = Field(..., description="Current page URL")
    first: str = Field(..., description="First page URL")
    last: str = Field(..., description="Last page URL")
    next: Optional[str] = Field(None, description="Next page URL")
    prev: Optional[str] = Field(None, description="Previous page URL")


class PaginatedResponse(BaseModel):
    """Generic paginated response wrapper.

    This is a container for paginated data with metadata and navigation links.
    The actual data is stored in the 'data' field as a list of items.

    Attributes:
        data: List of items for the current page
        pagination: Pagination metadata (counts, page info)
        links: Navigation links (optional)

    Examples:
        >>> from mcpgateway.schemas import ToolRead
        >>> response = PaginatedResponse(
        ...     data=[],
        ...     pagination=PaginationMeta(
        ...         page=1, per_page=50, total_items=0,
        ...         total_pages=0, has_next=False, has_prev=False
        ...     ),
        ...     links=None
        ... )
        >>> response.pagination.page
        1
    """

    data: List[Any] = Field(..., description="List of items")
    pagination: PaginationMeta = Field(..., description="Pagination metadata")
    links: Optional[PaginationLinks] = Field(None, description="Navigation links")


# ============================================================================
# Cursor Pagination Response Schemas (for main API endpoints)
# ============================================================================


class CursorPaginatedToolsResponse(BaseModel):
    """Cursor-paginated response for tools list endpoint."""

    tools: List["ToolRead"] = Field(..., description="List of tools for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


class CursorPaginatedServersResponse(BaseModel):
    """Cursor-paginated response for servers list endpoint."""

    servers: List["ServerRead"] = Field(..., description="List of servers for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


class CursorPaginatedGatewaysResponse(BaseModel):
    """Cursor-paginated response for gateways list endpoint."""

    gateways: List["GatewayRead"] = Field(..., description="List of gateways for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


class CursorPaginatedResourcesResponse(BaseModel):
    """Cursor-paginated response for resources list endpoint."""

    resources: List["ResourceRead"] = Field(..., description="List of resources for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


class CursorPaginatedPromptsResponse(BaseModel):
    """Cursor-paginated response for prompts list endpoint."""

    prompts: List["PromptRead"] = Field(..., description="List of prompts for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


class CursorPaginatedA2AAgentsResponse(BaseModel):
    """Cursor-paginated response for A2A agents list endpoint."""

    agents: List["A2AAgentRead"] = Field(..., description="List of A2A agents for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


class CursorPaginatedTeamsResponse(BaseModel):
    """Cursor-paginated response for teams list endpoint."""

    teams: List["TeamResponse"] = Field(..., description="List of teams for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


class CursorPaginatedUsersResponse(BaseModel):
    """Cursor-paginated response for users list endpoint."""

    users: List["EmailUserResponse"] = Field(..., description="List of users for this page")
    next_cursor: Optional[str] = Field(None, alias="nextCursor", description="Cursor for the next page, null if no more pages")


# ============================================================================
# Observability Schemas (OpenTelemetry-style traces, spans, events, metrics)
# ============================================================================


class ObservabilityTraceBase(BaseModel):
    """Base schema for observability traces."""

    name: str = Field(..., description="Trace name (e.g., 'POST /tools/invoke')")
    start_time: datetime = Field(..., description="Trace start timestamp")
    end_time: Optional[datetime] = Field(None, description="Trace end timestamp")
    duration_ms: Optional[float] = Field(None, description="Total duration in milliseconds")
    status: str = Field("unset", description="Trace status (unset, ok, error)")
    status_message: Optional[str] = Field(None, description="Status message or error description")
    http_method: Optional[str] = Field(None, description="HTTP method")
    http_url: Optional[str] = Field(None, description="HTTP URL")
    http_status_code: Optional[int] = Field(None, description="HTTP status code")
    user_email: Optional[str] = Field(None, description="User email")
    user_agent: Optional[str] = Field(None, description="User agent string")
    ip_address: Optional[str] = Field(None, description="Client IP address")
    attributes: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional trace attributes")
    resource_attributes: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Resource attributes")


class ObservabilityTraceCreate(ObservabilityTraceBase):
    """Schema for creating an observability trace."""

    trace_id: Optional[str] = Field(None, description="Trace ID (generated if not provided)")


class ObservabilityTraceUpdate(BaseModel):
    """Schema for updating an observability trace."""

    end_time: Optional[datetime] = None
    duration_ms: Optional[float] = None
    status: Optional[str] = None
    status_message: Optional[str] = None
    http_status_code: Optional[int] = None
    attributes: Optional[Dict[str, Any]] = None


class ObservabilityTraceRead(ObservabilityTraceBase):
    """Schema for reading an observability trace."""

    trace_id: str = Field(..., description="Trace ID")
    created_at: datetime = Field(..., description="Creation timestamp")

    model_config = {"from_attributes": True}


class ObservabilitySpanBase(BaseModel):
    """Base schema for observability spans."""

    trace_id: str = Field(..., description="Parent trace ID")
    parent_span_id: Optional[str] = Field(None, description="Parent span ID (for nested spans)")
    name: str = Field(..., description="Span name (e.g., 'database_query', 'tool_invocation')")
    kind: str = Field("internal", description="Span kind (internal, server, client, producer, consumer)")
    start_time: datetime = Field(..., description="Span start timestamp")
    end_time: Optional[datetime] = Field(None, description="Span end timestamp")
    duration_ms: Optional[float] = Field(None, description="Span duration in milliseconds")
    status: str = Field("unset", description="Span status (unset, ok, error)")
    status_message: Optional[str] = Field(None, description="Status message")
    attributes: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Span attributes")
    resource_name: Optional[str] = Field(None, description="Resource name")
    resource_type: Optional[str] = Field(None, description="Resource type (tool, resource, prompt, gateway, a2a_agent)")
    resource_id: Optional[str] = Field(None, description="Resource ID")


class ObservabilitySpanCreate(ObservabilitySpanBase):
    """Schema for creating an observability span."""

    span_id: Optional[str] = Field(None, description="Span ID (generated if not provided)")


class ObservabilitySpanUpdate(BaseModel):
    """Schema for updating an observability span."""

    end_time: Optional[datetime] = None
    duration_ms: Optional[float] = None
    status: Optional[str] = None
    status_message: Optional[str] = None
    attributes: Optional[Dict[str, Any]] = None


class ObservabilitySpanRead(ObservabilitySpanBase):
    """Schema for reading an observability span."""

    span_id: str = Field(..., description="Span ID")
    created_at: datetime = Field(..., description="Creation timestamp")

    model_config = {"from_attributes": True}


class ObservabilityEventBase(BaseModel):
    """Base schema for observability events."""

    span_id: str = Field(..., description="Parent span ID")
    name: str = Field(..., description="Event name (e.g., 'exception', 'log', 'checkpoint')")
    timestamp: datetime = Field(..., description="Event timestamp")
    attributes: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Event attributes")
    severity: Optional[str] = Field(None, description="Log severity (debug, info, warning, error, critical)")
    message: Optional[str] = Field(None, description="Event message")
    exception_type: Optional[str] = Field(None, description="Exception class name")
    exception_message: Optional[str] = Field(None, description="Exception message")
    exception_stacktrace: Optional[str] = Field(None, description="Exception stacktrace")


class ObservabilityEventCreate(ObservabilityEventBase):
    """Schema for creating an observability event."""


class ObservabilityEventRead(ObservabilityEventBase):
    """Schema for reading an observability event."""

    id: int = Field(..., description="Event ID")
    created_at: datetime = Field(..., description="Creation timestamp")

    model_config = {"from_attributes": True}


class ObservabilityMetricBase(BaseModel):
    """Base schema for observability metrics."""

    name: str = Field(..., description="Metric name (e.g., 'http.request.duration', 'tool.invocation.count')")
    metric_type: str = Field(..., description="Metric type (counter, gauge, histogram)")
    value: float = Field(..., description="Metric value")
    timestamp: datetime = Field(..., description="Metric timestamp")
    unit: Optional[str] = Field(None, description="Metric unit (ms, count, bytes, etc.)")
    attributes: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Metric attributes/labels")
    resource_type: Optional[str] = Field(None, description="Resource type")
    resource_id: Optional[str] = Field(None, description="Resource ID")
    trace_id: Optional[str] = Field(None, description="Associated trace ID")


class ObservabilityMetricCreate(ObservabilityMetricBase):
    """Schema for creating an observability metric."""


class ObservabilityMetricRead(ObservabilityMetricBase):
    """Schema for reading an observability metric."""

    id: int = Field(..., description="Metric ID")
    created_at: datetime = Field(..., description="Creation timestamp")

    model_config = {"from_attributes": True}


class ObservabilityTraceWithSpans(ObservabilityTraceRead):
    """Schema for reading a trace with its spans."""

    spans: List[ObservabilitySpanRead] = Field(default_factory=list, description="List of spans in this trace")


class ObservabilitySpanWithEvents(ObservabilitySpanRead):
    """Schema for reading a span with its events."""

    events: List[ObservabilityEventRead] = Field(default_factory=list, description="List of events in this span")


# --- Performance Monitoring Schemas ---


class WorkerMetrics(BaseModel):
    """Metrics for a single worker process."""

    pid: int = Field(..., description="Process ID")
    cpu_percent: float = Field(..., description="CPU utilization percentage")
    memory_rss_mb: float = Field(..., description="Resident Set Size memory in MB")
    memory_vms_mb: float = Field(..., description="Virtual Memory Size in MB")
    threads: int = Field(..., description="Number of threads")
    connections: int = Field(0, description="Number of network connections")
    open_fds: Optional[int] = Field(None, description="Number of open file descriptors")
    status: str = Field("running", description="Worker status")
    create_time: Optional[datetime] = Field(None, description="Worker start time")
    uptime_seconds: Optional[int] = Field(None, description="Worker uptime in seconds")


class SystemMetricsSchema(BaseModel):
    """System-wide resource metrics."""

    # CPU metrics
    cpu_percent: float = Field(..., description="Total CPU utilization percentage")
    cpu_count: int = Field(..., description="Number of logical CPU cores")
    cpu_freq_mhz: Optional[float] = Field(None, description="Current CPU frequency in MHz")
    load_avg_1m: Optional[float] = Field(None, description="1-minute load average")
    load_avg_5m: Optional[float] = Field(None, description="5-minute load average")
    load_avg_15m: Optional[float] = Field(None, description="15-minute load average")

    # Memory metrics
    memory_total_mb: int = Field(..., description="Total physical memory in MB")
    memory_used_mb: int = Field(..., description="Used physical memory in MB")
    memory_available_mb: int = Field(..., description="Available memory in MB")
    memory_percent: float = Field(..., description="Memory utilization percentage")
    swap_total_mb: int = Field(0, description="Total swap space in MB")
    swap_used_mb: int = Field(0, description="Used swap space in MB")

    # Disk metrics
    disk_total_gb: float = Field(..., description="Total disk space in GB")
    disk_used_gb: float = Field(..., description="Used disk space in GB")
    disk_percent: float = Field(..., description="Disk utilization percentage")

    # Network metrics
    network_bytes_sent: int = Field(0, description="Total network bytes sent")
    network_bytes_recv: int = Field(0, description="Total network bytes received")
    network_connections: int = Field(0, description="Active network connections")

    # Process info
    boot_time: Optional[datetime] = Field(None, description="System boot time")


class RequestMetricsSchema(BaseModel):
    """HTTP request performance metrics."""

    requests_total: int = Field(0, description="Total HTTP requests")
    requests_per_second: float = Field(0, description="Current request rate")
    requests_1xx: int = Field(0, description="1xx informational responses")
    requests_2xx: int = Field(0, description="2xx success responses")
    requests_3xx: int = Field(0, description="3xx redirect responses")
    requests_4xx: int = Field(0, description="4xx client error responses")
    requests_5xx: int = Field(0, description="5xx server error responses")

    # Response time percentiles
    response_time_avg_ms: float = Field(0, description="Average response time in ms")
    response_time_p50_ms: float = Field(0, description="50th percentile response time")
    response_time_p95_ms: float = Field(0, description="95th percentile response time")
    response_time_p99_ms: float = Field(0, description="99th percentile response time")

    # Error rate
    error_rate: float = Field(0, description="Percentage of 4xx/5xx responses")

    # Active requests
    active_requests: int = Field(0, description="Currently processing requests")


class DatabaseMetricsSchema(BaseModel):
    """Database connection pool metrics."""

    pool_size: int = Field(0, description="Connection pool size")
    connections_in_use: int = Field(0, description="Active connections")
    connections_available: int = Field(0, description="Available connections")
    overflow: int = Field(0, description="Overflow connections")
    query_count: int = Field(0, description="Total queries executed")
    query_avg_time_ms: float = Field(0, description="Average query time in ms")


class CacheMetricsSchema(BaseModel):
    """Redis cache metrics."""

    connected: bool = Field(False, description="Redis connection status")
    version: Optional[str] = Field(None, description="Redis version")
    used_memory_mb: float = Field(0, description="Redis memory usage in MB")
    connected_clients: int = Field(0, description="Connected Redis clients")
    ops_per_second: int = Field(0, description="Redis operations per second")
    hit_rate: float = Field(0, description="Cache hit rate percentage")
    keyspace_hits: int = Field(0, description="Successful key lookups")
    keyspace_misses: int = Field(0, description="Failed key lookups")


class HealthStatusItem(BaseModel):
    """Individual health status item for a service component."""

    name: str = Field(..., description="Component name (e.g., 'Database', 'Cache')")
    status_code: int = Field(..., description="HTTP status code (200 for healthy, 503 for unhealthy)")
    message: str = Field(..., description="Status message describing the component state")


class HealthCheckResponse(BaseModel):
    """Health check response containing status of all monitored components."""

    status: str = Field(..., description="Overall health status: 'healthy' if all components are healthy, 'unhealthy' otherwise")
    status_items: List[HealthStatusItem] = Field(..., description="List of component health statuses")
    mcp_runtime: Dict[str, Any] = Field(default_factory=dict, description="MCP runtime diagnostics and configuration")


class GunicornMetricsSchema(BaseModel):
    """Gunicorn server metrics."""

    master_pid: Optional[int] = Field(None, description="Master process PID")
    workers_total: int = Field(0, description="Total configured workers")
    workers_active: int = Field(0, description="Currently active workers")
    workers_idle: int = Field(0, description="Idle workers")
    max_requests: int = Field(0, description="Max requests before worker restart")


class PerformanceSnapshotCreate(BaseModel):
    """Schema for creating a performance snapshot."""

    host: str = Field(..., description="Hostname")
    worker_id: Optional[str] = Field(None, description="Worker identifier")
    metrics_json: Dict[str, Any] = Field(..., description="Serialized metrics data")


class PerformanceSnapshotRead(BaseModel):
    """Schema for reading a performance snapshot."""

    id: int = Field(..., description="Snapshot ID")
    timestamp: datetime = Field(..., description="Snapshot timestamp")
    host: str = Field(..., description="Hostname")
    worker_id: Optional[str] = Field(None, description="Worker identifier")
    metrics_json: Dict[str, Any] = Field(..., description="Serialized metrics data")
    created_at: datetime = Field(..., description="Creation timestamp")

    model_config = {"from_attributes": True}


class PerformanceAggregateBase(BaseModel):
    """Base schema for performance aggregates."""

    period_start: datetime = Field(..., description="Start of aggregation period")
    period_end: datetime = Field(..., description="End of aggregation period")
    period_type: str = Field(..., description="Aggregation type (hourly, daily)")
    host: Optional[str] = Field(None, description="Host (None for cluster-wide)")

    # Request aggregates
    requests_total: int = Field(0, description="Total requests in period")
    requests_2xx: int = Field(0, description="2xx responses in period")
    requests_4xx: int = Field(0, description="4xx responses in period")
    requests_5xx: int = Field(0, description="5xx responses in period")
    avg_response_time_ms: float = Field(0, description="Average response time")
    p95_response_time_ms: float = Field(0, description="95th percentile response time")
    peak_requests_per_second: float = Field(0, description="Peak request rate")

    # Resource aggregates
    avg_cpu_percent: float = Field(0, description="Average CPU utilization")
    avg_memory_percent: float = Field(0, description="Average memory utilization")
    peak_cpu_percent: float = Field(0, description="Peak CPU utilization")
    peak_memory_percent: float = Field(0, description="Peak memory utilization")


class PerformanceAggregateCreate(PerformanceAggregateBase):
    """Schema for creating a performance aggregate."""


class PerformanceAggregateRead(PerformanceAggregateBase):
    """Schema for reading a performance aggregate."""

    id: int = Field(..., description="Aggregate ID")
    created_at: datetime = Field(..., description="Creation timestamp")

    model_config = {"from_attributes": True}


class PerformanceDashboard(BaseModel):
    """Complete performance dashboard data."""

    timestamp: datetime = Field(..., description="Dashboard generation timestamp")
    uptime_seconds: int = Field(0, description="Application uptime in seconds")
    host: str = Field(..., description="Current hostname")

    # Current metrics
    system: SystemMetricsSchema = Field(..., description="Current system metrics")
    requests: RequestMetricsSchema = Field(..., description="Current request metrics")
    database: DatabaseMetricsSchema = Field(..., description="Current database metrics")
    cache: CacheMetricsSchema = Field(..., description="Current cache metrics")
    gunicorn: GunicornMetricsSchema = Field(..., description="Current Gunicorn metrics")
    workers: List[WorkerMetrics] = Field(default_factory=list, description="Per-worker metrics")

    # Cluster info (for distributed mode)
    cluster_hosts: List[str] = Field(default_factory=list, description="Known cluster hosts")
    is_distributed: bool = Field(False, description="Running in distributed mode")


class PerformanceHistoryResponse(BaseModel):
    """Response for historical performance data."""

    aggregates: List[PerformanceAggregateRead] = Field(default_factory=list, description="Historical aggregates")
    period_type: str = Field(..., description="Aggregation period type")
    total_count: int = Field(0, description="Total matching records")


# ---------------------------------------------------------------------------
# Tool Plugin Binding Schemas
# ---------------------------------------------------------------------------


class PluginBindingMode(str, Enum):
    """Plugin execution mode for tool plugin bindings."""

    ENFORCE = "enforce"
    ENFORCE_IGNORE_ERROR = "enforce_ignore_error"  # Deprecated: use SEQUENTIAL + on_error=ignore
    PERMISSIVE = "permissive"
    DISABLED = "disabled"
    SEQUENTIAL = "sequential"
    CONCURRENT = "concurrent"
    TRANSFORM = "transform"
    AUDIT = "audit"
    FIRE_AND_FORGET = "fire_and_forget"


# --- Policy item (one plugin, one or more tools) ---


class PluginPolicyItem(BaseModel):
    """A single plugin policy entry within a team's binding payload.

    Attributes:
        tool_names: List of tool names this policy applies to. Use ``["*"]`` for all tools.
        plugin_id: The plugin to bind.
        mode: Execution mode.
        priority: Execution order — lower numbers run first.
        config: Plugin-specific configuration.
    """

    model_config = ConfigDict(extra="forbid")

    tool_names: List[str] = Field(..., min_length=1, description="Tool names to apply the policy to; use ['*'] for all tools in the team")
    plugin_id: str = Field(..., description="Plugin class name to bind, e.g. 'OutputLengthGuardPlugin'")
    mode: PluginBindingMode = Field(
        PluginBindingMode.ENFORCE, description="Execution mode: enforce, enforce_ignore_error, permissive, disabled, sequential, concurrent, transform, audit, or fire_and_forget"
    )
    priority: int = Field(50, ge=1, le=1000, description="Execution priority; lower numbers run first")
    config: Dict[str, Any] = Field(
        ...,
        description="Plugin-specific configuration. On upsert the entire config is fully replaced; there is no merge with the previously stored config.",
    )
    on_error: Optional[Literal["fail", "ignore", "disable"]] = Field(None, description="Error handling: fail (block on error), ignore (swallow errors), disable (disable plugin on error)")
    binding_reference_id: Optional[str] = Field(
        None,
        max_length=255,
        pattern=r"^[a-zA-Z0-9][a-zA-Z0-9_.-]*$",
        description="Optional external reference ID for correlating this binding with an upstream system",
    )


# --- Per-team policies wrapper ---


class TeamPolicies(BaseModel):
    """Policies for a single team."""

    model_config = ConfigDict(extra="forbid")

    policies: List[PluginPolicyItem] = Field(..., min_length=1, description="List of plugin policies for this team")


# --- Top-level request body ---


class ToolPluginBindingRequest(BaseModel):
    """Request body for POST /v1/tools/plugin_bindings.

    The payload is a dict keyed by team_id, each value being a ``TeamPolicies``
    object.  Multiple teams can be configured in a single request.  If a
    (team_id, tool_name, plugin_id) triple already exists the row is updated
    in place (upsert); otherwise a new row is inserted.

    Example::

        {
            "team_abc": {
                "policies": [
                    {
                        "tool_names": ["tool_a", "tool_b"],
                        "plugin_id": "OUTPUT_LENGTH_GUARD",
                        "mode": "enforce",
                        "priority": 10,
                        "config": {"max_chars": 2000, "strategy": "truncate"}
                    }
                ]
            }
        }
    """

    teams: Dict[str, TeamPolicies] = Field(..., min_length=1, description="Map of team_id to its plugin policies")


# --- Response schemas ---


class ToolPluginBindingResponse(BaseModelWithConfigDict):
    """A single tool plugin binding record returned from the API.

    Attributes:
        id: Unique binding identifier (UUID).
        team_id: Team the binding belongs to.
        tool_name: Tool name the policy applies to.
        plugin_id: Plugin identifier.
        mode: Execution mode.
        priority: Execution priority.
        config: Plugin-specific configuration.
        created_at: Creation timestamp.
        created_by: Email of creator.
        updated_at: Last update timestamp.
        updated_by: Email of last updater.
    """

    id: str = Field(..., description="Unique binding identifier")
    team_id: str = Field(..., description="Team the binding belongs to")
    tool_name: str = Field(..., description="Tool name the policy applies to")
    plugin_id: str = Field(..., description="Plugin identifier")
    mode: str = Field(..., description="Execution mode")
    priority: int = Field(..., description="Execution priority")
    config: Dict[str, Any] = Field(..., description="Plugin-specific configuration")
    on_error: Optional[str] = Field(None, description="Error handling policy")
    binding_reference_id: Optional[str] = Field(None, description="Optional external reference ID for correlating with an upstream system")
    created_at: datetime = Field(..., description="Creation timestamp")
    created_by: str = Field(..., description="Email of creator")
    updated_at: datetime = Field(..., description="Last update timestamp")
    updated_by: str = Field(..., description="Email of last updater")

    @field_serializer("created_at", "updated_at")
    def serialize_dt(self, v: datetime) -> str:
        """Serialize datetime fields to ISO 8601.

        Args:
            v: Datetime to serialize.

        Returns:
            ISO 8601 string.
        """
        return encode_datetime(v)


class ToolPluginBindingListResponse(BaseModelWithConfigDict):
    """Response for GET /v1/tools/plugin_bindings[/{team_id}]."""

    bindings: List[ToolPluginBindingResponse] = Field(default_factory=list, description="List of tool plugin bindings")
    total: int = Field(0, description="Total number of bindings returned")
