from __future__ import annotations

from dataclasses import dataclass, replace
from typing import Any, Iterable


_UNKNOWN_POLICY_MESSAGE = "unknown policy '{name}'; expected one of: {choices}"


@dataclass(frozen=True)
class ExecutionPolicy:
    name: str
    allowed_commands: frozenset[str]
    blocked_patterns: tuple[str, ...]
    allow_network: bool = True
    allow_delete: bool = True
    allowed_domains: frozenset[str] | None = None
    max_file_bytes: int = 5 * 1024 * 1024
    allowed_capabilities: frozenset[str] | None = None

    def with_overrides(
        self,
        *,
        allowed_commands: set[str] | None = None,
        blocked_patterns: tuple[str, ...] | None = None,
        allowed_domains: Iterable[str] | None = None,
        max_file_bytes: int | None = None,
        allowed_capabilities: Iterable[str] | None = None,
    ) -> "ExecutionPolicy":
        return replace(
            self,
            allowed_commands=frozenset(allowed_commands) if allowed_commands is not None else self.allowed_commands,
            blocked_patterns=tuple(blocked_patterns) if blocked_patterns is not None else self.blocked_patterns,
            allowed_domains=(frozenset(allowed_domains) if allowed_domains is not None else self.allowed_domains),
            max_file_bytes=max_file_bytes if max_file_bytes is not None else self.max_file_bytes,
            allowed_capabilities=(frozenset(allowed_capabilities) if allowed_capabilities is not None else self.allowed_capabilities),
        )

    @staticmethod
    def from_dict(data: dict[str, Any]) -> "ExecutionPolicy":
        return ExecutionPolicy(
            name=str(data.get("name", "custom")),
            allowed_commands=frozenset(data.get("allowed_commands", ())),
            blocked_patterns=tuple(data.get("blocked_patterns", ())),
            allow_network=bool(data.get("allow_network", True)),
            allow_delete=bool(data.get("allow_delete", True)),
            allowed_domains=frozenset(data["allowed_domains"]) if "allowed_domains" in data else None,
            max_file_bytes=int(data.get("max_file_bytes", 5 * 1024 * 1024)),
            allowed_capabilities=frozenset(data["allowed_capabilities"]) if "allowed_capabilities" in data else None,
        )


POLICIES: dict[str, ExecutionPolicy] = {
    "standard": ExecutionPolicy(
        name="standard",
        allowed_commands=frozenset({"ls"}),
        blocked_patterns=("rm", "sudo", "&&", ";", "||", "|", "`"),
        allow_network=True,
        allow_delete=True,
    ),
    "strict": ExecutionPolicy(
        name="strict",
        allowed_commands=frozenset({"ls"}),
        blocked_patterns=("rm", "sudo", "&&", ";", "||", "|", "`"),
        allow_network=False,
        allow_delete=False,
        allowed_capabilities=frozenset({"filesystem", "memory", "utility", "data"}),
    ),
    "dev": ExecutionPolicy(
        name="dev",
        allowed_commands=frozenset({"python", "node", "ls"}),
        blocked_patterns=("rm", "sudo", "&&", ";"),
        allow_network=True,
        allow_delete=True,
    ),
}


def build_policy(
    policy: str | ExecutionPolicy = "standard",
    *,
    allowed_commands: set[str] | None = None,
    blocked_patterns: tuple[str, ...] | None = None,
    allowed_domains: Iterable[str] | None = None,
    max_file_bytes: int | None = None,
    allowed_capabilities: Iterable[str] | None = None,
) -> ExecutionPolicy:
    if isinstance(policy, ExecutionPolicy):
        base = policy
    else:
        try:
            base = POLICIES[policy]
        except KeyError as error:
            choices = ", ".join(sorted(POLICIES))
            raise ValueError(_UNKNOWN_POLICY_MESSAGE.format(name=policy, choices=choices)) from error
    return base.with_overrides(
        allowed_commands=allowed_commands,
        blocked_patterns=blocked_patterns,
        allowed_domains=allowed_domains,
        max_file_bytes=max_file_bytes,
        allowed_capabilities=allowed_capabilities,
    )