"""StorageBackend seam (§2.4). The append-only canonical store sits behind this interface so the backing
store can move (our server now → an Infobip/FINA-class store later) without redesign. Implementations MUST
preserve: append-only (no in-place update/delete of a stored canonical object), canonical-hash purity
(routing/infra fields live outside `content_hash`), and idempotent append by `content_hash`.
"""

from __future__ import annotations

import abc
from dataclasses import dataclass
from typing import Iterator, Optional


@dataclass(frozen=True)
class StoredEvent:
    """A persisted canonical VEB/PAC-RO event. `content_hash` / `artifact_hash` are the A1 digests; the
    journal offset is assigned by the store, monotonic, and is infra (NOT part of either hash)."""

    journal_offset: int
    content_hash: bytes
    artifact_hash: bytes
    body_binding_kind: str  # INLINE_CANONICAL_BODY | ENCRYPTED_EXTERNAL_BODY
    canonical_bytes: bytes
    proof_set_bytes: bytes = b""
    verification_profile_ref: str = ""
    # links (e.g. a governance decision → its source) — infra, never hashed into content
    linked_content_hashes: tuple[bytes, ...] = ()


class StorageBackend(abc.ABC):
    """Append-only canonical journal. One grammar, one journal (CONTRACTS.md)."""

    @abc.abstractmethod
    def append(self, event: StoredEvent) -> int:
        """Append `event`; return its journal offset. Idempotent by `content_hash`: re-appending the same
        content returns the existing offset and does NOT duplicate."""

    @abc.abstractmethod
    def get_by_content_hash(self, content_hash: bytes) -> Optional[StoredEvent]: ...

    @abc.abstractmethod
    def scan_from(self, offset: int, limit: Optional[int] = None) -> Iterator[StoredEvent]:
        """Replay-ordered scan from `offset` (inclusive) — the basis for the transactional outbox and the
        deterministic replay cursor.

        With `limit`, the backend returns at most that many events AND is required to bound its own
        fetch/materialisation to the limit (bounded-batch design v0.1): a caller must never receive a
        lazily-truncated view of an eagerly-loaded tail. `limit=None` keeps full-scan behaviour."""

    @abc.abstractmethod
    def tip_offset(self) -> int:
        """Highest assigned journal offset (or -1 if empty)."""

    @abc.abstractmethod
    def count_from(self, offset: int) -> int:
        """EXACT number of stored events with journal_offset >= `offset`, without materialising them.

        This — not `tip_offset - watermark` arithmetic — is the basis for backlog depth: journal
        offsets are NOT guaranteed gapless (PostgreSQL identity values are consumed by
        `ON CONFLICT DO NOTHING` and rolled-back inserts), so an offset-range subtraction can
        report phantom backlog that no row backs."""
