"""CQRS command dataclasses for McpServer and Group CRUD operations.

All commands are frozen dataclasses — immutable value objects that represent
a single intent to mutate state. Handlers receive these via the command bus.
"""

from dataclasses import dataclass, field

from ...domain.policies.egress_l7 import L7Policy
from ...domain.value_objects.compat import resolve_legacy_mcp_server_id as _resolve_legacy_mcp_server_id
from ...domain.value_objects.provenance import Provenance
from .commands import Command


# =============================================================================
# McpServer CRUD Commands
# =============================================================================


@dataclass(frozen=True)
class CreateMcpServerCommand(Command):
    """Create and register a new mcp_server.

    Attributes:
        mcp_server_id: Unique identifier for the new mcp_server.
        mode: McpServer mode ("subprocess", "docker", "remote").
        command: Subprocess command list (required for subprocess mode).
        image: Docker image name (required for docker mode).
        endpoint: HTTP endpoint URL (required for remote mode).
        env: Environment variables to pass to the mcp_server.
        idle_ttl_s: Idle TTL in seconds before auto-shutdown.
        health_check_interval_s: Health check interval in seconds.
        description: Human-readable description / preprompt.
        volumes: Docker volume mounts. Present because a discovered container
            can carry them and the aggregate has always accepted them; without
            it, routing discovery through this command would drop them silently.
        read_only: Mount the container root read-only. Same reasoning as
            volumes -- and losing this one would silently relax a container's
            hardening, which is the worse direction to drop a field in. The
            default mirrors the aggregate's, which is True: a caller that says
            nothing gets the hardened container, not the permissive one.
        source: Who is registering this mcp_server ("api", "config",
            "discovery:docker"). A label for an operator to read. **Not** a
            security input: it is free text and some routes forward it, so a
            policy that branched on it would be settable by whoever it is meant
            to constrain.
        provenance: How this registration reached the bus, as a type rather than
            a string. This is what SSRF policy branches on. It defaults to HUMAN
            so a caller that says nothing gets the strict rules -- a new call
            site cannot relax a security check by forgetting an argument -- and
            the REST route never passes it, so it cannot be set from a request
            body.
        runtime_addresses: For DISCOVERY, the addresses the container runtime
            reported for this container or pod. The endpoint must resolve to one
            of them. Without this, DISCOVERY buys nothing: provenance grants a
            *specific address*, never an address class, or a container that
            labels itself with a neighbour's address launders its way there.
    """

    mcp_server_id: str
    mode: str
    command: list[str] | None = None
    image: str | None = None
    endpoint: str | None = None
    env: dict[str, str] = field(default_factory=dict)
    idle_ttl_s: int = 300
    health_check_interval_s: int = 60
    description: str | None = None
    volumes: list[str] | None = None
    read_only: bool = True
    source: str = "api"
    provenance: Provenance = Provenance.HUMAN
    runtime_addresses: frozenset[str] | None = None


@dataclass(frozen=True)
class UpdateMcpServerCommand(Command):
    """Update mutable configuration fields on an existing mcp_server.

    Only non-None fields are applied. Fields not specified are unchanged.

    Attributes:
        mcp_server_id: Identifier of the mcp_server to update.
        description: New human-readable description (optional).
        env: New environment variable dict, replaces existing (optional).
        idle_ttl_s: New idle TTL in seconds (optional).
        health_check_interval_s: New health check interval in seconds (optional).
        source: Who is updating this mcp_server ("api", "config").
    """

    mcp_server_id: str
    description: str | None = None
    env: dict[str, str] | None = None
    idle_ttl_s: int | None = None
    health_check_interval_s: int | None = None
    source: str = "api"


@dataclass(frozen=True)
class SetL7PolicyCommand(Command):
    """Attach, replace, or clear the L7 egress policy on an existing mcp_server.

    Delivered by the operator when it compiles an MCPEgressPolicy. A None policy
    clears enforcement.

    Attributes:
        mcp_server_id: Identifier of the mcp_server to govern.
        policy: The compiled L7 policy, or None to clear it.
        source: Who set this policy ("api", "operator").
    """

    mcp_server_id: str
    policy: L7Policy | None = None
    source: str = "api"


