"""Declarative runtime/deployment metadata.

These types describe *how* the platform should run an agent: lifecycle,
state needs, isolation level, resource budget, egress policy. They are
read by the deployer and by the registry; agent code itself should not
depend on which runtime is selected.
"""
from __future__ import annotations

from enum import Enum

from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, PositiveInt


class Lifecycle(str, Enum):
    """How long an instance of the agent process lives."""

    EPHEMERAL = "ephemeral"  # spawned per-invocation, torn down after
    SESSION = "session"  # spawned per-session, kept until the session ends
    WARM = "warm"  # long-running service, multiplexed across callers


class State(str, Enum):
    """What kind of state the agent retains between invocations."""

    NONE = "none"  # purely functional
    SESSION = "session"  # in-memory state for the lifetime of a session
    DURABLE = "durable"  # persisted across restarts (storage required)


class Sandbox(str, Enum):
    """Isolation level. The platform always runs agents under microsandbox.

    Modeled as an enum (rather than a constant) so the wire format stays
    stable if more isolation tiers are added later, but only one value is
    currently valid: every agent runs in a microvm-class sandbox.
    """

    MICROSANDBOX = "microsandbox"


class Resources(BaseModel):
    """Resource budget hint for the deployer."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    cpu: str = "100m"  # k8s-style CPU spec, e.g. "500m", "2"
    memory: str = "256Mi"  # k8s-style memory, e.g. "512Mi", "4Gi"
    gpu: NonNegativeInt = 0
    max_runtime_seconds: PositiveInt = 600


class SkillPolicy(BaseModel):
    """Per-skill operational policy advertised on the Agent Card."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    timeout_seconds: float | None = None
    idempotent: bool = False
    max_retries: NonNegativeInt = 0
    cost_class: str | None = None  # informational, e.g. "cheap" / "expensive"
    # When True, the skill is allowed to call ``ctx.request_scope(...)`` to
    # negotiate additional workspace access mid-execution. Off by default —
    # platform refuses scope negotiation otherwise.
    allow_scope_expansion: bool = False


class EgressPolicy(BaseModel):
    """What external hosts the agent is allowed to talk to."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    allow_hosts: tuple[str, ...] = ()
    allow_internal_services: tuple[str, ...] = ()  # e.g. cluster service DNS
    deny_internet_by_default: bool = True


class LLMProvisioning(str, Enum):
    """Where the agent's LLM credentials come from.

    Drives both runtime behaviour (``ctx.llm`` resolution) and what the
    marketplace can charge the caller.
    """

    PLATFORM = "platform"
    """Use the platform's shared LiteLLM gateway (free during beta;
    folded into platform pricing in prod). Authors don't need to know
    or care about keys."""

    CALLER_PROVIDED = "caller_provided"
    """Use the caller's own LLM credentials, forwarded by the CP in the
    invoke body. The author's per-call price covers the skill IP /
    compute only; the LLM bill goes to the caller's provider directly."""

    AGENT_BYOK = "agent_byok"
    """Author brings their own LLM key (set via env / Secret). The
    per-call price typically includes a markup on the LLM cost."""


class Pricing(BaseModel):
    """Per-agent pricing declaration. Surfaced on the Card; the
    marketplace uses it to display "this much per call" to potential
    callers.
    """

    model_config = ConfigDict(extra="forbid", frozen=True)

    price_per_call_usd: float = 0.0
    """Flat charge per skill invocation, in USD. ``0.0`` = free."""

    caller_pays_llm: bool = False
    """If True, the caller's LLM provider eats the inference cost
    directly (matches ``LLMProvisioning.CALLER_PROVIDED``). If False the
    author's price is expected to cover LLM cost too."""

    notes: str = ""
    """Free-form marketing text the marketplace will render next to the
    price (e.g. "first 100 calls free")."""


class AgentRuntime(BaseModel):
    """Aggregate runtime declaration; published on the Agent Card."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    lifecycle: Lifecycle = Lifecycle.EPHEMERAL
    state: State = State.NONE
    sandbox: Sandbox = Sandbox.MICROSANDBOX
    resources: Resources = Field(default_factory=Resources)
    concurrency: PositiveInt = 1
    egress: EgressPolicy = Field(default_factory=EgressPolicy)
    tools_used: tuple[str, ...] = ()
    llm_provisioning: LLMProvisioning = LLMProvisioning.PLATFORM
    pricing: Pricing = Field(default_factory=Pricing)
    wants_cp_jwt: bool = False
    """When True, the platform forwards the *caller's* control-plane JWT
    to this agent in the /invoke body so the skill can call back into
    user-scoped CP endpoints (/v1/me/files, /v1/agents/from-tarball,
    etc.) on the user's behalf. Only opt in for trusted platform agents
    — a JWT lets the holder do anything the user can do."""

    apt_packages: tuple[str, ...] = ()
    """Extra Debian packages to apt-install on top of the base image at
    build time. Use this when the agent needs system binaries the
    Python-only base doesn't ship — ffmpeg, imagemagick, poppler-utils,
    etc. Each entry must match ``[a-z0-9.+-]{2,64}`` (validated by the
    control plane); anything fancier (custom apt repos, --no-install-
    recommends overrides) belongs in a bespoke base image, not here."""
