"""Policy schema — Pydantic models for agentsh policy definitions.

See spec Section 7.1 for the full model hierarchy.
"""

from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, field_validator

# FileOp is a plain string (not a Literal enum) — agentsh accepts
# extensible operation names like 'open', 'stat', 'list', etc.
FileOp = str

# All policy models use extra="forbid" to catch typos in policy keys.

# ─── File rules ─────────────────────────────────────────


class FileAllowRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    allow: str | list[str]
    ops: list[str] | None = None


class FileDenyRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    deny: str | list[str]
    ops: list[str] | None = None


class FileRedirectRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    redirect: str | list[str]
    to: str
    ops: list[str] | None = None


class FileAuditRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    audit: str | list[str]
    ops: list[str] | None = None


class FileSoftDeleteRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    soft_delete: str | list[str]


FileRule = FileAllowRule | FileDenyRule | FileRedirectRule | FileAuditRule | FileSoftDeleteRule

# ─── Network rules ──────────────────────────────────────


class NetworkAllowRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    allow: str | list[str]
    ports: list[int] | None = None

    @field_validator("ports", mode="before")
    @classmethod
    def _validate_ports(cls, v: list[int] | None) -> list[int] | None:
        if v is not None:
            for p in v:
                if not isinstance(p, int):
                    raise ValueError(f"Port must be an integer, got {type(p).__name__}: {p}")
                if not (1 <= p <= 65535):
                    raise ValueError(f"Port {p} out of range 1-65535")
        return v


class NetworkDenyRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    deny: str | list[str]


class NetworkRedirectRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    redirect: str
    to: str


class NetworkAllowCidrsRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    allow_cidrs: list[str]
    ports: list[int] | None = None

    @field_validator("ports", mode="before")
    @classmethod
    def _validate_ports(cls, v: list[int] | None) -> list[int] | None:
        if v is not None:
            for p in v:
                if not isinstance(p, int):
                    raise ValueError(f"Port must be an integer, got {type(p).__name__}: {p}")
                if not (1 <= p <= 65535):
                    raise ValueError(f"Port {p} out of range 1-65535")
        return v


class NetworkDenyCidrsRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    deny_cidrs: list[str]


NetworkRule = (
    NetworkAllowRule
    | NetworkDenyRule
    | NetworkRedirectRule
    | NetworkAllowCidrsRule
    | NetworkDenyCidrsRule
)

# ─── Command rules ──────────────────────────────────────


class CommandRedirectTarget(BaseModel):
    model_config = ConfigDict(extra="forbid")
    cmd: str
    args: list[str]


class CommandAllowRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    allow: str | list[str]


class CommandDenyRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    deny: str | list[str]


class CommandRedirectRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    redirect: str | list[str]
    to: str | CommandRedirectTarget


CommandRule = CommandAllowRule | CommandDenyRule | CommandRedirectRule

# ─── Env rules ──────────────────────────────────────────


class EnvRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    commands: list[str]
    allow: list[str] | None = None
    deny: list[str] | None = None


# ─── DNS / Connect redirects ────────────────────────────


class DnsRedirect(BaseModel):
    model_config = ConfigDict(extra="forbid")
    match: str
    resolve_to: str


class ConnectRedirect(BaseModel):
    model_config = ConfigDict(extra="forbid")
    match: str
    redirect_to: str


# ─── Package rules ─────────────────────────────────────


class LicenseSpdxMatch(BaseModel):
    model_config = ConfigDict(extra="forbid")
    allow: list[str] | None = None
    deny: list[str] | None = None


class PackageMatch(BaseModel):
    model_config = ConfigDict(extra="forbid")
    packages: list[str] | None = None
    name_patterns: list[str] | None = None
    finding_type: str | None = None
    severity: str | list[str] | None = None
    reasons: list[str] | None = None
    license_spdx: LicenseSpdxMatch | None = None
    ecosystem: str | None = None
    options: dict[str, Any] | None = None


class PackageRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    match: PackageMatch
    action: Literal["allow", "warn", "approve", "block"]
    reason: str | None = None


# ─── Env policy (top-level, distinct from per-command env rules) ─


class EnvPolicy(BaseModel):
    model_config = ConfigDict(extra="forbid")
    allow: list[str] | None = None
    deny: list[str] | None = None
    max_bytes: int | None = None
    max_keys: int | None = None
    block_iteration: bool | None = None


# ─── Signal rules ──────────────────────────────────────


class SignalTarget(BaseModel):
    model_config = ConfigDict(extra="forbid")
    type: Literal["self", "children", "session", "parent", "external", "system"]
    pattern: str | None = None


class SignalRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    name: str
    signals: list[str]
    target: SignalTarget
    decision: Literal["allow", "deny", "audit"]
    fallback: str | None = None
    message: str | None = None


# ─── Unix socket rules ────────────────────────────────


class UnixSocketRule(BaseModel):
    model_config = ConfigDict(extra="forbid")
    name: str
    paths: list[str]
    operations: list[str] | None = None
    decision: Literal["allow", "deny"]
    message: str | None = None


# ─── Resource limits ───────────────────────────────────


class ResourceLimits(BaseModel):
    model_config = ConfigDict(extra="forbid")
    max_memory_mb: int | None = None
    cpu_quota_percent: int | None = None
    pids_max: int | None = None
    command_timeout: str | None = None
    session_timeout: str | None = None
    idle_timeout: str | None = None


# ─── Audit settings ────────────────────────────────────


class AuditSettings(BaseModel):
    model_config = ConfigDict(extra="forbid")
    log_allowed: bool | None = None
    log_denied: bool | None = None
    log_approved: bool | None = None
    include_stdout: bool | None = None
    include_stderr: bool | None = None


# ─── PolicyDefinition ───────────────────────────────────


class PolicyDefinition(BaseModel):
    model_config = ConfigDict(extra="forbid")
    file: list[FileRule] | None = None
    network: list[NetworkRule] | None = None
    commands: list[CommandRule] | None = None
    env: list[EnvRule] | None = None
    dns: list[DnsRedirect] | None = None
    connect: list[ConnectRedirect] | None = None
    package_rules: list[PackageRule] | None = None
    env_policy: EnvPolicy | None = None
    signal_rules: list[SignalRule] | None = None
    unix_socket_rules: list[UnixSocketRule] | None = None
    resource_limits: ResourceLimits | None = None
    audit_settings: AuditSettings | None = None


def validate_policy(policy: dict[str, Any] | PolicyDefinition) -> PolicyDefinition:
    """Validate and return a PolicyDefinition. Raises ValidationError."""
    if isinstance(policy, PolicyDefinition):
        return policy
    return PolicyDefinition.model_validate(policy)
