"""DispatchContext — per-call state passed to dispatch().

Bundles the runtime knobs every dispatch needs: which StepRunner to bind
runners against, who's calling (caller_id), project/episode/shot identity
for receipts, and a receipts-log destination override.

Frozen dataclass. Construct once per logical entry point (per request
in a server, per CLI invocation, per test).
"""

from __future__ import annotations

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


@dataclass(frozen=True)
class DispatchContext:
    """Per-dispatch context.

    Required:
        caller_id: free-form string identifying the entry point. See
            GenerationReceipt docstring for recommended values.
        step_runner: a constructed StepRunner. dispatch() calls
            register_default_runners(step_runner) lazily on first use.

    Optional:
        project: project slug.
        episode: episode number.
        receipts_log_path: override for the JSONL audit log destination.
            None → default at $RECOIL_ROOT/_dispatch_logs/receipts.jsonl.
            "" or "DISABLED" → log writing disabled (tests).
        provenance_overrides: dict merged into receipt.provenance after
            dispatch() populates the standard keys.
    """

    caller_id: str
    step_runner: Any  # avoid circular: don't import StepRunner here
    project: Optional[str] = None
    episode: Optional[int] = None
    receipts_log_path: Optional[str] = None
    provenance_overrides: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not isinstance(self.caller_id, str) or not self.caller_id:
            raise ValueError("DispatchContext.caller_id must be a non-empty string")
        if self.step_runner is None:
            raise ValueError("DispatchContext.step_runner is required (got None)")
