"""Actor mailbox models and scheduling placeholders.

Goal: keep this file as the single place that documents mailbox concerns so we
can evolve a minimal implementation without locking in premature decisions.
See `issues/actor-mailbox-policy-scenarios.md` for the scenario catalog.

Mailbox responsibilities to clarify
- Message unit: what is an "actor call" envelope for queuing purposes.
- Enqueue API: how callers submit, and what receipt or handle they get back.
- Dequeue API: how the executor pulls work and when it is considered "started".
- Ordering semantics: FIFO, priority, or policy-driven reordering.
- Admission gating: whether the mailbox itself refuses items or relies on policy.
- Backpressure: what happens when the queue is full or rate-limited.
- Cancellation: cancel queued items, and how to surface the result.
- Timeouts: queue wait timeouts vs execution timeouts.
- Retry and requeue: should a failed call re-enter the mailbox.
- Introspection: queue length, per-actor lag, oldest wait time.
- Shutdown: draining, rejecting, or failing pending items on actor stop.
- Concurrency model: strict serial vs controlled parallel dequeue.
- Loop interaction: runtime loop vs user loop delivery.

Typical application scenarios that drive mailbox design
- Strict serial actors with a single in-flight call.
- Actor call actor as an IO-like operation that should not block the mailbox.
- High fan-in request streams that need bounded queues and clear backpressure.
- Control-plane calls that may need priority over data-plane work.
- Long-running IO tasks inside actors (LLM calls, external APIs) where queue
  wait times must be observable to avoid invisible latency.
- In-container actor graphs that share resources and may want fairness.
- Mixed sync/async actor implementations on separate loops.
- Burst traffic where queue policies should prevent memory spikes.

Non-goals for the first minimal implementation
- Persistence or durability across process restarts.
- Cross-node queue distribution or remote mailbox sharding.
- Complex priority or preemption logic (unless required by a scenario).

Open questions for later
- Whether mailbox should expose "gate" semantics (pause dequeue) separately
  from admission control.
- Whether "cancel" should include executing calls or only queued calls.
- How mailbox should interact with future concurrency controls.

Current direction (draft, not locked)
- Use a unified message envelope for actor calls with deadline/priority fields.
- Admission rejection should surface to callers as exceptions.
- Ack is not required for internal messaging; send/receive are decoupled.
- Backpressure may propagate upstream via runtime control signals.
- Actor creation should accept a config that carries mailbox settings.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Protocol


@dataclass
class ActorCallEnvelope:
    """Minimal actor call envelope for mailbox and policy layers."""

    request_id: str
    actor_id: str
    method: str
    caller: str | None = None
    args: tuple[Any, ...] = ()
    kwargs: dict[str, Any] = field(default_factory=dict)
    trace_id: str | None = None
    deadline: float | None = None
    priority: int | None = None
    lane: str | None = None
    created_at: float | None = None
    enqueued_at: float | None = None
    metadata: dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class MailboxReceipt:
    request_id: str
    token: str | None = None


@dataclass
class MailboxStats:
    queue_len: int = 0
    inflight: int = 0
    oldest_wait_ms: float | None = None
    rejects: int = 0


class ActorCallRejected(RuntimeError):
    """Raised when a mailbox or policy rejects an actor call."""

    def __init__(self, reason: str, envelope: ActorCallEnvelope | None = None):
        super().__init__(reason)
        self.reason = reason
        self.envelope = envelope


class Mailbox(Protocol):
    def enqueue(self, envelope: ActorCallEnvelope) -> MailboxReceipt:
        ...

    def dequeue(self) -> ActorCallEnvelope | None:
        ...

    def finish(self, receipt: MailboxReceipt) -> None:
        ...

    def fail(self, receipt: MailboxReceipt, error: BaseException) -> None:
        ...

    def cancel(self, receipt: MailboxReceipt) -> bool:
        ...

    def size(self) -> int:
        ...

    def stats(self) -> MailboxStats:
        ...


# TODO: define mailbox implementations once policy decisions land.
