"""AUTO-GENERATED by scripts/generate_sync.py - DO NOT EDIT.

Source: repository.py

This module provides synchronous versions of the async classes.
Changes should be made to the source file, then regenerated.
"""

import logging
import random
import time
import warnings
from typing import TYPE_CHECKING, Any, cast

import boto3
from botocore.exceptions import ClientError
from ulid import ULID

from . import schema
from .config_cache import CacheStats as CacheStats
from .exceptions import EntityExistsError
from .limiter import OnUnavailable as OnUnavailable
from .models import (
    AuditAction,
    AuditEvent,
    BackendCapabilities,
    BucketState,
    Entity,
    Limit,
    OnUnavailableAction,
    StackOptions,
    UsageSnapshot,
    UsageSummary,
    validate_identifier,
    validate_resource,
)
from .naming import normalize_stack_name
from .sync_config_cache import ConfigSource, SyncConfigCache
from .sync_repository_protocol import SpeculativeFailureReason, SpeculativeResult

if TYPE_CHECKING:
    from .sync_repository_builder import SyncRepositoryBuilder
logger = logging.getLogger(__name__)


class SyncRepository:
    """Async DynamoDB repository for rate limiter data.

    Handles all DynamoDB operations including entities, buckets,
    limit configs, and transactions.

    Use :meth:`open` to open a repository (recommended), or :meth:`builder`
    for custom infrastructure options (permission boundaries, IAM config).

    .. deprecated::
        Direct construction via ``SyncRepository(...)`` is deprecated and will be
        removed in v1.0.0. Use ``SyncRepository.open(...)`` or
        ``SyncRepository.builder().build()`` instead.

    Args:
        name: Resource identifier (e.g., "my-app"). Used as the
            CloudFormation stack_name and DynamoDB table_name.
        region: AWS region (e.g., "us-east-1").
        endpoint_url: Custom endpoint URL (e.g., LocalStack).
        stack_options: Configuration for CloudFormation infrastructure.
            Pass StackOptions to enable declarative infrastructure management.
        config_cache_ttl: TTL in seconds for config cache (default: 60, 0 to disable).
            Controls caching of resolved limit configs in resolve_limits().

    Example::

        # Most users (auto-provisions if needed)
        repo = SyncRepository.open("my-app")

        # Custom infrastructure options
        repo = SyncRepository.builder().namespace("my-app").build()
    """

    def __init__(
        self,
        name: str,
        region: str | None = None,
        endpoint_url: str | None = None,
        stack_options: StackOptions | None = None,
        config_cache_ttl: int = 60,
        parallel_mode: str = "auto",
        *,
        _skip_deprecation_warning: bool = False,
    ) -> None:
        if not _skip_deprecation_warning:
            warnings.warn(
                "Directly calling SyncRepository(...) is deprecated. Use SyncRepository.open(...) for most use cases or SyncRepository.builder().build() for custom infrastructure options. This will be removed in v1.0.0.",
                DeprecationWarning,
                stacklevel=2,
            )
        self.stack_name = normalize_stack_name(name)
        self.table_name = self.stack_name
        self.region = region
        self.endpoint_url = endpoint_url
        self._namespace_id = schema.DEFAULT_NAMESPACE
        self._namespace_name = "default"
        self._bucket_ttl_refill_multiplier = 7
        self._stack_options = stack_options
        self._session: boto3.Session | None = None
        self._client: Any = None
        self._caller_identity_arn: str | None = None
        self._caller_identity_fetched = False
        self._audit_retention_days_cache: int | None = None
        self._builder_initialized = False
        self._auto_update = True
        self._is_scoped = False
        self._capabilities = BackendCapabilities(
            supports_audit_logging=True,
            supports_usage_snapshots=True,
            supports_infrastructure_management=True,
            supports_change_streams=True,
            supports_batch_operations=True,
        )
        self._config_cache = SyncConfigCache(
            ttl_seconds=config_cache_ttl, namespace_id=self._namespace_id
        )
        self._config_cache_ttl = config_cache_ttl
        self._entity_cache: dict[tuple[str, str], tuple[bool, str | None, dict[str, int]]] = {}
        self._on_unavailable_cache: OnUnavailableAction | None = None
        self._namespace_cache: dict[str, str] = {}
        self._parallel_mode = parallel_mode
        self._executor_fn = self._resolve_parallel_mode(parallel_mode)
        self._thread_pool: Any = None

    @classmethod
    def builder(cls) -> "SyncRepositoryBuilder":
        """Create a SyncRepositoryBuilder for fluent configuration.

        For most use cases, prefer :meth:`open` instead. Use ``builder()``
        when you need custom infrastructure options (permission boundaries,
        Lambda config, IAM role naming).

        Stack defaults mirror :meth:`open`: ``ZAEL_STACK`` env var
        or ``"zae-limiter"``, namespace ``"default"``.

        Example:
            repo = (
                SyncRepository.builder()
                .namespace("my-app")
                .lambda_memory(512)
                .build()
            )
        """
        from .sync_repository_builder import SyncRepositoryBuilder

        return SyncRepositoryBuilder()

    @classmethod
    def open(
        cls,
        namespace: str | None = None,
        parallel_mode: str = "auto",
        *,
        stack: str | None = None,
        region: str | None = None,
        endpoint_url: str | None = None,
        config_cache_ttl: int = 60,
        auto_update: bool = True,
    ) -> "SyncRepository":
        """Open a repository, auto-provisioning infrastructure if needed.

        This is the recommended entry point for most applications.
        Namespace is the primary parameter — stack name defaults to
        ``"zae-limiter"`` and is rarely needed.

        **Auto-provision behavior:**

        - If the DynamoDB table doesn't exist, deploys a new stack with
          default options (aggregator enabled).
        - If the table exists but the namespace isn't registered, registers it.
        - The ``"default"`` namespace is always registered.
        - Version check and Lambda auto-update run on every call.

        For custom infrastructure options (permission boundaries, Lambda
        config, IAM role naming), use ``SyncRepository.builder()`` instead.

        **Stack resolution:** ``stack`` arg → ``ZAEL_STACK`` env var
        → ``"zae-limiter"``.

        **Namespace resolution:** ``namespace`` arg → ``ZAEL_NAMESPACE``
        env var → ``"default"``.

        Args:
            namespace: Namespace to open. Defaults to ``ZAEL_NAMESPACE``
                env var or ``"default"``.
            stack: Stack name. Defaults to ``ZAEL_STACK`` env var
                or ``"zae-limiter"``.
            region: AWS region (e.g., ``"us-east-1"``).
            endpoint_url: Custom endpoint URL (e.g., LocalStack).
            config_cache_ttl: Config cache TTL in seconds (default: 60,
                0 to disable).
            auto_update: Auto-update Lambda on version mismatch
                (default: True).

        Returns:
            Fully initialized SyncRepository ready for use.

        Raises:
            IncompatibleSchemaError: If schema migration is required.
            VersionMismatchError: If auto_update is False and versions differ.

        Example::

            # Most users
            repo = SyncRepository.open("my-app")

            # Multi-tenant
            repo_alpha = SyncRepository.open("tenant-alpha")

            # Explicit stack
            repo = SyncRepository.open("my-app", stack="custom-stack")

            # Simplest (stack="zae-limiter", namespace="default")
            repo = SyncRepository.open()
        """
        from .naming import resolve_namespace_name, resolve_stack_name

        name = resolve_stack_name(stack)
        ns_name = resolve_namespace_name(namespace)
        repo = cls(
            name=name,
            region=region,
            endpoint_url=endpoint_url,
            config_cache_ttl=config_cache_ttl,
            _skip_deprecation_warning=True,
            parallel_mode=parallel_mode,
        )
        repo._auto_update = auto_update
        try:
            namespace_id = repo._resolve_namespace(ns_name)
        except ClientError as e:
            if e.response["Error"]["Code"] == "ResourceNotFoundException":
                repo._stack_options = StackOptions()
                repo._ensure_infrastructure_internal()
                repo._register_namespace("default")
                namespace_id = repo._register_namespace(ns_name)
            else:
                raise
        else:
            if namespace_id is None:
                namespace_id = repo._register_namespace(ns_name)
        repo._namespace_id = namespace_id
        repo._namespace_name = ns_name
        repo._reinitialize_config_cache(namespace_id)
        if auto_update:
            repo._check_and_update_version_auto()
        else:
            repo._check_version_strict()
        repo._builder_initialized = True
        return repo

    @property
    def namespace_name(self) -> str:
        """The human-readable namespace name."""
        return self._namespace_name

    @property
    def namespace_id(self) -> str:
        """The opaque namespace ID used in DynamoDB keys."""
        return self._namespace_id

    @property
    def capabilities(self) -> BackendCapabilities:
        """Declare which extended features this backend supports."""
        return self._capabilities

    def _get_client(self) -> Any:
        """Get or create the DynamoDB client."""
        if self._client is None:
            self._session = boto3.Session()
            self._client = self._session.client(
                "dynamodb", region_name=self.region, endpoint_url=self.endpoint_url
            )
        return self._client

    def namespace(
        self,
        name: str,
        *,
        on_unavailable: "OnUnavailableAction | None" = None,
        bucket_ttl_multiplier: int | None = None,
    ) -> "SyncRepository":
        """Return a scoped SyncRepository for the given namespace.

        The scoped repo shares the DynamoDB client, entity cache, and
        namespace cache with the parent, but has its own ``SyncConfigCache``
        and namespace identity.  Calling ``close()`` on a scoped repo
        is a no-op (it does not close the shared client).

        Args:
            name: Namespace name to resolve.
            on_unavailable: Override on_unavailable behavior for this
                namespace ("allow" or "block").  Persisted via
                ``set_system_defaults()``.
            bucket_ttl_multiplier: Override bucket TTL multiplier for
                this scoped repo.  Defaults to the parent's value.

        Returns:
            A new SyncRepository scoped to the resolved namespace.

        Raises:
            NamespaceNotFoundError: If the namespace is not registered.
        """
        from .exceptions import NamespaceNotFoundError

        namespace_id = self._resolve_namespace(name)
        if namespace_id is None:
            raise NamespaceNotFoundError(name)
        scoped = SyncRepository.__new__(SyncRepository)
        scoped.stack_name = self.stack_name
        scoped.table_name = self.table_name
        scoped.region = self.region
        scoped.endpoint_url = self.endpoint_url
        scoped._namespace_id = namespace_id
        scoped._namespace_name = name
        scoped._bucket_ttl_refill_multiplier = (
            bucket_ttl_multiplier
            if bucket_ttl_multiplier is not None
            else self._bucket_ttl_refill_multiplier
        )
        scoped._stack_options = None
        scoped._session = self._session
        scoped._client = self._client
        scoped._caller_identity_arn = self._caller_identity_arn
        scoped._caller_identity_fetched = self._caller_identity_fetched
        scoped._audit_retention_days_cache = self._audit_retention_days_cache
        scoped._builder_initialized = self._builder_initialized
        scoped._auto_update = self._auto_update
        scoped._is_scoped = True
        scoped._capabilities = self._capabilities
        scoped._config_cache_ttl = self._config_cache_ttl
        scoped._config_cache = SyncConfigCache(
            ttl_seconds=self._config_cache_ttl, namespace_id=namespace_id
        )
        scoped._entity_cache = self._entity_cache
        scoped._namespace_cache = self._namespace_cache
        scoped._on_unavailable_cache = None
        if on_unavailable is not None:
            existing_limits, _ = scoped.get_system_defaults()
            scoped.set_system_defaults(limits=existing_limits, on_unavailable=on_unavailable)
        scoped._parallel_mode = self._parallel_mode
        scoped._executor_fn = self._executor_fn
        scoped._thread_pool = self._thread_pool
        return scoped

    def close(self) -> None:
        """Close the DynamoDB client.

        No-op for scoped repos (created via ``namespace()``).
        """
        if self._is_scoped:
            return
        if self._client is not None:
            self._client = None
            self._session = None
        self._cleanup_thread_pool()

    def _get_item(self, pk: str, sk: str) -> dict[str, Any] | None:
        """Get a raw DynamoDB item by primary key (testing helper).

        Args:
            pk: Partition key value (already formatted, e.g., "ENTITY#user-1")
            sk: Sort key value (already formatted, e.g., "#BUCKET#api")

        Returns:
            Deserialized item dict or None if not found.
        """
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name, Key={"PK": {"S": pk}, "SK": {"S": sk}}
        )
        item = response.get("Item")
        if not item:
            return None
        result: dict[str, Any] = {}
        for key, value in item.items():
            if "S" in value:
                result[key] = value["S"]
            elif "N" in value:
                result[key] = int(value["N"])
        return result

    def _get_caller_identity_arn(self) -> str | None:
        """
        Get the ARN of the AWS caller identity (lazy cached).

        Returns the full ARN of the IAM user/role making API calls.
        Returns None if the identity cannot be determined (e.g., local testing).
        """
        if self._caller_identity_fetched:
            return self._caller_identity_arn
        self._caller_identity_fetched = True
        try:
            if self._session is None:
                self._session = boto3.Session()
            sts_client = self._session.client(
                "sts", region_name=self.region, endpoint_url=self.endpoint_url
            )
            response = sts_client.get_caller_identity()
            self._caller_identity_arn = response.get("Arn")
        except Exception:
            self._caller_identity_arn = None
        return self._caller_identity_arn

    def _now_ms(self) -> int:
        """Current time in milliseconds."""
        return int(time.time() * 1000)

    def create_table(self) -> None:
        """Create the DynamoDB table if it doesn't exist."""
        client = self._get_client()
        definition = schema.get_table_definition(self.table_name)
        try:
            client.create_table(**definition)
            waiter = client.get_waiter("table_exists")
            waiter.wait(TableName=self.table_name)
        except ClientError as e:
            if e.response["Error"]["Code"] != "ResourceInUseException":
                raise

    def delete_table(self) -> None:
        """Delete the DynamoDB table."""
        client = self._get_client()
        try:
            client.delete_table(TableName=self.table_name)
        except ClientError as e:
            if e.response["Error"]["Code"] != "ResourceNotFoundException":
                raise

    def delete_stack(self) -> None:
        """Delete the CloudFormation stack and all associated resources.

        Permanently removes the stack including DynamoDB table, Lambda
        aggregator, IAM roles, and CloudWatch log groups. Waits for
        deletion to complete. No-op if stack doesn't exist.

        Raises:
            StackCreationError: If deletion fails.
        """
        from .infra.sync_stack_manager import SyncStackManager

        with SyncStackManager(self.stack_name, self.region, self.endpoint_url) as manager:
            manager.delete_stack(self.stack_name)

    def ensure_infrastructure(self) -> None:
        """
        Ensure DynamoDB infrastructure exists.

        .. deprecated::
            Use ``SyncRepository.builder(...).build()`` instead, which handles
            infrastructure creation during the build step.

        Creates CloudFormation stack using stack_options passed to the constructor.
        No-op if stack_options was not provided.

        Raises:
            StackCreationError: If CloudFormation stack creation fails
        """
        import warnings

        warnings.warn(
            "ensure_infrastructure() is deprecated. Use SyncRepository.builder(...).build() instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        self._ensure_infrastructure_internal()

    def _ensure_infrastructure_internal(self) -> None:
        """Internal: ensure infrastructure exists (no deprecation warning)."""
        if self._stack_options is None:
            return
        from .infra.sync_stack_manager import SyncStackManager

        with SyncStackManager(self.stack_name, self.region, self.endpoint_url) as manager:
            manager.create_stack(stack_options=self._stack_options)
            if self._stack_options.enable_aggregator:
                manager.deploy_lambda_code()
        self._write_audit_retention_config()

    def create_stack(self, stack_options: StackOptions | None = None) -> None:
        """
        Create DynamoDB infrastructure via CloudFormation.

        .. deprecated:: 0.6.0
            Use :meth:`ensure_infrastructure` instead. Pass stack_options
            to the SyncRepository constructor. Will be removed in v1.0.0.

        Args:
            stack_options: Configuration for CloudFormation stack.
                If None, uses the stack_options passed to the constructor.

        Raises:
            StackCreationError: If CloudFormation stack creation fails
        """
        import warnings

        warnings.warn(
            "create_stack() is deprecated. Use ensure_infrastructure() instead. Pass stack_options to the SyncRepository constructor. This will be removed in v1.0.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        if stack_options is not None:
            saved = self._stack_options
            self._stack_options = stack_options
            try:
                self._ensure_infrastructure_internal()
            finally:
                self._stack_options = saved
        else:
            self._ensure_infrastructure_internal()

    def _register_namespace(self, name: str) -> str:
        """Register a namespace (idempotent).

        Creates two records under RESERVED_NAMESPACE:
        - ``PK=_/SYSTEM#, SK=#NAMESPACE#{name}`` (name → ID lookup)
        - ``PK=_/SYSTEM#, SK=#NSID#{id}`` (ID → name lookup)

        Uses TransactWriteItems with ConditionExpression to ensure atomicity.
        On TransactionCanceledException (namespace exists), resolves and returns
        the existing ID.

        Returns:
            The namespace_id (either newly created or existing).
        """
        import secrets

        client = self._get_client()
        namespace_id = secrets.token_urlsafe(8)
        while namespace_id.startswith("-"):
            namespace_id = secrets.token_urlsafe(8)
        now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        try:
            client.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": self.table_name,
                            "Item": {
                                "PK": {"S": pk},
                                "SK": {"S": schema.sk_namespace(name)},
                                "namespace_id": {"S": namespace_id},
                                "namespace_name": {"S": name},
                                "status": {"S": "active"},
                                "created_at": {"S": now},
                                "GSI4PK": {"S": schema.RESERVED_NAMESPACE},
                                "GSI4SK": {"S": pk},
                            },
                            "ConditionExpression": "attribute_not_exists(PK)",
                        }
                    },
                    {
                        "Put": {
                            "TableName": self.table_name,
                            "Item": {
                                "PK": {"S": pk},
                                "SK": {"S": schema.sk_nsid(namespace_id)},
                                "namespace_id": {"S": namespace_id},
                                "namespace_name": {"S": name},
                                "status": {"S": "active"},
                                "created_at": {"S": now},
                                "GSI4PK": {"S": schema.RESERVED_NAMESPACE},
                                "GSI4SK": {"S": pk},
                            },
                            "ConditionExpression": "attribute_not_exists(PK)",
                        }
                    },
                ]
            )
        except ClientError as e:
            if e.response["Error"]["Code"] == "TransactionCanceledException":
                existing_id = self._resolve_namespace(name)
                if existing_id is not None:
                    return existing_id
                raise
            raise
        self._namespace_cache[name] = namespace_id
        return namespace_id

    def _resolve_namespace(self, name: str) -> str | None:
        """Resolve a namespace name to its opaque ID.

        Returns None if the namespace doesn't exist or has status "deleted".
        """
        if name in self._namespace_cache:
            return self._namespace_cache[name]
        client = self._get_client()
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        response = client.get_item(
            TableName=self.table_name, Key={"PK": {"S": pk}, "SK": {"S": schema.sk_namespace(name)}}
        )
        item = response.get("Item")
        if not item:
            return None
        status = item.get("status", {}).get("S", "")
        if status in ("deleted", "purging"):
            return None
        namespace_id: str = item["namespace_id"]["S"]
        self._namespace_cache[name] = namespace_id
        return namespace_id

    def register_namespace(self, namespace: str) -> str:
        """Register a namespace in the registry (idempotent).

        Creates forward (name -> ID) and reverse (ID -> name) mappings.
        Idempotent: returns existing ID if namespace already registered.

        Args:
            namespace: Namespace name to register.

        Returns:
            The namespace_id (either newly created or existing).

        Raises:
            ValueError: If namespace is the reserved namespace ``"_"``.
        """
        if namespace == schema.RESERVED_NAMESPACE:
            raise ValueError(
                f"Namespace name {schema.RESERVED_NAMESPACE!r} is reserved for system use"
            )
        return self._register_namespace(namespace)

    def register_namespaces(self, namespaces: list[str]) -> dict[str, str]:
        """Bulk-register multiple namespaces.

        Registers all namespaces in DynamoDB (forward + reverse records each).

        Args:
            namespaces: List of namespace names to register.

        Returns:
            Mapping of ``{name: namespace_id}`` for all namespaces.

        Raises:
            ValueError: If any namespace is the reserved namespace ``"_"``.
        """
        for ns in namespaces:
            if ns == schema.RESERVED_NAMESPACE:
                raise ValueError(
                    f"Namespace name {schema.RESERVED_NAMESPACE!r} is reserved for system use"
                )
        ids = self._run_in_executor(
            *[lambda ns=ns: self._register_namespace(ns) for ns in namespaces]
        )
        return dict(zip(namespaces, ids))

    def get_namespace(self, namespace: str) -> dict[str, str] | None:
        """Get details for a single namespace by name.

        Args:
            namespace: Namespace name to look up.

        Returns:
            Dict with ``{name, namespace_id, status, created_at}``
            or ``None`` if not found.
        """
        client = self._get_client()
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        response = client.get_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_namespace(namespace)}},
        )
        item = response.get("Item")
        if not item:
            return None
        return {
            "name": namespace,
            "namespace_id": item["namespace_id"]["S"],
            "status": item.get("status", {}).get("S", "unknown"),
            "created_at": item.get("created_at", {}).get("S", "unknown"),
        }

    def list_namespaces(self) -> list[dict[str, str]]:
        """List all active namespaces with their IDs.

        Performs a Query on ``PK = "_/SYSTEM#"`` with
        ``SK begins_with "#NAMESPACE#"`` (forward records only).

        Returns:
            List of ``{name, namespace_id, created_at}`` dicts.
        """
        client = self._get_client()
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        results: list[dict[str, str]] = []
        exclusive_start_key = None
        while True:
            query_params: dict[str, Any] = {
                "TableName": self.table_name,
                "KeyConditionExpression": "PK = :pk AND begins_with(SK, :sk_prefix)",
                "ExpressionAttributeValues": {
                    ":pk": {"S": pk},
                    ":sk_prefix": {"S": schema.sk_namespace_prefix()},
                },
            }
            if exclusive_start_key is not None:
                query_params["ExclusiveStartKey"] = exclusive_start_key
            response = client.query(**query_params)
            for item in response.get("Items", []):
                results.append(
                    {
                        "name": item["namespace_name"]["S"],
                        "namespace_id": item["namespace_id"]["S"],
                        "created_at": item.get("created_at", {}).get("S", ""),
                    }
                )
            if "LastEvaluatedKey" not in response:
                break
            exclusive_start_key = response["LastEvaluatedKey"]
        return results

    def delete_namespace(self, namespace: str) -> None:
        """Soft-delete a namespace. O(1) for data plane.

        Removes the forward record and marks the reverse record as
        ``status="deleted"``.  Data items are NOT deleted — they remain
        orphaned under the namespace's random ID prefix.

        No-op if the namespace does not exist.

        Args:
            namespace: Namespace name to delete.

        Raises:
            ValueError: If namespace is the reserved namespace ``"_"``.
        """
        if namespace == schema.RESERVED_NAMESPACE:
            raise ValueError(
                f"Namespace name {schema.RESERVED_NAMESPACE!r} is reserved for system use"
            )
        client = self._get_client()
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        response = client.get_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_namespace(namespace)}},
        )
        item = response.get("Item")
        if not item:
            return
        namespace_id: str = item["namespace_id"]["S"]
        now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        client.delete_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_namespace(namespace)}},
        )
        client.update_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_nsid(namespace_id)}},
            UpdateExpression="SET #status = :deleted, deleted_at = :now",
            ExpressionAttributeNames={"#status": "status"},
            ExpressionAttributeValues={":deleted": {"S": "deleted"}, ":now": {"S": now}},
        )
        self._namespace_cache.pop(namespace, None)

    def recover_namespace(self, namespace_id: str) -> str:
        """Recover a deleted namespace by its ID.

        Reads the reverse record to find the original name, re-creates
        the forward record, and marks the reverse record as active.

        Args:
            namespace_id: Opaque namespace ID to recover.

        Returns:
            The recovered namespace name.

        Raises:
            EntityNotFoundError: If the reverse record does not exist.
            ValueError: If the reverse record has ``status="purging"``
                or ``status="active"``.
        """
        from .exceptions import EntityNotFoundError

        client = self._get_client()
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        response = client.get_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_nsid(namespace_id)}},
        )
        item = response.get("Item")
        if not item:
            raise EntityNotFoundError(namespace_id)
        status = item.get("status", {}).get("S", "")
        namespace_name: str = item["namespace_name"]["S"]
        if namespace_name == schema.RESERVED_NAMESPACE:
            raise ValueError(
                f"Namespace name {schema.RESERVED_NAMESPACE!r} is reserved for system use"
            )
        if status == "purging":
            raise ValueError(
                f"Cannot recover namespace '{namespace_name}' — purge is in progress and is terminal"
            )
        if status == "active":
            raise ValueError(f"Namespace '{namespace_name}' is already active (not deleted)")
        now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        created_at = item.get("created_at", {}).get("S", now)
        try:
            client.put_item(
                TableName=self.table_name,
                Item={
                    "PK": {"S": pk},
                    "SK": {"S": schema.sk_namespace(namespace_name)},
                    "namespace_id": {"S": namespace_id},
                    "namespace_name": {"S": namespace_name},
                    "status": {"S": "active"},
                    "created_at": {"S": created_at},
                    "GSI4PK": {"S": schema.RESERVED_NAMESPACE},
                    "GSI4SK": {"S": pk},
                },
                ConditionExpression="attribute_not_exists(PK)",
            )
        except client.exceptions.ConditionalCheckFailedException:
            raise ValueError(
                f"Cannot recover namespace '{namespace_name}' — the name has been re-registered by another caller"
            ) from None
        client.update_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_nsid(namespace_id)}},
            UpdateExpression="SET #status = :active REMOVE deleted_at",
            ExpressionAttributeNames={"#status": "status"},
            ExpressionAttributeValues={":active": {"S": "active"}},
        )
        self._namespace_cache[namespace_name] = namespace_id
        return namespace_name

    def list_orphan_namespaces(self) -> list[dict[str, str]]:
        """List deleted namespaces with orphaned data.

        Queries reverse records (``SK begins_with "#NSID#"``) and
        filters for ``status="deleted"``.

        Returns:
            List of ``{namespace_id, namespace, deleted_at}`` dicts.
        """
        client = self._get_client()
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        results: list[dict[str, str]] = []
        exclusive_start_key = None
        while True:
            query_params: dict[str, Any] = {
                "TableName": self.table_name,
                "KeyConditionExpression": "PK = :pk AND begins_with(SK, :sk_prefix)",
                "ExpressionAttributeValues": {
                    ":pk": {"S": pk},
                    ":sk_prefix": {"S": schema.sk_nsid_prefix()},
                },
            }
            if exclusive_start_key is not None:
                query_params["ExclusiveStartKey"] = exclusive_start_key
            response = client.query(**query_params)
            for item in response.get("Items", []):
                status = item.get("status", {}).get("S", "")
                if status == "deleted":
                    results.append(
                        {
                            "namespace_id": item["namespace_id"]["S"],
                            "namespace": item["namespace_name"]["S"],
                            "deleted_at": item.get("deleted_at", {}).get("S", ""),
                        }
                    )
            if "LastEvaluatedKey" not in response:
                break
            exclusive_start_key = response["LastEvaluatedKey"]
        return results

    def purge_namespace(self, namespace_id: str) -> None:
        """Purge all orphaned data for a deleted namespace.

        Verifies the namespace is in ``"deleted"`` status, transitions to
        ``"purging"``, queries GSI4 to find all items, deletes them in
        batches, then removes the reverse record.

        Safe to call on a non-existent namespace_id (no-op).

        Args:
            namespace_id: Opaque namespace ID to purge.

        Raises:
            ValueError: If the namespace is ``"active"`` (cannot purge
                an active namespace).
        """
        client = self._get_client()
        pk = schema.pk_system(schema.RESERVED_NAMESPACE)
        response = client.get_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_nsid(namespace_id)}},
        )
        item = response.get("Item")
        if not item:
            return
        status = item.get("status", {}).get("S", "")
        if status == "active":
            raise ValueError(
                f"Cannot purge active namespace '{item['namespace_name']['S']}'. Delete it first with delete_namespace()."
            )
        if status != "purging":
            client.update_item(
                TableName=self.table_name,
                Key={"PK": {"S": pk}, "SK": {"S": schema.sk_nsid(namespace_id)}},
                UpdateExpression="SET #status = :purging",
                ExpressionAttributeNames={"#status": "status"},
                ExpressionAttributeValues={":purging": {"S": "purging"}},
            )
        exclusive_start_key = None
        while True:
            query_params: dict[str, Any] = {
                "TableName": self.table_name,
                "IndexName": schema.GSI4_NAME,
                "KeyConditionExpression": "GSI4PK = :pk",
                "ExpressionAttributeValues": {":pk": {"S": namespace_id}},
            }
            if exclusive_start_key is not None:
                query_params["ExclusiveStartKey"] = exclusive_start_key
            response = client.query(**query_params)
            items = response.get("Items", [])
            if items:
                delete_requests = [
                    {"DeleteRequest": {"Key": {"PK": i["PK"], "SK": i["SK"]}}} for i in items
                ]
                for batch_start in range(0, len(delete_requests), 25):
                    chunk = delete_requests[batch_start : batch_start + 25]
                    unprocessed: list[Any] = chunk
                    while unprocessed:
                        resp = client.batch_write_item(RequestItems={self.table_name: unprocessed})
                        unprocessed = resp.get("UnprocessedItems", {}).get(self.table_name, [])
            if "LastEvaluatedKey" not in response:
                break
            exclusive_start_key = response["LastEvaluatedKey"]
        client.delete_item(
            TableName=self.table_name,
            Key={"PK": {"S": pk}, "SK": {"S": schema.sk_nsid(namespace_id)}},
        )

    def _reinitialize_config_cache(self, namespace_id: str) -> None:
        """Reinitialize the config cache with a new namespace_id."""
        self._config_cache = SyncConfigCache(
            ttl_seconds=self._config_cache_ttl, namespace_id=namespace_id
        )

    def _check_and_update_version_auto(self) -> None:
        """Check version compatibility and auto-update Lambda if needed.

        Used when ``auto_update=True``. On version mismatch, deploys
        updated Lambda code and updates the version record. On schema
        migration needed, raises ``IncompatibleSchemaError``.
        """
        from . import __version__
        from .version import InfrastructureVersion, check_compatibility

        version_record = self.get_version_record()
        if version_record is None:
            self._initialize_version_record()
            return
        infra_version = InfrastructureVersion.from_record(version_record)
        compatibility = check_compatibility(__version__, infra_version)
        if compatibility.is_compatible and (not compatibility.requires_lambda_update):
            return
        if compatibility.requires_schema_migration:
            from .exceptions import IncompatibleSchemaError

            raise IncompatibleSchemaError(
                client_version=__version__,
                schema_version=infra_version.schema_version,
                message=compatibility.message,
            )
        if compatibility.requires_lambda_update:
            self._perform_lambda_update()

    def _check_version_strict(self) -> None:
        """Check version compatibility in strict mode (no auto-update).

        Raises ``VersionMismatchError`` if the Lambda version differs
        from the client version.
        """
        from . import __version__
        from .exceptions import VersionMismatchError
        from .version import InfrastructureVersion, check_compatibility

        version_record = self.get_version_record()
        if version_record is None:
            self._initialize_version_record()
            return
        infra_version = InfrastructureVersion.from_record(version_record)
        compatibility = check_compatibility(__version__, infra_version)
        if compatibility.is_compatible and (not compatibility.requires_lambda_update):
            return
        if compatibility.requires_schema_migration:
            from .exceptions import IncompatibleSchemaError

            raise IncompatibleSchemaError(
                client_version=__version__,
                schema_version=infra_version.schema_version,
                message=compatibility.message,
            )
        if compatibility.requires_lambda_update:
            raise VersionMismatchError(
                client_version=__version__,
                schema_version=infra_version.schema_version,
                lambda_version=infra_version.lambda_version,
                message=compatibility.message,
                can_auto_update=True,
            )

    def _initialize_version_record(self) -> None:
        """Initialize the version record for first-time setup."""
        from . import __version__
        from .version import get_schema_version

        self.set_version_record(
            schema_version=get_schema_version(),
            lambda_version=__version__,
            client_min_version="0.0.0",
            updated_by=f"client:{__version__}",
        )

    def _perform_lambda_update(self) -> None:
        """Update Lambda code to match client version."""
        from . import __version__
        from .infra.sync_stack_manager import SyncStackManager
        from .version import get_schema_version

        with SyncStackManager(self.stack_name, self.region, self.endpoint_url) as manager:
            manager.deploy_lambda_code()
            self.set_version_record(
                schema_version=get_schema_version(),
                lambda_version=__version__,
                client_min_version="0.0.0",
                updated_by=f"client:{__version__}",
            )

    def create_entity(
        self,
        entity_id: str,
        name: str | None = None,
        parent_id: str | None = None,
        cascade: bool = False,
        metadata: dict[str, str] | None = None,
        principal: str | None = None,
    ) -> Entity:
        """
        Create a new entity.

        Args:
            entity_id: Unique identifier for the entity
            name: Optional display name (defaults to entity_id)
            parent_id: Optional parent entity ID (for hierarchical limits)
            cascade: If True, acquire() will also consume from parent entity
            metadata: Optional key-value metadata
            principal: Caller identity for audit logging

        Returns:
            The created Entity

        Raises:
            InvalidIdentifierError: If entity_id or parent_id is invalid
            EntityExistsError: If entity already exists
        """
        validate_identifier(entity_id, "entity_id")
        if parent_id is not None:
            validate_identifier(parent_id, "parent_id")
        client = self._get_client()
        now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
            "SK": {"S": schema.sk_meta()},
            "entity_id": {"S": entity_id},
            "name": {"S": name or entity_id},
            "parent_id": {"S": parent_id} if parent_id else {"NULL": True},
            "cascade": {"BOOL": cascade},
            "metadata": {"M": self._serialize_map(metadata or {})},
            "created_at": {"S": now},
            "GSI4PK": {"S": self._namespace_id},
            "GSI4SK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
        }
        if parent_id:
            item["GSI1PK"] = {"S": schema.gsi1_pk_parent(self._namespace_id, parent_id)}
            item["GSI1SK"] = {"S": schema.gsi1_sk_child(entity_id)}
        try:
            client.put_item(
                TableName=self.table_name, Item=item, ConditionExpression="attribute_not_exists(PK)"
            )
        except ClientError as e:
            if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
                raise EntityExistsError(entity_id)
            raise
        self._log_audit_event(
            action=AuditAction.ENTITY_CREATED,
            entity_id=entity_id,
            principal=principal,
            details={
                "name": name or entity_id,
                "parent_id": parent_id,
                "cascade": cascade,
                "metadata": metadata or {},
            },
        )
        return Entity(
            id=entity_id,
            name=name or entity_id,
            parent_id=parent_id,
            cascade=cascade,
            metadata=metadata or {},
            created_at=now,
        )

    def get_entity(self, entity_id: str) -> Entity | None:
        """Get an entity by ID."""
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
                "SK": {"S": schema.sk_meta()},
            },
        )
        item = response.get("Item")
        cache_key = (self._namespace_id, entity_id)
        existing_shards = self._entity_cache.get(cache_key, (False, None, {}))[2]
        if not item:
            self._entity_cache[cache_key] = (False, None, existing_shards)
            return None
        entity = self._deserialize_entity(item)
        self._entity_cache[cache_key] = (entity.cascade, entity.parent_id, existing_shards)
        return entity

    def delete_entity(self, entity_id: str, principal: str | None = None) -> None:
        """
        Delete an entity and all its related records.

        Args:
            entity_id: ID of the entity to delete
            principal: Caller identity for audit logging
        """
        client = self._get_client()
        response = client.query(
            TableName=self.table_name,
            KeyConditionExpression="PK = :pk",
            ExpressionAttributeValues={
                ":pk": {"S": schema.pk_entity(self._namespace_id, entity_id)}
            },
        )
        items = response.get("Items", [])
        gsi3_response = client.query(
            TableName=self.table_name,
            IndexName="GSI3",
            KeyConditionExpression="GSI3PK = :gsi3pk",
            ExpressionAttributeValues={
                ":gsi3pk": {"S": schema.gsi3_pk_entity(self._namespace_id, entity_id)}
            },
        )
        bucket_items = gsi3_response.get("Items", [])
        all_items = items + bucket_items
        if not all_items:
            return
        delete_requests = [
            {"DeleteRequest": {"Key": {"PK": item["PK"], "SK": item["SK"]}}} for item in all_items
        ]
        for i in range(0, len(delete_requests), 25):
            chunk = delete_requests[i : i + 25]
            client.batch_write_item(RequestItems={self.table_name: chunk})
        self._log_audit_event(
            action=AuditAction.ENTITY_DELETED,
            entity_id=entity_id,
            principal=principal,
            details={"records_deleted": len(all_items)},
        )

    def get_children(self, parent_id: str) -> list[Entity]:
        """Get all children of a parent entity."""
        client = self._get_client()
        response = client.query(
            TableName=self.table_name,
            IndexName=schema.GSI1_NAME,
            KeyConditionExpression="GSI1PK = :pk",
            ExpressionAttributeValues={
                ":pk": {"S": schema.gsi1_pk_parent(self._namespace_id, parent_id)}
            },
        )
        entities = []
        for item in response.get("Items", []):
            entity = self._deserialize_entity(item)
            if entity:
                entities.append(entity)
        return entities

    def get_bucket(
        self, entity_id: str, resource: str, limit_name: str, shard_id: int = 0
    ) -> BucketState | None:
        """Get a single limit's bucket from the composite item."""
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)},
                "SK": {"S": schema.sk_state()},
            },
        )
        item = response.get("Item")
        if not item:
            return None
        buckets = self._deserialize_composite_bucket(item)
        for b in buckets:
            if b.limit_name == limit_name:
                return b
        return None

    def get_buckets(
        self, entity_id: str, resource: str | None = None, shard_id: int = 0
    ) -> list[BucketState]:
        """Get all buckets for an entity, optionally filtered by resource.

        With pre-shard buckets (v0.9.0+), each item lives on its own partition
        key ``PK={ns}/BUCKET#{id}#{resource}#{shard}``. When resource is
        specified, fetches the single bucket at the given shard_id. When
        resource is None, uses GSI3 (KEYS_ONLY) to discover all bucket PKs,
        then BatchGetItem to fetch full items.

        The internal ``wcu`` infrastructure limit is filtered from the
        returned bucket states.

        Args:
            entity_id: Entity to query buckets for.
            resource: Resource name filter, or None for all resources.
            shard_id: Shard index (used only when resource is specified).

        Returns:
            List of BucketState objects (one per application limit).
        """
        client = self._get_client()
        if resource:
            response = client.get_item(
                TableName=self.table_name,
                Key={
                    "PK": {
                        "S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)
                    },
                    "SK": {"S": schema.sk_state()},
                },
            )
            item = response.get("Item")
            if not item:
                return []
            return [
                b
                for b in self._deserialize_composite_bucket(item)
                if b.limit_name != schema.WCU_LIMIT_NAME
            ]
        key_condition = "GSI3PK = :gsi3pk"
        expression_values: dict[str, Any] = {
            ":gsi3pk": {"S": schema.gsi3_pk_entity(self._namespace_id, entity_id)}
        }
        response = client.query(
            TableName=self.table_name,
            IndexName="GSI3",
            KeyConditionExpression=key_condition,
            ExpressionAttributeValues=expression_values,
        )
        gsi3_items = response.get("Items", [])
        if not gsi3_items:
            return []
        request_keys = [{"PK": item["PK"], "SK": item["SK"]} for item in gsi3_items]
        buckets: list[BucketState] = []
        for i in range(0, len(request_keys), 100):
            chunk = request_keys[i : i + 100]
            batch_response = client.batch_get_item(RequestItems={self.table_name: {"Keys": chunk}})
            for full_item in batch_response.get("Responses", {}).get(self.table_name, []):
                buckets.extend(self._deserialize_composite_bucket(full_item))
        return [b for b in buckets if b.limit_name != schema.WCU_LIMIT_NAME]

    def batch_get_buckets(
        self, keys: list[tuple[str, str]]
    ) -> dict[tuple[str, str, str], BucketState]:
        """
        Batch get composite buckets in a single DynamoDB call.

        With composite items, each (entity_id, resource, shard) is a single
        DynamoDB item containing all limits. Returns individual BucketStates
        keyed by (entity_id, resource, limit_name) for backward compatibility.

        Args:
            keys: List of (entity_id, resource) tuples. Uses shard_id=0.

        Returns:
            Dict mapping (entity_id, resource, limit_name) to BucketState.
            Missing composite items are not included in the result.

        Note:
            DynamoDB BatchGetItem supports up to 100 items per request.
            For larger batches, this method automatically chunks the requests.
        """
        if not keys:
            return {}
        client = self._get_client()
        result: dict[tuple[str, str, str], BucketState] = {}
        unique_keys = list(set(keys))
        for i in range(0, len(unique_keys), 100):
            chunk = unique_keys[i : i + 100]
            request_keys = [
                {
                    "PK": {"S": schema.pk_bucket(self._namespace_id, entity_id, resource, 0)},
                    "SK": {"S": schema.sk_state()},
                }
                for entity_id, resource in chunk
            ]
            response = client.batch_get_item(RequestItems={self.table_name: {"Keys": request_keys}})
            items = response.get("Responses", {}).get(self.table_name, [])
            for item in items:
                buckets = self._deserialize_composite_bucket(item)
                for bucket in buckets:
                    key = (bucket.entity_id, bucket.resource, bucket.limit_name)
                    result[key] = bucket
        return result

    def batch_get_entity_and_buckets(
        self, entity_id: str, bucket_keys: list[tuple[str, str]]
    ) -> tuple[Entity | None, dict[tuple[str, str, str], BucketState]]:
        """
        Fetch entity metadata and composite buckets in a single BatchGetItem.

        With composite items, each (entity_id, resource) pair is a single
        DynamoDB item. Includes the entity's #META record alongside bucket
        records to avoid a separate get_entity() round trip.

        Args:
            entity_id: Entity whose META record to include
            bucket_keys: List of (entity_id, resource) for composite buckets

        Returns:
            Tuple of (entity_or_none, bucket_dict) where bucket_dict maps
            (entity_id, resource, limit_name) to BucketState.

        Note:
            DynamoDB BatchGetItem supports up to 100 items per request.
            The META key counts toward that limit.
        """
        client = self._get_client()
        meta_key = {
            "PK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
            "SK": {"S": schema.sk_meta()},
        }
        request_keys = [meta_key]
        unique_bucket_keys = list(set(bucket_keys))
        for eid, resource in unique_bucket_keys:
            request_keys.append(
                {
                    "PK": {"S": schema.pk_bucket(self._namespace_id, eid, resource, 0)},
                    "SK": {"S": schema.sk_state()},
                }
            )
        entity: Entity | None = None
        buckets: dict[tuple[str, str, str], BucketState] = {}
        for i in range(0, len(request_keys), 100):
            chunk = request_keys[i : i + 100]
            response = client.batch_get_item(RequestItems={self.table_name: {"Keys": chunk}})
            items = response.get("Responses", {}).get(self.table_name, [])
            for item in items:
                sk = item.get("SK", {}).get("S", "")
                if sk == schema.sk_meta():
                    entity = self._deserialize_entity(item)
                elif sk.startswith(schema.SK_BUCKET):
                    for bucket in self._deserialize_composite_bucket(item):
                        key = (bucket.entity_id, bucket.resource, bucket.limit_name)
                        buckets[key] = bucket
        cache_key = (self._namespace_id, entity_id)
        existing_shards = self._entity_cache.get(cache_key, (False, None, {}))[2]
        if entity is not None:
            self._entity_cache[cache_key] = (entity.cascade, entity.parent_id, existing_shards)
        else:
            self._entity_cache[cache_key] = (False, None, existing_shards)
        return (entity, buckets)

    def batch_get_configs(
        self, keys: list[tuple[str, str]]
    ) -> dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]:
        """
        Batch get config items in a single DynamoDB call.

        Fetches config records (entity, resource, system level) in a single
        BatchGetItem request and returns deserialized limits.

        Args:
            keys: List of (PK, SK) tuples identifying config items

        Returns:
            Dict mapping (PK, SK) to (limits, on_unavailable) tuples.
            on_unavailable is extracted from system config items (None for others).
            Missing items are not included in the result.

        Note:
            DynamoDB BatchGetItem supports up to 100 items per request.
            For larger batches, this method automatically chunks the requests.
            Uses eventually consistent reads (0.5 RCU per item).
        """
        if not keys:
            return {}
        client = self._get_client()
        result: dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]] = {}
        unique_keys = list(set(keys))
        for i in range(0, len(unique_keys), 100):
            chunk = unique_keys[i : i + 100]
            request_keys = [{"PK": {"S": pk}, "SK": {"S": sk}} for pk, sk in chunk]
            response = client.batch_get_item(
                RequestItems={self.table_name: {"Keys": request_keys, "ConsistentRead": False}}
            )
            items = response.get("Responses", {}).get(self.table_name, [])
            for item in items:
                pk = item.get("PK", {}).get("S", "")
                sk = item.get("SK", {}).get("S", "")
                if pk and sk:
                    limits = self._deserialize_composite_limits(item)
                    ou_attr = item.get("on_unavailable", {})
                    ou_str = ou_attr.get("S") if ou_attr else None
                    on_unavailable: OnUnavailableAction | None = (
                        cast(OnUnavailableAction, ou_str) if ou_str else None
                    )
                    result[pk, sk] = (limits, on_unavailable)
        return result

    def get_or_create_bucket(self, entity_id: str, resource: str, limit: Limit) -> BucketState:
        """
        Get an existing bucket or create a new one with the given limit.

        If the bucket exists, it is returned. If not, a new bucket is created
        with capacity set to the limit's capacity.

        Args:
            entity_id: Entity owning the bucket
            resource: Resource name (e.g., "gpt-4")
            limit: Limit configuration for the bucket

        Returns:
            Existing or newly created BucketState
        """
        existing = self.get_bucket(entity_id, resource, limit.name)
        if existing is not None:
            return existing
        now_ms = self._now_ms()
        state = BucketState(
            entity_id=entity_id,
            resource=resource,
            limit_name=limit.name,
            tokens_milli=limit.capacity * 1000,
            last_refill_ms=now_ms,
            capacity_milli=limit.capacity * 1000,
            refill_amount_milli=limit.refill_amount * 1000,
            refill_period_ms=limit.refill_period_seconds * 1000,
            total_consumed_milli=0,
        )
        put_item = self.build_bucket_put_item(state)
        self.transact_write([put_item])
        return state

    def build_bucket_put_item(self, state: BucketState, ttl_seconds: int = 86400) -> dict[str, Any]:
        """Build a PutItem for a composite bucket (for use in transactions).

        Wraps build_composite_create for backward compatibility with protocol.
        """
        now_ms = self._now_ms()
        return self.build_composite_create(
            entity_id=state.entity_id,
            resource=state.resource,
            states=[state],
            now_ms=now_ms,
            ttl_seconds=ttl_seconds,
        )

    def build_bucket_update_item(
        self,
        entity_id: str,
        resource: str,
        limit_name: str,
        new_tokens_milli: int,
        new_last_refill_ms: int,
        expected_tokens_milli: int | None = None,
        shard_id: int = 0,
    ) -> dict[str, Any]:
        """Build an UpdateItem for a single limit in a composite bucket.

        Legacy method — prefer build_composite_normal/retry/adjust for
        composite writes. This updates one limit's tk within the composite item.
        """
        tk_attr = schema.bucket_attr(limit_name, schema.BUCKET_FIELD_TK)
        update: dict[str, dict[str, Any]] = {
            "Update": {
                "TableName": self.table_name,
                "Key": {
                    "PK": {
                        "S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)
                    },
                    "SK": {"S": schema.sk_state()},
                },
                "UpdateExpression": "SET #tokens = :tokens, #refill = :refill",
                "ExpressionAttributeNames": {"#tokens": tk_attr, "#refill": schema.BUCKET_FIELD_RF},
                "ExpressionAttributeValues": {
                    ":tokens": {"N": str(new_tokens_milli)},
                    ":refill": {"N": str(new_last_refill_ms)},
                },
            }
        }
        if expected_tokens_milli is not None:
            update["Update"]["ConditionExpression"] = "#tokens = :expected"
            update["Update"]["ExpressionAttributeValues"][":expected"] = {
                "N": str(expected_tokens_milli)
            }
        return update

    def build_composite_create(
        self,
        entity_id: str,
        resource: str,
        states: list[BucketState],
        now_ms: int,
        ttl_seconds: int | None = 86400,
        cascade: bool = False,
        parent_id: str | None = None,
        shard_id: int = 0,
        shard_count: int = 1,
    ) -> dict[str, Any]:
        """Build a PutItem for creating a new composite bucket.

        Used on first acquire for an entity+resource. Condition ensures no
        concurrent creation race (attribute_not_exists).

        Args:
            entity_id: Entity owning the bucket
            resource: Resource name
            states: BucketState objects for each limit
            now_ms: Current timestamp in milliseconds
            ttl_seconds: TTL in seconds from now, or None to omit TTL
            cascade: Whether the entity has cascade enabled
            parent_id: The entity's parent_id (if any)
            shard_id: Shard index for this bucket (default 0)
            shard_count: Total number of shards (default 1)
        """
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)},
            "SK": {"S": schema.sk_state()},
            "entity_id": {"S": entity_id},
            "resource": {"S": resource},
            schema.BUCKET_FIELD_RF: {"N": str(now_ms)},
            "GSI2PK": {"S": schema.gsi2_pk_resource(self._namespace_id, resource)},
            "GSI2SK": {"S": schema.gsi2_sk_bucket(entity_id, shard_id)},
            "cascade": {"BOOL": cascade},
            "GSI3PK": {"S": schema.gsi3_pk_entity(self._namespace_id, entity_id)},
            "GSI3SK": {"S": schema.gsi3_sk_bucket(resource, shard_id)},
            "GSI4PK": {"S": self._namespace_id},
            "GSI4SK": {"S": schema.gsi4_sk_bucket(entity_id, resource, shard_id)},
            "shard_count": {"N": str(shard_count)},
        }
        if parent_id is not None:
            item["parent_id"] = {"S": parent_id}
        if ttl_seconds is not None:
            item["ttl"] = {"N": str(schema.calculate_ttl(now_ms, ttl_seconds))}
        wcu_cp_milli = schema.WCU_LIMIT_CAPACITY * 1000
        wcu_ra_milli = schema.WCU_LIMIT_REFILL_AMOUNT * 1000
        wcu_rp_ms = schema.WCU_LIMIT_REFILL_PERIOD_SECONDS * 1000
        wcu_name = schema.WCU_LIMIT_NAME
        item[schema.bucket_attr(wcu_name, schema.BUCKET_FIELD_TK)] = {"N": str(wcu_cp_milli)}
        item[schema.bucket_attr(wcu_name, schema.BUCKET_FIELD_CP)] = {"N": str(wcu_cp_milli)}
        item[schema.bucket_attr(wcu_name, schema.BUCKET_FIELD_RA)] = {"N": str(wcu_ra_milli)}
        item[schema.bucket_attr(wcu_name, schema.BUCKET_FIELD_RP)] = {"N": str(wcu_rp_ms)}
        item[schema.bucket_attr(wcu_name, schema.BUCKET_FIELD_TC)] = {"N": "0"}
        for state in states:
            name = state.limit_name
            item[schema.bucket_attr(name, schema.BUCKET_FIELD_TK)] = {"N": str(state.tokens_milli)}
            item[schema.bucket_attr(name, schema.BUCKET_FIELD_CP)] = {
                "N": str(state.capacity_milli)
            }
            item[schema.bucket_attr(name, schema.BUCKET_FIELD_RA)] = {
                "N": str(state.refill_amount_milli)
            }
            item[schema.bucket_attr(name, schema.BUCKET_FIELD_RP)] = {
                "N": str(state.refill_period_ms)
            }
            tc = state.total_consumed_milli if state.total_consumed_milli is not None else 0
            item[schema.bucket_attr(name, schema.BUCKET_FIELD_TC)] = {"N": str(tc)}
        return {
            "Put": {
                "TableName": self.table_name,
                "Item": item,
                "ConditionExpression": "attribute_not_exists(PK)",
            }
        }

    def build_composite_normal(
        self,
        entity_id: str,
        resource: str,
        consumed: dict[str, int],
        refill_amounts: dict[str, int],
        now_ms: int,
        expected_rf: int,
        ttl_seconds: int | None = None,
        shard_id: int = 0,
    ) -> dict[str, Any]:
        """Build an UpdateItem for the normal write path (ADR-115 path 2).

        ADD tk:(refill - consumed), tc:consumed for each limit.
        SET rf:now. CONDITION rf = :expected.

        Args:
            entity_id: Entity owning the bucket
            resource: Resource name
            consumed: Amount consumed per limit (millitokens)
            refill_amounts: Refill amount per limit (millitokens)
            now_ms: Current timestamp in milliseconds
            expected_rf: Expected refill timestamp for optimistic lock
            ttl_seconds: TTL behavior:
                - None: Don't change TTL
                - 0: REMOVE ttl (entity has custom limits)
                - >0: SET ttl to (now + ttl_seconds)
            shard_id: Shard index for this bucket (default 0)
        """
        add_parts: list[str] = []
        set_parts: list[str] = ["#rf = :now"]
        remove_parts: list[str] = []
        attr_names: dict[str, str] = {"#rf": schema.BUCKET_FIELD_RF}
        attr_values: dict[str, Any] = {
            ":now": {"N": str(now_ms)},
            ":expected_rf": {"N": str(expected_rf)},
        }
        if ttl_seconds is not None:
            attr_names["#ttl"] = "ttl"
            if ttl_seconds > 0:
                set_parts.append("#ttl = :ttl_val")
                attr_values[":ttl_val"] = {"N": str(schema.calculate_ttl(now_ms, ttl_seconds))}
            else:
                remove_parts.append("#ttl")
        condition_parts: list[str] = ["#rf = :expected_rf"]
        for name in consumed:
            c = consumed[name]
            r = refill_amounts.get(name, 0)
            tk_delta = r - c
            tk_alias = f"#b_{name}_tk"
            tc_alias = f"#b_{name}_tc"
            tk_val = f":b_{name}_tk_delta"
            tc_val = f":b_{name}_tc_delta"
            attr_names[tk_alias] = schema.bucket_attr(name, schema.BUCKET_FIELD_TK)
            attr_names[tc_alias] = schema.bucket_attr(name, schema.BUCKET_FIELD_TC)
            attr_values[tk_val] = {"N": str(tk_delta)}
            attr_values[tc_val] = {"N": str(c)}
            add_parts.append(f"{tk_alias} {tk_val}")
            add_parts.append(f"{tc_alias} {tc_val}")
            floor = max(0, c - r)
            if floor > 0:
                floor_val = f":b_{name}_tk_floor"
                attr_values[floor_val] = {"N": str(floor)}
                condition_parts.append(f"{tk_alias} >= {floor_val}")
        update_expr = f"SET {', '.join(set_parts)} ADD {', '.join(add_parts)}"
        if remove_parts:
            update_expr += f" REMOVE {', '.join(remove_parts)}"
        return {
            "Update": {
                "TableName": self.table_name,
                "Key": {
                    "PK": {
                        "S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)
                    },
                    "SK": {"S": schema.sk_state()},
                },
                "UpdateExpression": update_expr,
                "ConditionExpression": " AND ".join(condition_parts),
                "ExpressionAttributeNames": attr_names,
                "ExpressionAttributeValues": attr_values,
            }
        }

    def build_composite_retry(
        self, entity_id: str, resource: str, consumed: dict[str, int], shard_id: int = 0
    ) -> dict[str, Any]:
        """Build an UpdateItem for the retry write path (ADR-115 path 3).

        Lost optimistic lock — skip refill, only consume.
        ADD tk:(-consumed), tc:consumed for each limit.
        CONDITION: tk >= consumed per limit (prevent negative on acquire).
        """
        add_parts: list[str] = []
        condition_parts: list[str] = []
        attr_names: dict[str, str] = {}
        attr_values: dict[str, Any] = {}
        for name in consumed:
            c = consumed[name]
            tk_alias = f"#b_{name}_tk"
            tc_alias = f"#b_{name}_tc"
            tk_neg_val = f":b_{name}_tk_neg"
            tc_val = f":b_{name}_tc_delta"
            tk_threshold = f":b_{name}_tk_min"
            attr_names[tk_alias] = schema.bucket_attr(name, schema.BUCKET_FIELD_TK)
            attr_names[tc_alias] = schema.bucket_attr(name, schema.BUCKET_FIELD_TC)
            attr_values[tk_neg_val] = {"N": str(-c)}
            attr_values[tc_val] = {"N": str(c)}
            attr_values[tk_threshold] = {"N": str(c)}
            add_parts.append(f"{tk_alias} {tk_neg_val}")
            add_parts.append(f"{tc_alias} {tc_val}")
            condition_parts.append(f"{tk_alias} >= {tk_threshold}")
        update_expr = f"ADD {', '.join(add_parts)}"
        condition_expr = " AND ".join(condition_parts)
        return {
            "Update": {
                "TableName": self.table_name,
                "Key": {
                    "PK": {
                        "S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)
                    },
                    "SK": {"S": schema.sk_state()},
                },
                "UpdateExpression": update_expr,
                "ConditionExpression": condition_expr,
                "ExpressionAttributeNames": attr_names,
                "ExpressionAttributeValues": attr_values,
            }
        }

    def build_composite_adjust(
        self, entity_id: str, resource: str, deltas: dict[str, int], shard_id: int = 0
    ) -> dict[str, Any]:
        """Build an UpdateItem for the adjust write path (ADR-115 path 4).

        Unconditional ADD for post-hoc correction. Can go negative by design.
        Positive delta = consumed more (subtract tokens, add to counter).
        Negative delta = consumed less (add tokens, subtract from counter).
        """
        add_parts: list[str] = []
        attr_names: dict[str, str] = {}
        attr_values: dict[str, Any] = {}
        for name, delta in deltas.items():
            if delta == 0:
                continue
            tk_alias = f"#b_{name}_tk"
            tc_alias = f"#b_{name}_tc"
            tk_val = f":b_{name}_tk_delta"
            tc_val = f":b_{name}_tc_delta"
            attr_names[tk_alias] = schema.bucket_attr(name, schema.BUCKET_FIELD_TK)
            attr_names[tc_alias] = schema.bucket_attr(name, schema.BUCKET_FIELD_TC)
            attr_values[tk_val] = {"N": str(-delta)}
            attr_values[tc_val] = {"N": str(delta)}
            add_parts.append(f"{tk_alias} {tk_val}")
            add_parts.append(f"{tc_alias} {tc_val}")
        if not add_parts:
            return {}
        update_expr = f"ADD {', '.join(add_parts)}"
        return {
            "Update": {
                "TableName": self.table_name,
                "Key": {
                    "PK": {
                        "S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)
                    },
                    "SK": {"S": schema.sk_state()},
                },
                "UpdateExpression": update_expr,
                "ExpressionAttributeNames": attr_names,
                "ExpressionAttributeValues": attr_values,
            }
        }

    def transact_write(self, items: list[dict[str, Any]]) -> None:
        """Execute a write, using single-item API when possible to halve WCU cost."""
        if not items:
            return
        client = self._get_client()
        if len(items) == 1:
            item = items[0]
            if "Put" in item:
                client.put_item(**item["Put"])
            elif "Update" in item:
                client.update_item(**item["Update"])
            elif "Delete" in item:
                client.delete_item(**item["Delete"])
            else:
                client.transact_write_items(TransactItems=items)
        else:
            client.transact_write_items(TransactItems=items)

    def write_each(self, items: list[dict[str, Any]]) -> None:
        """Write items independently without cross-item atomicity (1 WCU each).

        Each item is dispatched as a single PutItem, UpdateItem, or DeleteItem
        call. Use for unconditional writes (e.g., ADD adjustments) where partial
        success is acceptable.
        """
        if not items:
            return
        client = self._get_client()
        for item in items:
            if "Put" in item:
                client.put_item(**item["Put"])
            elif "Update" in item:
                client.update_item(**item["Update"])
            elif "Delete" in item:
                client.delete_item(**item["Delete"])

    def speculative_consume(
        self,
        entity_id: str,
        resource: str,
        consume: dict[str, int],
        ttl_seconds: int | None = None,
        shard_id: int | None = None,
    ) -> SpeculativeResult:
        """Attempt speculative UpdateItem with condition check.

        Checks entity cache for cascade metadata. If cache hit + cascade,
        issues child+parent UpdateItems concurrently via _run_in_executor
        and returns nested parent_result.

        When ``shard_id`` is explicitly provided, targets that shard directly
        without cascade logic (used for shard retry).

        Args:
            entity_id: Entity owning the bucket
            resource: Resource name
            consume: Amount per limit (tokens, not milli)
            ttl_seconds: TTL in seconds from now, or None for no TTL change
            shard_id: Explicit shard to target (skips random selection and
                cascade logic). None means auto-select from entity cache.

        Returns:
            SpeculativeResult with:
            - On cache hit + cascade + both succeed: parent_result populated
            - On cache miss or non-cascade: parent_result is None
            - On failure: old_buckets from ALL_OLD (or None if bucket missing)
        """
        if shard_id is not None:
            return self._speculative_consume_single(
                entity_id, resource, consume, ttl_seconds, shard_id=shard_id
            )
        cache_key = (self._namespace_id, entity_id)
        cache_entry = self._entity_cache.get(cache_key)
        shard_count = 1
        if cache_entry is not None:
            shard_count = cache_entry[2].get(resource, 1)
        effective_shard_id = random.randrange(shard_count) if shard_count > 1 else 0
        if cache_entry is not None:
            cascade_cached, parent_id_cached, shards_cached = cache_entry
            if cascade_cached and parent_id_cached:
                child_result: SpeculativeResult
                parent_result: SpeculativeResult
                child_result, parent_result = self._run_in_executor(
                    lambda: self._speculative_consume_single(
                        entity_id, resource, consume, ttl_seconds, shard_id=effective_shard_id
                    ),
                    lambda: self._speculative_consume_single(
                        parent_id_cached, resource, consume, ttl_seconds
                    ),
                )
                if child_result.success:
                    shards_cached = {**shards_cached, resource: child_result.shard_count}
                    self._entity_cache[cache_key] = (
                        child_result.cascade,
                        child_result.parent_id,
                        shards_cached,
                    )
                else:
                    child_result.cascade = cascade_cached
                    child_result.parent_id = parent_id_cached
                child_result.parent_result = parent_result
                return child_result
        result = self._speculative_consume_single(
            entity_id, resource, consume, ttl_seconds, shard_id=effective_shard_id
        )
        if result.success:
            existing_shards = self._entity_cache.get(cache_key, (False, None, {}))[2]
            existing_shards = {**existing_shards, resource: result.shard_count}
            self._entity_cache[cache_key] = (result.cascade, result.parent_id, existing_shards)
        return result

    def _speculative_consume_single(
        self,
        entity_id: str,
        resource: str,
        consume: dict[str, int],
        ttl_seconds: int | None = None,
        shard_id: int = 0,
    ) -> SpeculativeResult:
        """Issue a single speculative UpdateItem on a bucket shard.

        In addition to consuming the application-level limits, this method
        auto-consumes 1 WCU (1000 millitokens) from the ``wcu`` infrastructure
        limit and includes ``wcu tk >= 1000`` in the condition expression.

        Args:
            entity_id: Entity owning the bucket.
            resource: Resource name.
            consume: Amount per limit (tokens, not milli).
            ttl_seconds: TTL in seconds, or None for no TTL change.
            shard_id: Target shard index (default 0).

        Returns:
            SpeculativeResult with shard_id and shard_count populated.
        """
        client = self._get_client()
        add_parts: list[str] = []
        condition_parts: list[str] = ["attribute_exists(PK)"]
        attr_names: dict[str, str] = {}
        attr_values: dict[str, Any] = {}
        for limit_name, amount in consume.items():
            amount_milli = amount * 1000
            tk_attr = schema.bucket_attr(limit_name, schema.BUCKET_FIELD_TK)
            tc_attr = schema.bucket_attr(limit_name, schema.BUCKET_FIELD_TC)
            tk_alias = f"#tk_{limit_name}"
            tc_alias = f"#tc_{limit_name}"
            neg_val = f":neg_{limit_name}"
            pos_val = f":pos_{limit_name}"
            thresh_val = f":thresh_{limit_name}"
            attr_names[tk_alias] = tk_attr
            attr_names[tc_alias] = tc_attr
            attr_values[neg_val] = {"N": str(-amount_milli)}
            attr_values[pos_val] = {"N": str(amount_milli)}
            attr_values[thresh_val] = {"N": str(amount_milli)}
            add_parts.append(f"{tk_alias} {neg_val}")
            add_parts.append(f"{tc_alias} {pos_val}")
            condition_parts.append(f"{tk_alias} >= {thresh_val}")
        wcu_tk_attr = schema.bucket_attr(schema.WCU_LIMIT_NAME, schema.BUCKET_FIELD_TK)
        wcu_tc_attr = schema.bucket_attr(schema.WCU_LIMIT_NAME, schema.BUCKET_FIELD_TC)
        attr_names["#wcu_tk"] = wcu_tk_attr
        attr_names["#wcu_tc"] = wcu_tc_attr
        wcu_milli = 1000
        attr_values[":neg_wcu"] = {"N": str(-wcu_milli)}
        attr_values[":pos_wcu"] = {"N": str(wcu_milli)}
        attr_values[":thresh_wcu"] = {"N": str(wcu_milli)}
        add_parts.append("#wcu_tk :neg_wcu")
        add_parts.append("#wcu_tc :pos_wcu")
        condition_parts.append("#wcu_tk >= :thresh_wcu")
        update_expr = "ADD " + ", ".join(add_parts)
        if ttl_seconds is not None:
            now_ms = self._now_ms()
            ttl_epoch = schema.calculate_ttl(now_ms, ttl_seconds)
            update_expr = f"SET #ttl = :ttl {update_expr}"
            attr_names["#ttl"] = "ttl"
            attr_values[":ttl"] = {"N": str(ttl_epoch)}
        now_epoch = self._now_ms() // 1000
        attr_names["#ttl"] = "ttl"
        attr_values[":now_epoch"] = {"N": str(now_epoch)}
        condition_parts.append("(attribute_not_exists(#ttl) OR #ttl > :now_epoch)")
        condition_expr = " AND ".join(condition_parts)
        try:
            response = client.update_item(
                TableName=self.table_name,
                Key={
                    "PK": {
                        "S": schema.pk_bucket(self._namespace_id, entity_id, resource, shard_id)
                    },
                    "SK": {"S": schema.sk_state()},
                },
                UpdateExpression=update_expr,
                ConditionExpression=condition_expr,
                ExpressionAttributeNames=attr_names,
                ExpressionAttributeValues=attr_values,
                ReturnValues="ALL_NEW",
                ReturnValuesOnConditionCheckFailure="ALL_OLD",
            )
            item = response["Attributes"]
            buckets = self._deserialize_composite_bucket(item)
            cascade = item.get("cascade", {}).get("BOOL", False)
            parent_id = item.get("parent_id", {}).get("S")
            shard_count = int(item.get("shard_count", {}).get("N", "1"))
            return SpeculativeResult(
                success=True,
                buckets=buckets,
                cascade=cascade,
                parent_id=parent_id,
                shard_id=shard_id,
                shard_count=shard_count,
            )
        except ClientError as e:
            if e.response.get("Error", {}).get("Code") == "ConditionalCheckFailedException":
                old_item = cast(dict[str, Any] | None, e.response.get("Item"))
                if old_item:
                    old_buckets = self._deserialize_composite_bucket(old_item)
                    old_shard_count = int(old_item.get("shard_count", {}).get("N", "1"))
                    wcu_exhausted = any(
                        b.limit_name == schema.WCU_LIMIT_NAME and b.tokens_milli < 1000
                        for b in old_buckets
                    )
                    app_exhausted = any(
                        b.limit_name != schema.WCU_LIMIT_NAME
                        and b.tokens_milli < consume.get(b.limit_name, 0) * 1000
                        for b in old_buckets
                    )
                    if wcu_exhausted and app_exhausted:
                        reason = SpeculativeFailureReason.BOTH_EXHAUSTED
                    elif wcu_exhausted:
                        reason = SpeculativeFailureReason.WCU_EXHAUSTED
                    else:
                        reason = SpeculativeFailureReason.APP_LIMIT_EXHAUSTED
                    return SpeculativeResult(
                        success=False,
                        old_buckets=old_buckets,
                        shard_id=shard_id,
                        shard_count=old_shard_count,
                        failure_reason=reason,
                    )
                else:
                    return SpeculativeResult(
                        success=False,
                        shard_id=shard_id,
                        failure_reason=SpeculativeFailureReason.BUCKET_MISSING,
                    )
            raise

    def bump_shard_count(self, entity_id: str, resource: str, current_count: int) -> int:
        """Double shard_count on shard 0 via conditional write.

        Shard 0 is the source of truth for shard_count. The conditional
        expression ``shard_count = :old`` prevents double-bumping when
        multiple clients race to double concurrently. Also updates the
        entity cache with the new shard_count.

        Args:
            entity_id: Entity owning the bucket.
            resource: Resource name.
            current_count: Current shard_count to double.

        Returns:
            The new shard_count (doubled), or the current value if another
            client already doubled (ConditionalCheckFailedException).
        """
        new_count = current_count * 2
        client = self._get_client()
        try:
            client.update_item(
                TableName=self.table_name,
                Key={
                    "PK": {"S": schema.pk_bucket(self._namespace_id, entity_id, resource, 0)},
                    "SK": {"S": schema.sk_state()},
                },
                UpdateExpression="SET shard_count = :new",
                ConditionExpression="shard_count = :old",
                ExpressionAttributeValues={
                    ":old": {"N": str(current_count)},
                    ":new": {"N": str(new_count)},
                },
            )
            effective_count = new_count
            if new_count > schema.WCU_SHARD_WARN_THRESHOLD:
                logger.warning(
                    "High shard count after doubling: entity_id=%s resource=%s shard_count=%d threshold=%d",
                    entity_id,
                    resource,
                    new_count,
                    schema.WCU_SHARD_WARN_THRESHOLD,
                )
        except ClientError as e:
            if e.response.get("Error", {}).get("Code") == "ConditionalCheckFailedException":
                effective_count = current_count
            else:
                raise
        cache_key = (self._namespace_id, entity_id)
        entry = self._entity_cache.get(cache_key, (False, None, {}))
        shards = {**entry[2], resource: effective_count}
        self._entity_cache[cache_key] = (entry[0], entry[1], shards)
        return effective_count

    def set_limits(
        self,
        entity_id: str,
        limits: list[Limit],
        resource: str = schema.DEFAULT_RESOURCE,
        principal: str | None = None,
    ) -> None:
        """
        Store limit configs for an entity (composite format, ADR-114).

        All limits for an entity+resource are stored in a single composite item
        with SK '#CONFIG#{resource}'. This reduces cache-miss cost from N GetItem
        calls to 1 GetItem call.

        Args:
            entity_id: ID of the entity
            limits: List of Limit configurations to store
            resource: Resource name (defaults to "_default_")
            principal: Caller identity for audit logging
        """
        client = self._get_client()
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
            "SK": {"S": schema.sk_config(resource)},
            "entity_id": {"S": entity_id},
            "resource": {"S": resource},
            "config_version": {"N": "1"},
            "GSI3PK": {"S": schema.gsi3_pk_entity_config(self._namespace_id, resource)},
            "GSI3SK": {"S": schema.gsi3_sk_entity(entity_id)},
            "GSI4PK": {"S": self._namespace_id},
            "GSI4SK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
        }
        self._serialize_composite_limits(limits, item)
        try:
            client.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": self.table_name,
                            "Item": item,
                            "ConditionExpression": "attribute_not_exists(PK)",
                        }
                    },
                    {
                        "Update": {
                            "TableName": self.table_name,
                            "Key": {
                                "PK": {"S": schema.pk_system(self._namespace_id)},
                                "SK": {"S": schema.sk_entity_config_resources()},
                            },
                            "UpdateExpression": "SET GSI4PK = if_not_exists(GSI4PK, :gsi4pk), GSI4SK = if_not_exists(GSI4SK, :gsi4sk) ADD #resource :one",
                            "ExpressionAttributeNames": {"#resource": resource},
                            "ExpressionAttributeValues": {
                                ":one": {"N": "1"},
                                ":gsi4pk": {"S": self._namespace_id},
                                ":gsi4sk": {"S": schema.pk_system(self._namespace_id)},
                            },
                        }
                    },
                ]
            )
        except ClientError as e:
            if e.response["Error"]["Code"] == "TransactionCanceledException":
                reasons = e.response.get("CancellationReasons", [])
                if reasons and reasons[0].get("Code") == "ConditionalCheckFailed":
                    client.put_item(TableName=self.table_name, Item=item)
                else:
                    raise
            else:
                raise
        self._sync_bucket_params(entity_id, resource, limits, bucket_ttl_refill_multiplier=0)
        self._config_cache.evict_entity(entity_id, resource)
        self._log_audit_event(
            action=AuditAction.LIMITS_SET,
            entity_id=entity_id,
            principal=principal,
            resource=resource,
            details={"limits": [limit.to_dict() for limit in limits]},
        )

    def _sync_bucket_params(
        self,
        entity_id: str,
        resource: str,
        limits: list[Limit],
        bucket_ttl_refill_multiplier: int | None = None,
        stale_limit_names: set[str] | None = None,
    ) -> None:
        """Sync bucket static params when limits change (issue #294, #327).

        Updates capacity, refill_amount, refill_period, and TTL for
        existing buckets. Optionally removes stale limit attributes.
        Uses conditional update with attribute_exists(PK) to skip if
        bucket doesn't exist yet.

        Args:
            entity_id: ID of the entity
            resource: Resource name
            limits: New limit configurations
            bucket_ttl_refill_multiplier: TTL behavior (issue #327):
                - None: Don't change TTL
                - 0: REMOVE ttl (entity has custom limits)
                - >0: SET ttl to (now + calculated_seconds)
            stale_limit_names: Limit names to REMOVE from bucket (issue #327).
                Used when downgrading from entity config to defaults where
                the old config had limits not present in the new defaults.
        """
        if not limits:
            return
        client = self._get_client()
        set_parts: list[str] = []
        remove_parts: list[str] = []
        expr_names: dict[str, str] = {}
        expr_values: dict[str, dict[str, str]] = {}
        for i, limit in enumerate(limits):
            name = limit.name
            cp_attr = schema.bucket_attr(name, schema.BUCKET_FIELD_CP)
            set_parts.append(f"#cp{i} = :cp{i}")
            expr_names[f"#cp{i}"] = cp_attr
            expr_values[f":cp{i}"] = {"N": str(limit.capacity * 1000)}
            ra_attr = schema.bucket_attr(name, schema.BUCKET_FIELD_RA)
            set_parts.append(f"#ra{i} = :ra{i}")
            expr_names[f"#ra{i}"] = ra_attr
            expr_values[f":ra{i}"] = {"N": str(limit.refill_amount * 1000)}
            rp_attr = schema.bucket_attr(name, schema.BUCKET_FIELD_RP)
            set_parts.append(f"#rp{i} = :rp{i}")
            expr_names[f"#rp{i}"] = rp_attr
            expr_values[f":rp{i}"] = {"N": str(limit.refill_period_seconds * 1000)}
        if bucket_ttl_refill_multiplier is not None:
            expr_names["#ttl"] = "ttl"
            if bucket_ttl_refill_multiplier > 0:
                ttl_seconds = schema.calculate_bucket_ttl_seconds(
                    limits, bucket_ttl_refill_multiplier
                )
                if ttl_seconds is not None:
                    now_ms = self._now_ms()
                    set_parts.append("#ttl = :ttl_val")
                    expr_values[":ttl_val"] = {"N": str(schema.calculate_ttl(now_ms, ttl_seconds))}
            else:
                remove_parts.append("#ttl")
        if stale_limit_names:
            for stale_name in stale_limit_names:
                for field in (
                    schema.BUCKET_FIELD_TK,
                    schema.BUCKET_FIELD_CP,
                    schema.BUCKET_FIELD_RA,
                    schema.BUCKET_FIELD_RP,
                    schema.BUCKET_FIELD_TC,
                ):
                    alias = f"#stale_{stale_name}_{field}"
                    expr_names[alias] = schema.bucket_attr(stale_name, field)
                    remove_parts.append(alias)
        update_expr = f"SET {', '.join(set_parts)}"
        if remove_parts:
            update_expr += f" REMOVE {', '.join(remove_parts)}"
        try:
            client.update_item(
                TableName=self.table_name,
                Key={
                    "PK": {"S": schema.pk_bucket(self._namespace_id, entity_id, resource, 0)},
                    "SK": {"S": schema.sk_state()},
                },
                UpdateExpression=update_expr,
                ConditionExpression="attribute_exists(PK)",
                ExpressionAttributeNames=expr_names,
                ExpressionAttributeValues=expr_values,
            )
        except ClientError as e:
            if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
                return
            raise

    def reconcile_bucket_to_defaults(
        self,
        entity_id: str,
        resource: str,
        effective_limits: list[Limit],
        stale_limit_names: set[str] | None = None,
    ) -> None:
        """Reconcile bucket to effective defaults after config deletion (issue #327).

        Updates limit fields (cp, ra, rp) to match the new effective
        limits, sets TTL (since entity is now on defaults), and removes
        stale limit attributes that no longer exist in the effective config.

        No-op if bucket doesn't exist (uses attribute_exists(PK) condition).

        Args:
            entity_id: ID of the entity
            resource: Resource name
            effective_limits: The new effective limits (resource/system defaults)
            stale_limit_names: Limit names to REMOVE from bucket (limits that
                were in the deleted entity config but not in effective defaults)
        """
        self._sync_bucket_params(
            entity_id,
            resource,
            effective_limits,
            bucket_ttl_refill_multiplier=self._bucket_ttl_refill_multiplier,
            stale_limit_names=stale_limit_names,
        )

    def _cleanup_entity_config_registry(self, resource: str) -> None:
        """Remove resource from entity config registry if count <= 0.

        Called after decrementing to clean up zero/negative counts.
        Uses conditional REMOVE to avoid race conditions.
        """
        client = self._get_client()
        try:
            client.update_item(
                TableName=self.table_name,
                Key={
                    "PK": {"S": schema.pk_system(self._namespace_id)},
                    "SK": {"S": schema.sk_entity_config_resources()},
                },
                UpdateExpression="REMOVE #resource",
                ConditionExpression="#resource <= :zero",
                ExpressionAttributeNames={"#resource": resource},
                ExpressionAttributeValues={":zero": {"N": "0"}},
            )
        except ClientError as e:
            if e.response["Error"]["Code"] != "ConditionalCheckFailedException":
                raise

    def get_limits(self, entity_id: str, resource: str = schema.DEFAULT_RESOURCE) -> list[Limit]:
        """Get stored limit configs for an entity (composite format, ADR-114).

        Reads a single composite item with SK '#CONFIG#{resource}' containing
        all limits for the entity+resource pair.
        """
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
                "SK": {"S": schema.sk_config(resource)},
            },
            ConsistentRead=False,
        )
        item = response.get("Item")
        if not item:
            return []
        return self._deserialize_composite_limits(item)

    def delete_limits(
        self, entity_id: str, resource: str = schema.DEFAULT_RESOURCE, principal: str | None = None
    ) -> None:
        """
        Delete stored limit configs for an entity (composite format, ADR-114).

        Deletes the single composite config item for this entity+resource.

        Args:
            entity_id: ID of the entity
            resource: Resource name (defaults to "_default_")
            principal: Caller identity for audit logging
        """
        client = self._get_client()
        try:
            client.transact_write_items(
                TransactItems=[
                    {
                        "Delete": {
                            "TableName": self.table_name,
                            "Key": {
                                "PK": {"S": schema.pk_entity(self._namespace_id, entity_id)},
                                "SK": {"S": schema.sk_config(resource)},
                            },
                            "ConditionExpression": "attribute_exists(PK)",
                        }
                    },
                    {
                        "Update": {
                            "TableName": self.table_name,
                            "Key": {
                                "PK": {"S": schema.pk_system(self._namespace_id)},
                                "SK": {"S": schema.sk_entity_config_resources()},
                            },
                            "UpdateExpression": "ADD #resource :minus_one",
                            "ExpressionAttributeNames": {"#resource": resource},
                            "ExpressionAttributeValues": {":minus_one": {"N": "-1"}},
                        }
                    },
                ]
            )
        except ClientError as e:
            if e.response["Error"]["Code"] == "TransactionCanceledException":
                reasons = e.response.get("CancellationReasons", [])
                if reasons and reasons[0].get("Code") == "ConditionalCheckFailed":
                    return
                else:
                    raise
            else:
                raise
        self._cleanup_entity_config_registry(resource)
        self._config_cache.evict_entity(entity_id, resource)
        self._log_audit_event(
            action=AuditAction.LIMITS_DELETED,
            entity_id=entity_id,
            principal=principal,
            resource=resource,
        )

    def list_entities_with_custom_limits(
        self, resource: str, limit: int | None = None, cursor: str | None = None
    ) -> tuple[list[str], str | None]:
        """
        List all entities that have custom limit configurations for a resource.

        Uses GSI3 sparse index for efficient queries. Only entity-level configs
        have GSI3 attributes, so this query returns only entities with custom
        limits (not system or resource defaults).

        Args:
            resource: Resource to filter by (required).
            limit: Maximum number of entities to return. None for all.
            cursor: Pagination cursor from previous call. None for first page.

        Returns:
            Tuple of (entity_ids, next_cursor). next_cursor is None if no more results.
        """
        import base64
        import json

        client = self._get_client()
        query_params: dict[str, Any] = {
            "TableName": self.table_name,
            "IndexName": schema.GSI3_NAME,
            "KeyConditionExpression": "GSI3PK = :pk",
            "ExpressionAttributeValues": {
                ":pk": {"S": schema.gsi3_pk_entity_config(self._namespace_id, resource)}
            },
        }
        if limit is not None:
            query_params["Limit"] = limit
        if cursor is not None:
            query_params["ExclusiveStartKey"] = json.loads(base64.b64decode(cursor))
        response = client.query(**query_params)
        entity_ids: list[str] = []
        for item in response.get("Items", []):
            entity_id = item.get("GSI3SK", {}).get("S")
            if entity_id:
                entity_ids.append(entity_id)
        next_cursor: str | None = None
        if "LastEvaluatedKey" in response:
            next_cursor = base64.b64encode(
                json.dumps(response["LastEvaluatedKey"]).encode()
            ).decode()
        return (entity_ids, next_cursor)

    def list_resources_with_entity_configs(self) -> list[str]:
        """
        List all resources that have entity-level custom limit configs.

        Uses the entity config resources registry (wide column with ref counts)
        for efficient O(1) lookup. Returns resources with count > 0.

        Returns:
            Sorted list of resource names with at least one entity having custom limits
        """
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_entity_config_resources()},
            },
            ConsistentRead=False,
        )
        item = response.get("Item", {})
        resources = []
        for attr_name, attr_value in item.items():
            if attr_name in ("PK", "SK"):
                continue
            count_str = attr_value.get("N")
            if count_str is not None and int(count_str) > 0:
                resources.append(attr_name)
        return sorted(resources)

    def set_resource_defaults(
        self, resource: str, limits: list[Limit], principal: str | None = None
    ) -> None:
        """
        Store default limit configs for a resource (composite format, ADR-114).

        All limits for a resource are stored in a single composite item
        with SK '#CONFIG'. This reduces cache-miss cost.

        Args:
            resource: Resource name
            limits: List of Limit configurations to store
            principal: Caller identity for audit logging
        """
        validate_resource(resource)
        client = self._get_client()
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_resource(self._namespace_id, resource)},
            "SK": {"S": schema.sk_config()},
            "resource": {"S": resource},
            "config_version": {"N": "1"},
            "GSI4PK": {"S": self._namespace_id},
            "GSI4SK": {"S": schema.pk_resource(self._namespace_id, resource)},
        }
        self._serialize_composite_limits(limits, item)
        client.put_item(TableName=self.table_name, Item=item)
        client.update_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_resources()},
            },
            UpdateExpression="SET GSI4PK = if_not_exists(GSI4PK, :gsi4pk), GSI4SK = if_not_exists(GSI4SK, :gsi4sk) ADD resources :resource",
            ExpressionAttributeValues={
                ":resource": {"SS": [resource]},
                ":gsi4pk": {"S": self._namespace_id},
                ":gsi4sk": {"S": schema.pk_system(self._namespace_id)},
            },
        )
        self._log_audit_event(
            action=AuditAction.LIMITS_SET,
            entity_id=f"$RESOURCE:{resource}",
            principal=principal,
            resource=resource,
            details={"limits": [limit.to_dict() for limit in limits]},
        )

    def get_resource_defaults(self, resource: str) -> list[Limit]:
        """Get stored default limit configs for a resource (composite format, ADR-114).

        Reads a single composite item with SK '#CONFIG' containing all limits
        for the resource.
        """
        validate_resource(resource)
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_resource(self._namespace_id, resource)},
                "SK": {"S": schema.sk_config()},
            },
            ConsistentRead=False,
        )
        item = response.get("Item")
        if not item:
            return []
        return self._deserialize_composite_limits(item)

    def delete_resource_defaults(self, resource: str, principal: str | None = None) -> None:
        """
        Delete stored default limit configs for a resource (composite format, ADR-114).

        Deletes the single composite config item for this resource.

        Args:
            resource: Resource name
            principal: Caller identity for audit logging
        """
        validate_resource(resource)
        client = self._get_client()
        client.delete_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_resource(self._namespace_id, resource)},
                "SK": {"S": schema.sk_config()},
            },
        )
        client.update_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_resources()},
            },
            UpdateExpression="DELETE resources :resource",
            ExpressionAttributeValues={":resource": {"SS": [resource]}},
        )
        self._log_audit_event(
            action=AuditAction.LIMITS_DELETED,
            entity_id=f"$RESOURCE:{resource}",
            principal=principal,
            resource=resource,
        )

    def list_resources_with_defaults(self) -> list[str]:
        """List all resources that have default limit configs from the resource registry."""
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_resources()},
            },
            ConsistentRead=False,
        )
        item = response.get("Item", {})
        resources_set = item.get("resources", {}).get("SS", [])
        return sorted(resources_set)

    def set_system_defaults(
        self,
        limits: list[Limit],
        on_unavailable: OnUnavailableAction | None = None,
        principal: str | None = None,
    ) -> None:
        """
        Store system-wide default limits and config (composite format, ADR-114).

        All system limits and config (on_unavailable) are stored in a single
        composite item with SK '#CONFIG'. This reduces cache-miss cost.

        Args:
            limits: List of Limit configurations (apply to all resources)
            on_unavailable: Behavior when DynamoDB unavailable ("allow" or "block")
            principal: Caller identity for audit logging
        """
        client = self._get_client()
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_system(self._namespace_id)},
            "SK": {"S": schema.sk_config()},
            "config_version": {"N": "1"},
        }
        if on_unavailable is not None:
            item["on_unavailable"] = {"S": on_unavailable}
        self._serialize_composite_limits(limits, item)
        item["GSI4PK"] = {"S": self._namespace_id}
        item["GSI4SK"] = {"S": schema.pk_system(self._namespace_id)}
        client.put_item(TableName=self.table_name, Item=item)
        self._log_audit_event(
            action=AuditAction.LIMITS_SET,
            entity_id="$SYSTEM",
            principal=principal,
            details={
                "limits": [limit.to_dict() for limit in limits],
                "on_unavailable": on_unavailable,
            },
        )

    def get_system_defaults(self) -> tuple[list[Limit], OnUnavailableAction | None]:
        """
        Get system-wide default limits and config (composite format, ADR-114).

        Reads a single composite item with SK '#CONFIG' containing all limits
        and on_unavailable setting.

        Returns:
            Tuple of (limits, on_unavailable). on_unavailable may be None if not set.
        """
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_config()},
            },
            ConsistentRead=False,
        )
        item = response.get("Item")
        if not item:
            return ([], None)
        limits = self._deserialize_composite_limits(item)
        on_unavailable_attr = item.get("on_unavailable", {})
        on_unavailable: OnUnavailableAction | None = (
            cast(OnUnavailableAction, on_unavailable_attr.get("S"))
            if on_unavailable_attr and on_unavailable_attr.get("S")
            else None
        )
        return (limits, on_unavailable)

    def delete_system_defaults(self, principal: str | None = None) -> None:
        """
        Delete all system-wide default limits and config (composite format, ADR-114).

        Deletes the single composite config item for system defaults.

        Args:
            principal: Caller identity for audit logging
        """
        client = self._get_client()
        limits, on_unavailable = self.get_system_defaults()
        client.delete_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_config()},
            },
        )
        self._log_audit_event(
            action=AuditAction.LIMITS_DELETED,
            entity_id="$SYSTEM",
            principal=principal,
            details={"limits": [limit.name for limit in limits], "on_unavailable": on_unavailable},
        )

    def get_system_limits(self) -> list[Limit]:
        """Get system-wide default limits (without config).

        This is a convenience method that returns only the limits.
        Use get_system_defaults() to also get on_unavailable config.
        """
        limits, _ = self.get_system_defaults()
        return limits

    def get_provisioner_state(self) -> dict[str, Any]:
        """Get the provisioner state record for this namespace.

        Returns:
            Dict with keys: managed_system, managed_resources, managed_entities,
            last_applied, applied_hash. Returns empty state if no record exists.
        """
        client = self._get_client()
        result = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_provisioner()},
            },
        )
        item = result.get("Item")
        if not item:
            return {
                "managed_system": False,
                "managed_resources": [],
                "managed_entities": {},
                "last_applied": None,
                "applied_hash": None,
            }
        managed_entities: dict[str, list[str]] = {}
        raw_entities = item.get("managed_entities", {}).get("M", {})
        for entity_id, resources_attr in raw_entities.items():
            managed_entities[entity_id] = [r["S"] for r in resources_attr.get("L", [])]
        return {
            "managed_system": item.get("managed_system", {}).get("BOOL", False),
            "managed_resources": [r["S"] for r in item.get("managed_resources", {}).get("L", [])],
            "managed_entities": managed_entities,
            "last_applied": item.get("last_applied", {}).get("S"),
            "applied_hash": item.get("applied_hash", {}).get("S"),
        }

    def put_provisioner_state(self, state: dict[str, Any]) -> None:
        """Write the provisioner state record for this namespace.

        Args:
            state: Dict with keys: managed_system, managed_resources,
                   managed_entities, last_applied, applied_hash.
        """
        client = self._get_client()
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_system(self._namespace_id)},
            "SK": {"S": schema.sk_provisioner()},
            "GSI4PK": {"S": self._namespace_id},
            "managed_system": {"BOOL": state["managed_system"]},
            "managed_resources": {"L": [{"S": r} for r in state["managed_resources"]]},
            "managed_entities": {
                "M": {
                    entity_id: {"L": [{"S": r} for r in resources]}
                    for entity_id, resources in state["managed_entities"].items()
                }
            },
            "last_applied": {"S": state["last_applied"]},
            "applied_hash": {"S": state["applied_hash"]},
        }
        client.put_item(TableName=self.table_name, Item=item)

    def _write_audit_retention_config(self) -> None:
        """
        Write audit_retention_days to system config item.

        Uses atomic UpdateItem to avoid overwriting other system config fields.
        Called from ensure_infrastructure() after stack creation.
        """
        if self._stack_options is None:
            return
        client = self._get_client()
        client.update_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_config()},
            },
            UpdateExpression="SET audit_retention_days = :ard",
            ExpressionAttributeValues={
                ":ard": {"N": str(self._stack_options.audit_retention_days)}
            },
        )
        self._audit_retention_days_cache = self._stack_options.audit_retention_days

    def _get_audit_retention_days(self) -> int:
        """
        Get audit retention days from system config or default.

        Returns cached value if available, otherwise reads from DynamoDB.
        Falls back to stack_options if available (saves DynamoDB call).
        Default is 90 days if not configured anywhere.
        """
        if self._audit_retention_days_cache is not None:
            return self._audit_retention_days_cache
        if self._stack_options is not None:
            self._audit_retention_days_cache = self._stack_options.audit_retention_days
            return self._audit_retention_days_cache
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(self._namespace_id)},
                "SK": {"S": schema.sk_config()},
            },
            ConsistentRead=False,
        )
        item = response.get("Item", {})
        ard = item.get("audit_retention_days", {}).get("N")
        self._audit_retention_days_cache = int(ard) if ard else 90
        return self._audit_retention_days_cache

    def get_version_record(self) -> dict[str, Any] | None:
        """
        Get the infrastructure version record.

        Returns:
            Version record with schema_version, lambda_version, etc.
            None if no version record exists.
        """
        client = self._get_client()
        response = client.get_item(
            TableName=self.table_name,
            Key={
                "PK": {"S": schema.pk_system(schema.RESERVED_NAMESPACE)},
                "SK": {"S": schema.sk_version()},
            },
        )
        item = response.get("Item")
        if not item:
            return None
        result: dict[str, Any] = {}
        for key in (
            "schema_version",
            "lambda_version",
            "client_min_version",
            "updated_at",
            "updated_by",
        ):
            if key in item:
                result[key] = self._deserialize_value(item[key])
        return result

    def ping(self) -> bool:
        """
        Check if the DynamoDB table is reachable.

        Performs a lightweight GetItem operation to verify connectivity.
        Does not verify the table is initialized or has valid data.

        Returns:
            True if the table is reachable, False otherwise.
        """
        try:
            client = self._get_client()
            client.get_item(
                TableName=self.table_name,
                Key={
                    "PK": {"S": schema.pk_system(schema.RESERVED_NAMESPACE)},
                    "SK": {"S": schema.sk_version()},
                },
            )
            return True
        except Exception:
            return False

    def set_version_record(
        self,
        schema_version: str,
        lambda_version: str | None = None,
        client_min_version: str = "0.0.0",
        updated_by: str | None = None,
    ) -> None:
        """
        Set the infrastructure version record.

        Args:
            schema_version: Current schema version (e.g., "1.0.0")
            lambda_version: Currently deployed Lambda version
            client_min_version: Minimum compatible client version
            updated_by: Identifier of what performed the update
        """
        client = self._get_client()
        now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_system(schema.RESERVED_NAMESPACE)},
            "SK": {"S": schema.sk_version()},
            "schema_version": {"S": schema_version},
            "client_min_version": {"S": client_min_version},
            "updated_at": {"S": now},
            "lambda_version": {"S": lambda_version} if lambda_version else {"NULL": True},
            "updated_by": {"S": updated_by} if updated_by else {"NULL": True},
            "GSI4PK": {"S": schema.RESERVED_NAMESPACE},
            "GSI4SK": {"S": schema.pk_system(schema.RESERVED_NAMESPACE)},
        }
        client.put_item(TableName=self.table_name, Item=item)

    def _generate_event_id(self) -> str:
        """Generate a unique event ID using ULID (monotonic, collision-free)."""
        return str(ULID())

    def _log_audit_event(
        self,
        action: str,
        entity_id: str,
        principal: str | None = None,
        resource: str | None = None,
        details: dict[str, Any] | None = None,
    ) -> AuditEvent:
        """
        Log an audit event to DynamoDB.

        Args:
            action: Type of action (see AuditAction constants)
            entity_id: ID of the entity affected
            principal: Caller identity who performed the action. If None,
                auto-detects from AWS STS caller identity (lazy cached).
            resource: Resource name for limit-related actions
            details: Additional action-specific details

        Returns:
            The created AuditEvent

        Raises:
            InvalidIdentifierError: If principal is invalid
        """
        if principal is None:
            principal = self._get_caller_identity_arn()
        if principal is not None and (not principal.startswith("arn:")):
            validate_identifier(principal, "principal")
        audit_retention_days = self._get_audit_retention_days()
        ttl_seconds = audit_retention_days * 86400
        client = self._get_client()
        now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        event_id = self._generate_event_id()
        event = AuditEvent(
            event_id=event_id,
            timestamp=now,
            action=action,
            entity_id=entity_id,
            principal=principal,
            resource=resource,
            details=details or {},
        )
        item: dict[str, Any] = {
            "PK": {"S": schema.pk_audit(self._namespace_id, entity_id)},
            "SK": {"S": schema.sk_audit(event_id)},
            "entity_id": {"S": entity_id},
            "event_id": {"S": event_id},
            "timestamp": {"S": now},
            "action": {"S": action},
            "principal": {"S": principal} if principal else {"NULL": True},
            "resource": {"S": resource} if resource else {"NULL": True},
            "details": {"M": self._serialize_map(details or {})},
            "ttl": {"N": str(schema.calculate_ttl(self._now_ms(), ttl_seconds))},
            "GSI4PK": {"S": self._namespace_id},
            "GSI4SK": {"S": schema.pk_audit(self._namespace_id, entity_id)},
        }
        client.put_item(TableName=self.table_name, Item=item)
        return event

    def get_audit_events(
        self, entity_id: str, limit: int = 100, start_event_id: str | None = None
    ) -> list[AuditEvent]:
        """
        Get audit events for an entity.

        Args:
            entity_id: ID of the entity to query
            limit: Maximum number of events to return
            start_event_id: Event ID to start after (for pagination)

        Returns:
            List of AuditEvent objects, ordered by most recent first
        """
        client = self._get_client()
        query_args: dict[str, Any] = {
            "TableName": self.table_name,
            "KeyConditionExpression": "PK = :pk AND begins_with(SK, :sk_prefix)",
            "ExpressionAttributeValues": {
                ":pk": {"S": schema.pk_audit(self._namespace_id, entity_id)},
                ":sk_prefix": {"S": schema.SK_AUDIT},
            },
            "ScanIndexForward": False,
            "Limit": limit,
        }
        if start_event_id:
            query_args["ExclusiveStartKey"] = {
                "PK": {"S": schema.pk_audit(self._namespace_id, entity_id)},
                "SK": {"S": schema.sk_audit(start_event_id)},
            }
        response = client.query(**query_args)
        events = []
        for item in response.get("Items", []):
            event = self._deserialize_audit_event(item)
            if event:
                events.append(event)
        return events

    def _deserialize_audit_event(self, item: dict[str, Any]) -> AuditEvent | None:
        """Deserialize a DynamoDB item to AuditEvent (flat format only)."""
        if "action" not in item or "S" not in item.get("action", {}):
            return None
        details_raw = item.get("details", {})
        details = self._deserialize_map(details_raw.get("M", {})) if "M" in details_raw else {}
        principal = self._deserialize_value(item["principal"]) if "principal" in item else None
        resource = self._deserialize_value(item["resource"]) if "resource" in item else None
        return AuditEvent(
            event_id=item.get("event_id", {}).get("S", ""),
            timestamp=item.get("timestamp", {}).get("S", ""),
            action=item["action"]["S"],
            entity_id=item.get("entity_id", {}).get("S", ""),
            principal=principal,
            resource=resource,
            details=details,
        )

    def get_usage_snapshots(
        self,
        entity_id: str | None = None,
        resource: str | None = None,
        window_type: str | None = None,
        start_time: str | None = None,
        end_time: str | None = None,
        limit: int = 100,
        next_key: dict[str, Any] | None = None,
    ) -> tuple[list[UsageSnapshot], dict[str, Any] | None]:
        """
        Query usage snapshots with filtering and pagination.

        Supports two query modes:
        1. Entity-scoped: Query by entity_id (uses primary key)
        2. Resource-scoped: Query by resource across all entities (uses GSI2)

        Args:
            entity_id: Entity to query (mutually exclusive for efficient queries)
            resource: Resource name filter (required if entity_id is None)
            window_type: Filter by window type ("hourly", "daily")
            start_time: Filter snapshots >= this timestamp (ISO format)
            end_time: Filter snapshots <= this timestamp (ISO format)
            limit: Maximum items to fetch from DynamoDB per page (default: 100)
            next_key: Pagination cursor from previous call

        Returns:
            Tuple of (snapshots, next_key). next_key is None if no more results.

        Raises:
            ValueError: If neither entity_id nor resource is provided

        Note:
            The ``limit`` parameter controls the DynamoDB query batch size.
            Client-side filters (window_type, start_time, end_time) are applied
            after fetching, so the returned count may be less than ``limit``.
            Use ``next_key`` to paginate through all matching results.
        """
        if entity_id is None and resource is None:
            raise ValueError("Either entity_id or resource must be provided")
        client = self._get_client()
        snapshots: list[UsageSnapshot] = []
        if entity_id is not None:
            key_condition = "PK = :pk AND begins_with(SK, :sk_prefix)"
            expression_values: dict[str, Any] = {
                ":pk": {"S": schema.pk_entity(self._namespace_id, entity_id)},
                ":sk_prefix": {"S": schema.SK_USAGE},
            }
            if resource:
                expression_values[":sk_prefix"] = {"S": f"{schema.SK_USAGE}{resource}#"}
            query_args: dict[str, Any] = {
                "TableName": self.table_name,
                "KeyConditionExpression": key_condition,
                "ExpressionAttributeValues": expression_values,
                "ScanIndexForward": False,
                "Limit": limit,
            }
            if next_key:
                query_args["ExclusiveStartKey"] = next_key
            response = client.query(**query_args)
        elif resource is not None:
            key_condition = "GSI2PK = :pk AND begins_with(GSI2SK, :sk_prefix)"
            expression_values = {
                ":pk": {"S": schema.gsi2_pk_resource(self._namespace_id, resource)},
                ":sk_prefix": {"S": "USAGE#"},
            }
            query_args = {
                "TableName": self.table_name,
                "IndexName": schema.GSI2_NAME,
                "KeyConditionExpression": key_condition,
                "ExpressionAttributeValues": expression_values,
                "ScanIndexForward": False,
                "Limit": limit,
            }
            if next_key:
                query_args["ExclusiveStartKey"] = next_key
            response = client.query(**query_args)
        else:
            raise ValueError("Either entity_id or resource must be provided")
        for item in response.get("Items", []):
            snapshot = self._deserialize_usage_snapshot(item)
            if snapshot is None:
                continue
            if window_type and snapshot.window_type != window_type:
                continue
            if start_time and snapshot.window_start < start_time:
                continue
            if end_time and snapshot.window_start > end_time:
                continue
            snapshots.append(snapshot)
        returned_next_key = response.get("LastEvaluatedKey")
        return (snapshots, returned_next_key)

    def get_usage_summary(
        self,
        entity_id: str | None = None,
        resource: str | None = None,
        window_type: str | None = None,
        start_time: str | None = None,
        end_time: str | None = None,
    ) -> UsageSummary:
        """
        Aggregate usage across snapshots into a summary.

        Fetches all matching snapshots (auto-paginates internally) and computes:
        - Total consumption per limit type
        - Average consumption per snapshot per limit type
        - Time range of aggregated data

        Args:
            entity_id: Entity to query
            resource: Resource name filter
            window_type: Filter by window type ("hourly", "daily")
            start_time: Filter snapshots >= this timestamp (ISO format)
            end_time: Filter snapshots <= this timestamp (ISO format)

        Returns:
            UsageSummary with aggregated statistics

        Raises:
            ValueError: If neither entity_id nor resource is provided
        """
        all_snapshots: list[UsageSnapshot] = []
        next_key: dict[str, Any] | None = None
        while True:
            snapshots, next_key = self.get_usage_snapshots(
                entity_id=entity_id,
                resource=resource,
                window_type=window_type,
                start_time=start_time,
                end_time=end_time,
                limit=1000,
                next_key=next_key,
            )
            all_snapshots.extend(snapshots)
            if next_key is None:
                break
            if len(all_snapshots) >= 10000:
                break
        total: dict[str, int] = {}
        counts: dict[str, int] = {}
        min_window: str | None = None
        max_window: str | None = None
        for snapshot in all_snapshots:
            if min_window is None or snapshot.window_start < min_window:
                min_window = snapshot.window_start
            if max_window is None or snapshot.window_start > max_window:
                max_window = snapshot.window_start
            for limit_name, value in snapshot.counters.items():
                total[limit_name] = total.get(limit_name, 0) + value
                counts[limit_name] = counts.get(limit_name, 0) + 1
        average: dict[str, float] = {}
        for limit_name, sum_value in total.items():
            count = counts.get(limit_name, 1)
            average[limit_name] = sum_value / count if count > 0 else 0.0
        return UsageSummary(
            snapshot_count=len(all_snapshots),
            total=total,
            average=average,
            min_window_start=min_window,
            max_window_start=max_window,
        )

    def _deserialize_usage_snapshot(self, item: dict[str, Any]) -> UsageSnapshot | None:
        """
        Deserialize a DynamoDB item to UsageSnapshot.

        Snapshots use FLAT schema (no nested data.M) to support atomic ADD
        operations. See issue #168.
        """
        entity_id = item.get("entity_id", {}).get("S", "")
        resource = item.get("resource", {}).get("S", "")
        window_type = item.get("window", {}).get("S", "")
        window_start = item.get("window_start", {}).get("S", "")
        if not entity_id or not resource or (not window_start):
            return None
        window_end = self._calculate_window_end(window_start, window_type)
        total_events = int(item.get("total_events", {}).get("N", "0"))
        excluded_keys = {
            "PK",
            "SK",
            "entity_id",
            "resource",
            "window",
            "window_start",
            "total_events",
            "GSI2PK",
            "GSI2SK",
            "ttl",
        }
        counters: dict[str, int] = {}
        for key, value in item.items():
            if key in excluded_keys:
                continue
            if "N" in value:
                counters[key] = int(value["N"])
        return UsageSnapshot(
            entity_id=entity_id,
            resource=resource,
            window_start=window_start,
            window_end=window_end,
            window_type=window_type,
            counters=counters,
            total_events=total_events,
        )

    def _calculate_window_end(self, window_start: str, window_type: str) -> str:
        """Calculate window end timestamp from start and type."""
        from datetime import datetime, timedelta

        try:
            dt = datetime.fromisoformat(window_start.replace("Z", "+00:00"))
            if window_type == "hourly":
                end_dt = dt.replace(minute=59, second=59, microsecond=999999)
            elif window_type == "daily":
                end_dt = dt.replace(hour=23, minute=59, second=59, microsecond=999999)
            elif window_type == "monthly":
                if dt.month == 12:
                    end_dt = dt.replace(year=dt.year + 1, month=1, day=1) - timedelta(seconds=1)
                else:
                    end_dt = dt.replace(month=dt.month + 1, day=1) - timedelta(seconds=1)
            else:
                end_dt = dt
            return end_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
        except (ValueError, AttributeError):
            return window_start

    def get_resource_buckets(
        self, resource: str, limit_name: str | None = None
    ) -> list[BucketState]:
        """Get all buckets for a resource across all entities.

        With composite items, each GSI2 entry is one composite item per
        entity. Returns individual BucketStates, optionally filtered by limit_name.
        """
        client = self._get_client()
        key_condition = "GSI2PK = :pk AND begins_with(GSI2SK, :sk_prefix)"
        expression_values: dict[str, Any] = {
            ":pk": {"S": schema.gsi2_pk_resource(self._namespace_id, resource)},
            ":sk_prefix": {"S": "BUCKET#"},
        }
        response = client.query(
            TableName=self.table_name,
            IndexName=schema.GSI2_NAME,
            KeyConditionExpression=key_condition,
            ExpressionAttributeValues=expression_values,
        )
        buckets: list[BucketState] = []
        for item in response.get("Items", []):
            for bucket in self._deserialize_composite_bucket(item):
                if limit_name is None or bucket.limit_name == limit_name:
                    buckets.append(bucket)
        return buckets

    def _serialize_map(self, data: dict[str, Any]) -> dict[str, Any]:
        """Serialize a Python dict to DynamoDB map format."""
        result: dict[str, Any] = {}
        for key, value in data.items():
            if isinstance(value, str):
                result[key] = {"S": value}
            elif isinstance(value, bool):
                result[key] = {"BOOL": value}
            elif isinstance(value, int | float):
                result[key] = {"N": str(value)}
            elif isinstance(value, dict):
                result[key] = {"M": self._serialize_map(value)}
            elif isinstance(value, list):
                result[key] = {"L": [self._serialize_value(v) for v in value]}
            elif value is None:
                result[key] = {"NULL": True}
        return result

    def _serialize_value(self, value: Any) -> dict[str, Any]:
        """Serialize a single value to DynamoDB format."""
        if isinstance(value, str):
            return {"S": value}
        elif isinstance(value, bool):
            return {"BOOL": value}
        elif isinstance(value, int | float):
            return {"N": str(value)}
        elif isinstance(value, dict):
            return {"M": self._serialize_map(value)}
        elif isinstance(value, list):
            return {"L": [self._serialize_value(v) for v in value]}
        elif value is None:
            return {"NULL": True}
        return {"S": str(value)}

    def _deserialize_map(self, data: dict[str, Any]) -> dict[str, Any]:
        """Deserialize a DynamoDB map to Python dict."""
        result = {}
        for key, value in data.items():
            result[key] = self._deserialize_value(value)
        return result

    def _deserialize_value(self, value: dict[str, Any]) -> Any:
        """Deserialize a single DynamoDB value."""
        if "S" in value:
            return value["S"]
        elif "N" in value:
            num_str = value["N"]
            return int(num_str) if "." not in num_str else float(num_str)
        elif "BOOL" in value:
            return value["BOOL"]
        elif "M" in value:
            return self._deserialize_map(value["M"])
        elif "L" in value:
            return [self._deserialize_value(v) for v in value["L"]]
        elif "NULL" in value:
            return None
        return None

    def _deserialize_entity(self, item: dict[str, Any]) -> Entity:
        """Deserialize a DynamoDB item to Entity (flat format only)."""
        entity_id = item.get("entity_id", {}).get("S", "")
        name_val = item["name"].get("S") if "name" in item else None
        parent_val = self._deserialize_value(item["parent_id"]) if "parent_id" in item else None
        cascade_val = item.get("cascade", {}).get("BOOL", False)
        metadata_val = (
            self._deserialize_map(item["metadata"].get("M", {}))
            if "metadata" in item and "M" in item.get("metadata", {})
            else {}
        )
        created_val = item.get("created_at", {}).get("S")
        return Entity(
            id=entity_id,
            name=name_val,
            parent_id=parent_val,
            cascade=cascade_val,
            metadata=metadata_val,
            created_at=created_val,
        )

    def _deserialize_bucket(self, item: dict[str, Any]) -> BucketState:
        """Deserialize a DynamoDB item to BucketState (flat format only)."""
        counter_attr = item.get("total_consumed_milli", {})
        total_consumed_milli = int(counter_attr["N"]) if "N" in counter_attr else None
        return BucketState(
            entity_id=item.get("entity_id", {}).get("S", ""),
            resource=item.get("resource", {}).get("S", ""),
            limit_name=item.get("limit_name", {}).get("S", ""),
            tokens_milli=int(item["tokens_milli"]["N"]),
            last_refill_ms=int(item.get("last_refill_ms", {}).get("N", "0")),
            capacity_milli=int(item.get("capacity_milli", {}).get("N", "0")),
            refill_amount_milli=int(item.get("refill_amount_milli", {}).get("N", "0")),
            refill_period_ms=int(item.get("refill_period_ms", {}).get("N", "0")),
            total_consumed_milli=total_consumed_milli,
        )

    def _deserialize_composite_bucket(self, item: dict[str, Any]) -> list[BucketState]:
        """Deserialize a composite DynamoDB item to a list of BucketStates.

        A composite bucket item stores all limits for an entity+resource in a
        single DynamoDB item. Per-limit attributes use the prefix b_{name}_{field}
        with a shared rf (refill timestamp). See ADR-114.
        """
        entity_id = item.get("entity_id", {}).get("S", "")
        resource = item.get("resource", {}).get("S", "")
        rf = int(item.get(schema.BUCKET_FIELD_RF, {}).get("N", "0"))
        limit_names: list[str] = []
        suffix = f"_{schema.BUCKET_FIELD_TK}"
        for attr_name in item:
            if attr_name.startswith(schema.BUCKET_ATTR_PREFIX) and attr_name.endswith(suffix):
                name = attr_name[len(schema.BUCKET_ATTR_PREFIX) : -len(suffix)]
                if name:
                    limit_names.append(name)
        buckets: list[BucketState] = []
        for name in limit_names:

            def _get(field: str) -> int:
                attr = schema.bucket_attr(name, field)
                return int(item.get(attr, {}).get("N", "0"))

            tc_attr = item.get(schema.bucket_attr(name, schema.BUCKET_FIELD_TC), {})
            total_consumed = int(tc_attr["N"]) if "N" in tc_attr else None
            buckets.append(
                BucketState(
                    entity_id=entity_id,
                    resource=resource,
                    limit_name=name,
                    tokens_milli=_get(schema.BUCKET_FIELD_TK),
                    last_refill_ms=rf,
                    capacity_milli=_get(schema.BUCKET_FIELD_CP),
                    refill_amount_milli=_get(schema.BUCKET_FIELD_RA),
                    refill_period_ms=_get(schema.BUCKET_FIELD_RP),
                    total_consumed_milli=total_consumed,
                )
            )
        return buckets

    def _serialize_composite_limits(
        self, limits: list[Limit], base_item: dict[str, Any]
    ) -> dict[str, Any]:
        """Add l_* attributes to a DynamoDB item for composite limit storage.

        Args:
            limits: List of Limit objects to serialize
            base_item: Base DynamoDB item to add attributes to (mutated in place)

        Returns:
            The modified base_item with l_{name}_{field} attributes added
        """
        for limit in limits:
            name = limit.name
            base_item[schema.limit_attr(name, schema.LIMIT_FIELD_CP)] = {"N": str(limit.capacity)}
            base_item[schema.limit_attr(name, schema.LIMIT_FIELD_RA)] = {
                "N": str(limit.refill_amount)
            }
            base_item[schema.limit_attr(name, schema.LIMIT_FIELD_RP)] = {
                "N": str(limit.refill_period_seconds)
            }
        return base_item

    def _deserialize_composite_limits(self, item: dict[str, Any]) -> list[Limit]:
        """Deserialize l_* attributes from a DynamoDB item to Limit objects.

        Discovers limit names by scanning for l_{name}_cp attributes.

        Args:
            item: DynamoDB item with l_{name}_{field} attributes

        Returns:
            List of Limit objects reconstructed from composite attributes
        """
        limit_names: list[str] = []
        suffix = f"_{schema.LIMIT_FIELD_CP}"
        for attr_name in item:
            if attr_name.startswith(schema.LIMIT_ATTR_PREFIX) and attr_name.endswith(suffix):
                name = attr_name[len(schema.LIMIT_ATTR_PREFIX) : -len(suffix)]
                if name:
                    limit_names.append(name)
        limits: list[Limit] = []
        for name in limit_names:

            def _get(field: str) -> int:
                attr = schema.limit_attr(name, field)
                return int(item.get(attr, {}).get("N", "0"))

            limits.append(
                Limit(
                    name=name,
                    capacity=_get(schema.LIMIT_FIELD_CP),
                    refill_amount=_get(schema.LIMIT_FIELD_RA),
                    refill_period_seconds=_get(schema.LIMIT_FIELD_RP),
                )
            )
        return limits

    def resolve_limits(
        self, entity_id: str, resource: str
    ) -> tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]:
        """Resolve effective limits using the four-level config hierarchy.

        Uses SyncConfigCache with batched fetch optimization. Falls back to
        sequential individual GetItem calls if batch resolution fails.

        Args:
            entity_id: Entity to resolve limits for
            resource: Resource being accessed

        Returns:
            Tuple of (limits, on_unavailable, config_source)
        """
        if self.capabilities.supports_batch_operations:
            try:
                return self._config_cache.resolve_limits(
                    entity_id, resource, self.batch_get_configs
                )
            except Exception:
                logger.debug("Batched config resolution failed, falling back to sequential")
        return self._resolve_limits_sequential(entity_id, resource)

    def _resolve_limits_sequential(
        self, entity_id: str, resource: str
    ) -> tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]:
        """Sequential fallback for resolve_limits().

        Queries each config level individually with caching.
        """
        entity_limits = self._config_cache.get_entity_limits(entity_id, resource, self.get_limits)
        if entity_limits:
            return (entity_limits, None, "entity")
        if resource != schema.DEFAULT_RESOURCE:
            entity_default_limits = self._config_cache.get_entity_limits(
                entity_id, schema.DEFAULT_RESOURCE, self.get_limits
            )
            if entity_default_limits:
                return (entity_default_limits, None, "entity_default")
        resource_limits = self._config_cache.get_resource_defaults(
            resource, self.get_resource_defaults
        )
        if resource_limits:
            return (resource_limits, None, "resource")
        system_limits, on_unavailable = self._config_cache.get_system_defaults(
            self.get_system_defaults
        )
        if system_limits:
            return (system_limits, on_unavailable, "system")
        return (None, on_unavailable, None)

    def resolve_on_unavailable(self) -> OnUnavailableAction:
        """Resolve on_unavailable from system config, with caching fallback.

        Returns the on_unavailable action from system config. Caches the
        value after first successful load so it's available as fallback
        when DynamoDB is unreachable. Defaults to "block" if no system
        config exists and no cached value is available.
        """
        try:
            _, on_unavailable = self._config_cache.get_system_defaults(self.get_system_defaults)
            if on_unavailable is not None:
                self._on_unavailable_cache = on_unavailable
                return on_unavailable
            if self._on_unavailable_cache is not None:
                return self._on_unavailable_cache
            return "block"
        except Exception:
            if self._on_unavailable_cache is not None:
                logger.warning(
                    "DynamoDB unavailable, using cached on_unavailable=%s",
                    self._on_unavailable_cache,
                )
                return self._on_unavailable_cache
            logger.warning("DynamoDB unavailable, defaulting on_unavailable=block")
            return "block"

    def invalidate_config_cache(self) -> None:
        """Invalidate all cached config entries (ADR-122)."""
        self._config_cache.invalidate_async()
        self._on_unavailable_cache = None

    def get_cache_stats(self) -> CacheStats:
        """Get config cache performance statistics (ADR-122)."""
        return self._config_cache.get_stats()

    @staticmethod
    def _resolve_parallel_mode(mode: str) -> Any:
        if mode == "auto":
            try:
                from gevent import joinall, monkey, spawn

                if monkey.is_module_patched("socket"):

                    def _executor(funcs: Any) -> Any:
                        greenlets = [spawn(fn) for fn in funcs]
                        joinall(greenlets, raise_error=True)
                        return tuple(g.value for g in greenlets)

                    return _executor
            except ImportError:
                logger.debug("gevent not available; falling back to non-gevent strategy")
            import os

            if os.cpu_count() == 1:
                return lambda funcs: tuple(fn() for fn in funcs)
            return None
        elif mode == "gevent":
            from gevent import joinall, monkey, spawn

            if not monkey.is_module_patched("socket"):
                import warnings

                warnings.warn(
                    "parallel_mode='gevent' without monkey-patching runs like serial. Call gevent.monkey.patch_all() before creating SyncRepository, or use parallel_mode='auto'.",
                    stacklevel=3,
                )

            def _executor(funcs: Any) -> Any:
                greenlets = [spawn(fn) for fn in funcs]
                joinall(greenlets, raise_error=True)
                return tuple(g.value for g in greenlets)

            return _executor
        elif mode == "threadpool":
            import os

            if os.cpu_count() == 1:
                import warnings

                warnings.warn(
                    "parallel_mode='threadpool' on a single-CPU host may cause GIL contention. Consider parallel_mode='auto' or 'serial'.",
                    stacklevel=3,
                )
            return None
        elif mode == "serial":
            return lambda funcs: tuple(fn() for fn in funcs)
        else:
            raise ValueError(
                f"Invalid parallel_mode: {mode!r}. Must be 'auto', 'gevent', 'threadpool', or 'serial'."
            )

    def _run_in_executor(self, *funcs: Any) -> Any:
        executor_fn = self._executor_fn
        if executor_fn is not None:
            return executor_fn(funcs)
        if self._thread_pool is None:
            from concurrent.futures import ThreadPoolExecutor

            self._thread_pool = ThreadPoolExecutor(max_workers=2)
        futures = [self._thread_pool.submit(fn) for fn in funcs]
        return tuple(f.result() for f in futures)

    def _cleanup_thread_pool(self) -> None:
        pool = getattr(self, "_thread_pool", None)
        if pool is not None:
            pool.shutdown(wait=False)
            self._thread_pool = None

    def __del__(self) -> None:
        self._cleanup_thread_pool()


if TYPE_CHECKING:
    from .sync_repository_protocol import SyncRepositoryProtocol

    _: SyncRepositoryProtocol = cast(SyncRepository, None)