@dataclass(frozen=True, init=False)
class DeleteMcpServerCommand(Command):
    """Delete a mcp_server, stopping it first if it is running.

    Attributes:
        mcp_server_id: Identifier of the mcp_server to delete.
        source: Who is deleting this mcp_server ("api", "config").
        provenance: How this deletion reached the bus, as a type rather than a
            string. DISCOVERY means a convergence loop decided the server is
            gone -- which is the deletion that has to be fenced, because it can
            be issued by an instance that stalled long enough to lose the
            management lease and has not noticed yet. HUMAN means an operator
            asked, and an operator asking is not a stale loop finishing: their
            deletion goes through wherever it lands. Set by the construction
            path, never by a request, for the same reason as on registration.
    """

    mcp_server_id: str
    source: str = "api"
    provenance: Provenance = Provenance.HUMAN

    def __init__(
        self,
        mcp_server_id: str | None = None,
        source: str = "api",
        provenance: Provenance = Provenance.HUMAN,
        **kwargs: object,
    ):
        object.__setattr__(self, "mcp_server_id", _resolve_legacy_mcp_server_id(mcp_server_id, kwargs))
        object.__setattr__(self, "source", source)
        object.__setattr__(self, "provenance", provenance)
        if kwargs:
            unexpected = ", ".join(sorted(kwargs))
            raise TypeError(f"Unexpected keyword argument(s): {unexpected}")

    @property
    def provider_id(self) -> str:
        return self.mcp_server_id


# =============================================================================
# Group CRUD Commands
# =============================================================================


@dataclass(frozen=True)
class CreateGroupCommand(Command):
    """Create a new mcp_server group.

    Attributes:
        group_id: Unique identifier for the new group.
        strategy: Load balancing strategy ("round_robin", "least_connections", "random").
        min_healthy: Minimum healthy members for HEALTHY group state.
        description: Human-readable description (optional).
        source: Who is creating this group ("api", "config").
    """

    group_id: str
    strategy: str = "round_robin"
    min_healthy: int = 1
    description: str | None = None
    source: str = "api"


@dataclass(frozen=True)
class UpdateGroupCommand(Command):
    """Update mutable configuration fields on an existing group.

    Only non-None fields are applied. Fields not specified are unchanged.

    Attributes:
        group_id: Identifier of the group to update.
        strategy: New load balancing strategy (optional).
        description: New human-readable description (optional).
        min_healthy: New minimum healthy member count (optional).
        source: Who is updating this group ("api", "config").
    """

    group_id: str
    strategy: str | None = None
    description: str | None = None
    min_healthy: int | None = None
    source: str = "api"


@dataclass(frozen=True)
class DeleteGroupCommand(Command):
    """Delete a group, stopping all members first.

    Attributes:
        group_id: Identifier of the group to delete.
        source: Who is deleting this group ("api", "config").
    """

    group_id: str
    source: str = "api"


@dataclass(frozen=True, init=False)
class AddGroupMemberCommand(Command):
    """Add a mcp_server to an existing group.

    Attributes:
        group_id: Identifier of the group to add the member to.
        mcp_server_id: Identifier of the mcp_server to add.
        weight: Load balancing weight (higher = more traffic).
        priority: Member priority (lower = higher priority).
    """

    group_id: str
    mcp_server_id: str
    weight: int = 1
    priority: int = 1

    def __init__(
        self,
        group_id: str,
        mcp_server_id: str | None = None,
        weight: int = 1,
        priority: int = 1,
        **kwargs: object,
    ):
        object.__setattr__(self, "group_id", group_id)
        object.__setattr__(self, "mcp_server_id", _resolve_legacy_mcp_server_id(mcp_server_id, kwargs))
        object.__setattr__(self, "weight", weight)
        object.__setattr__(self, "priority", priority)
        if kwargs:
            unexpected = ", ".join(sorted(kwargs))
            raise TypeError(f"Unexpected keyword argument(s): {unexpected}")

    @property
    def provider_id(self) -> str:
        return self.mcp_server_id


@dataclass(frozen=True, init=False)
class RemoveGroupMemberCommand(Command):
    """Remove a mcp_server from a group.

    Attributes:
        group_id: Identifier of the group to remove the member from.
        mcp_server_id: Identifier of the mcp_server to remove.
    """

    group_id: str
    mcp_server_id: str

    def __init__(self, group_id: str, mcp_server_id: str | None = None, **kwargs: object):
        object.__setattr__(self, "group_id", group_id)
        object.__setattr__(self, "mcp_server_id", _resolve_legacy_mcp_server_id(mcp_server_id, kwargs))
        if kwargs:
            unexpected = ", ".join(sorted(kwargs))
            raise TypeError(f"Unexpected keyword argument(s): {unexpected}")

    @property
    def provider_id(self) -> str:
        return self.mcp_server_id


# legacy aliases
globals().update(
    {
        "".join(("CreatePro", "viderCommand")): CreateMcpServerCommand,
        "".join(("UpdatePro", "viderCommand")): UpdateMcpServerCommand,
        "".join(("DeletePro", "viderCommand")): DeleteMcpServerCommand,
    }
)
