"""The author-facing `ctx` object handed to scan(inputs, ctx).

`ScanContext` is the second argument to the author's `scan(inputs, ctx)`. It is
two tiers (design §2.1):

  Capabilities you CALL:
    state       — the StateManager (B2): a disk-persisted, bounded, transactional
                  series. Passed in AS-IS (never wrapped/copied) so author mutations
                  and the scaffold's commit/rollback hit the SAME object. May be None
                  when history is disabled (state_history_max_count 0/unset).
    senpi_mcp   — the MCP client capability. PRESENT but EMPTY this block (None);
                  B5 fills it. The slot exists now so B5 is purely additive — no
                  structural change to ctx later.

  Read-only metadata you READ:
    wallet            — str, the strategy wallet (routing identity).
    scanner_name      — str, this scanner's recipe name.
    interval_seconds  — int, tick cadence in seconds.
    dry_run           — bool, True only during a validation run (see __init__).

The metadata is FROZEN: mutating, deleting, or adding any attribute raises. The
author can read `ctx.wallet` but cannot rebind it (or scribble a backdoor onto
ctx). `state` and `senpi_mcp` are likewise immutable bindings on ctx — the author
uses them through their own APIs, never by reassigning the slot.
"""

from __future__ import annotations

from typing import Any, Optional


class ScanContext:
    """Frozen, read-only metadata + capability handles for scan(inputs, ctx).

    Construction seam the scaffold uses:

        ScanContext(
            *,
            state,              # a StateManager instance (or None when history disabled)
            wallet: str,
            scanner_name: str,
            interval_seconds: int,
            senpi_mcp=None,     # B5 fills this; None/absent this block
        )
    """

    # Lock the attribute set: no __dict__, so any unknown attribute (mutate / add /
    # delete) raises AttributeError. The five documented slots are the only ones.
    __slots__ = (
        "state",
        "senpi_mcp",
        "wallet",
        "scanner_name",
        "interval_seconds",
        "dry_run",
    )

    def __init__(
        self,
        *,
        state: Any,
        wallet: str,
        scanner_name: str,
        interval_seconds: int,
        senpi_mcp: Optional[Any] = None,
        dry_run: bool = False,
    ) -> None:
        # Bypass our own frozen __setattr__ to populate the slots once at
        # construction. object.__setattr__ writes the slot directly.
        object.__setattr__(self, "state", state)
        object.__setattr__(self, "senpi_mcp", senpi_mcp)
        object.__setattr__(self, "wallet", wallet)
        object.__setattr__(self, "scanner_name", scanner_name)
        object.__setattr__(self, "interval_seconds", interval_seconds)
        # False in production, True when a validation run forces a single tick. A scanner that
        # returns early outside its trading window can consult this to run anyway, so validation
        # gets to see it do real work instead of watching it decline to start. Ignoring it is
        # safe — a tick that reads nothing is reported as unproven rather than passing.
        object.__setattr__(self, "dry_run", dry_run)

    # -- frozen: block every post-construction mutation --------------------

    def __setattr__(self, name: str, value: Any) -> None:
        raise AttributeError(
            f"ctx is read-only: cannot set {name!r}. The metadata "
            "(wallet/scanner_name/interval_seconds) is frozen, and the "
            "capability slots (state/senpi_mcp) are used through their own APIs, "
            "never reassigned."
        )

    def __delattr__(self, name: str) -> None:
        raise AttributeError(f"ctx is read-only: cannot delete {name!r}.")
