"""In-memory append-only journal (§2.4) — a real `StorageBackend` implementation used by the harness and
tests. Append-only (nothing is ever updated or deleted in place), idempotent by `content_hash` (re-appending
the same canonical object returns its existing offset), monotonic journal offsets. A PostgreSQL-backed
implementation lands behind the SAME interface (StorageBackend) so the rest of the pipeline never changes.
"""

from __future__ import annotations

from dataclasses import replace
from typing import Iterator, Optional

from cl.interfaces.storage import StorageBackend, StoredEvent


class InMemoryJournal(StorageBackend):
    def __init__(self) -> None:
        self._events: list = []  # index == journal_offset
        self._by_content: dict = {}  # content_hash -> offset

    def append(self, event: StoredEvent) -> int:
        existing = self._by_content.get(event.content_hash)
        if existing is not None:
            return existing  # idempotent — the same content never duplicates
        offset = len(self._events)
        stored = replace(event, journal_offset=offset)
        self._events.append(stored)
        self._by_content[event.content_hash] = offset
        return offset

    def get_by_content_hash(self, content_hash: bytes) -> Optional[StoredEvent]:
        offset = self._by_content.get(content_hash)
        return None if offset is None else self._events[offset]

    def scan_from(self, offset: int, limit: Optional[int] = None) -> Iterator[StoredEvent]:
        if offset < 0:
            offset = 0
        # The bound lives HERE, in the storage path (bounded-batch design v0.1) — the range itself is
        # cut, so at most `limit` events are ever touched, never a truncated view of a full walk.
        end = len(self._events) if limit is None else min(len(self._events), offset + limit)
        for i in range(offset, end):
            yield self._events[i]

    def tip_offset(self) -> int:
        return len(self._events) - 1

    def count_from(self, offset: int) -> int:
        # Offsets here ARE dense list indices, so arithmetic over the list is exact.
        return max(0, len(self._events) - max(offset, 0))
