"""Abstract base class for neural storage backends."""

from __future__ import annotations

from abc import ABC, abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING, Any, Literal

if TYPE_CHECKING:
    from neural_memory.core.alert import Alert
    from neural_memory.core.brain import Brain, BrainSnapshot
    from neural_memory.core.fiber import Fiber
    from neural_memory.core.memory_types import MemoryType, Priority, TypedMemory
    from neural_memory.core.neuron import Neuron, NeuronState, NeuronType
    from neural_memory.core.review_schedule import ReviewSchedule
    from neural_memory.core.synapse import Synapse, SynapseType
    from neural_memory.engine.brain_versioning import BrainVersion
    from neural_memory.engine.memory_stages import MaturationRecord, MemoryStage


class NeuralStorage(ABC):
    """
    Abstract interface for neural graph storage.

    Implementations must provide all methods for storing and
    retrieving neurons, synapses, fibers, and brain metadata.
    """

    _current_brain_id: str | None

    @property
    def current_brain_id(self) -> str | None:
        """The active brain ID, or None if not set."""
        return getattr(self, "_current_brain_id", None)

    # ========== Batch Operations ==========

    def disable_auto_save(self) -> None:  # noqa: B027
        """Disable auto-save for batch operations. No-op by default."""

    def enable_auto_save(self) -> None:  # noqa: B027
        """Re-enable auto-save after batch operations. No-op by default."""

    async def batch_save(self) -> None:  # noqa: B027
        """Flush pending writes from batch mode. No-op by default."""

    # ========== Neuron Operations ==========

    @abstractmethod
    async def add_neuron(self, neuron: Neuron) -> str:
        """
        Add a neuron to storage.

        Args:
            neuron: The neuron to add

        Returns:
            The neuron ID

        Raises:
            ValueError: If neuron with same ID already exists
        """
        ...

    @abstractmethod
    async def get_neuron(self, neuron_id: str) -> Neuron | None:
        """
        Get a neuron by ID.

        Args:
            neuron_id: The neuron ID

        Returns:
            The neuron if found, None otherwise
        """
        ...

    async def get_neurons_batch(self, neuron_ids: list[str]) -> dict[str, Neuron]:
        """Get multiple neurons by ID in a single operation.

        Default implementation falls back to sequential get_neuron.
        Backends should override for batch efficiency.

        Args:
            neuron_ids: List of neuron IDs to fetch

        Returns:
            Dict mapping neuron_id to Neuron for found neurons
        """
        results: dict[str, Neuron] = {}
        for nid in neuron_ids:
            neuron = await self.get_neuron(nid)
            if neuron is not None:
                results[nid] = neuron
        return results

    async def find_neurons_exact_batch(
        self,
        contents: list[str],
        type: NeuronType | None = None,
    ) -> dict[str, Neuron]:
        """Find neurons by exact content match for multiple contents at once.

        Default falls back to sequential find_neurons calls.
        Backends should override for batch efficiency.

        Args:
            contents: List of exact content strings to match
            type: Optional neuron type filter

        Returns:
            Dict mapping content string to first matching Neuron
        """
        results: dict[str, Neuron] = {}
        for content in contents:
            matches = await self.find_neurons(type=type, content_exact=content, limit=1)
            if matches:
                results[content] = matches[0]
        return results

    @abstractmethod
    async def find_neurons(
        self,
        type: NeuronType | None = None,
        content_contains: str | None = None,
        content_exact: str | None = None,
        time_range: tuple[datetime, datetime] | None = None,
        limit: int = 100,
    ) -> list[Neuron]:
        """
        Find neurons matching criteria.

        Args:
            type: Filter by neuron type
            content_contains: Filter by content substring (case-insensitive)
            content_exact: Filter by exact content match
            time_range: Filter by created_at within range
            limit: Maximum results to return

        Returns:
            List of matching neurons
        """
        ...

    @abstractmethod
    async def suggest_neurons(
        self,
        prefix: str,
        type_filter: NeuronType | None = None,
        limit: int = 5,
    ) -> list[dict[str, Any]]:
        """Suggest neurons matching a prefix, ranked by relevance + frequency.

        Returns list of dicts with keys: neuron_id, content, type,
        access_frequency, activation_level, score.
        """
        ...

    @abstractmethod
    async def update_neuron(self, neuron: Neuron) -> None:
        """
        Update an existing neuron.

        Args:
            neuron: The updated neuron (must have existing ID)

        Raises:
            ValueError: If neuron doesn't exist
        """
        ...

    @abstractmethod
    async def delete_neuron(self, neuron_id: str) -> bool:
        """
        Delete a neuron and its connected synapses.

        Args:
            neuron_id: The neuron ID to delete

        Returns:
            True if deleted, False if not found
        """
        ...

    # ========== Neuron State Operations ==========

    @abstractmethod
    async def get_neuron_state(self, neuron_id: str) -> NeuronState | None:
        """
        Get the activation state for a neuron.

        Args:
            neuron_id: The neuron ID

        Returns:
            The state if found, None otherwise
        """
        ...

    @abstractmethod
    async def update_neuron_state(self, state: NeuronState) -> None:
        """
        Update or create neuron activation state.

        Args:
            state: The state to save
        """
        ...

    async def get_all_neuron_states(self) -> list[NeuronState]:
        """Get all neuron states for the current brain.

        Returns:
            List of all neuron states
        """
        raise NotImplementedError

    async def get_neuron_states_batch(self, neuron_ids: list[str]) -> dict[str, NeuronState]:
        """Get activation states for multiple neurons in one call.

        Default implementation falls back to sequential get_neuron_state.
        Backends should override for batch efficiency.

        Args:
            neuron_ids: List of neuron IDs to fetch states for

        Returns:
            Dict mapping neuron_id to NeuronState for found states
        """
        result: dict[str, NeuronState] = {}
        for nid in neuron_ids:
            state = await self.get_neuron_state(nid)
            if state is not None:
                result[nid] = state
        return result

    # ========== Synapse Operations ==========

    @abstractmethod
    async def add_synapse(self, synapse: Synapse) -> str:
        """
        Add a synapse to storage.

        Args:
            synapse: The synapse to add

        Returns:
            The synapse ID

        Raises:
            ValueError: If synapse with same ID exists, or neurons don't exist
        """
        ...

    @abstractmethod
    async def get_synapse(self, synapse_id: str) -> Synapse | None:
        """
        Get a synapse by ID.

        Args:
            synapse_id: The synapse ID

        Returns:
            The synapse if found, None otherwise
        """
        ...

    @abstractmethod
    async def get_synapses(
        self,
        source_id: str | None = None,
        target_id: str | None = None,
        type: SynapseType | None = None,
        min_weight: float | None = None,
    ) -> list[Synapse]:
        """
        Find synapses matching criteria.

        Args:
            source_id: Filter by source neuron
            target_id: Filter by target neuron
            type: Filter by synapse type
            min_weight: Filter by minimum weight

        Returns:
            List of matching synapses
        """
        ...

    @abstractmethod
    async def update_synapse(self, synapse: Synapse) -> None:
        """
        Update an existing synapse.

        Args:
            synapse: The updated synapse

        Raises:
            ValueError: If synapse doesn't exist
        """
        ...

    @abstractmethod
    async def delete_synapse(self, synapse_id: str) -> bool:
        """
        Delete a synapse.

        Args:
            synapse_id: The synapse ID to delete

        Returns:
            True if deleted, False if not found
        """
        ...

    async def get_all_synapses(self) -> list[Synapse]:
        """Get all synapses for the current brain.

        Returns:
            List of all synapses
        """
        raise NotImplementedError

    async def get_synapses_for_neurons(
        self,
        neuron_ids: list[str],
        direction: str = "out",
    ) -> dict[str, list[Synapse]]:
        """Get all synapses for multiple neurons in one call.

        Default implementation falls back to sequential get_synapses.
        Backends should override for batch efficiency.

        Args:
            neuron_ids: List of neuron IDs
            direction: "out" for outgoing, "in" for incoming synapses

        Returns:
            Dict mapping neuron_id to list of synapses
        """
        result: dict[str, list[Synapse]] = {}
        for nid in neuron_ids:
            if direction == "out":
                result[nid] = await self.get_synapses(source_id=nid)
            else:
                result[nid] = await self.get_synapses(target_id=nid)
        return result

    # ========== Graph Traversal ==========

    @abstractmethod
    async def get_neighbors(
        self,
        neuron_id: str,
        direction: Literal["out", "in", "both"] = "both",
        synapse_types: list[SynapseType] | None = None,
        min_weight: float | None = None,
    ) -> list[tuple[Neuron, Synapse]]:
        """
        Get neighboring neurons connected by synapses.

        Args:
            neuron_id: The central neuron ID
            direction: Which direction to traverse
                - "out": Only follow outgoing synapses
                - "in": Only follow incoming synapses
                - "both": Follow both directions
            synapse_types: Only follow these synapse types
            min_weight: Only follow synapses with weight >= this

        Returns:
            List of (neighbor_neuron, connecting_synapse) tuples
        """
        ...

    @abstractmethod
    async def get_path(
        self,
        source_id: str,
        target_id: str,
        max_hops: int = 4,
        bidirectional: bool = False,
    ) -> list[tuple[Neuron, Synapse]] | None:
        """
        Find shortest path between two neurons.

        Args:
            source_id: Starting neuron ID
            target_id: Target neuron ID
            max_hops: Maximum path length
            bidirectional: Traverse both incoming and outgoing edges

        Returns:
            List of (neuron, synapse) pairs representing path, or None if no path
        """
        ...

    # ========== Fiber Operations ==========

    @abstractmethod
    async def add_fiber(self, fiber: Fiber) -> str:
        """
        Add a fiber to storage.

        Args:
            fiber: The fiber to add

        Returns:
            The fiber ID

        Raises:
            ValueError: If fiber with same ID exists
        """
        ...

    @abstractmethod
    async def get_fiber(self, fiber_id: str) -> Fiber | None:
        """
        Get a fiber by ID.

        Args:
            fiber_id: The fiber ID

        Returns:
            The fiber if found, None otherwise
        """
        ...

    @abstractmethod
    async def find_fibers(
        self,
        contains_neuron: str | None = None,
        time_overlaps: tuple[datetime, datetime] | None = None,
        tags: set[str] | None = None,
        min_salience: float | None = None,
        metadata_key: str | None = None,
        limit: int = 100,
    ) -> list[Fiber]:
        """
        Find fibers matching criteria.

        Args:
            contains_neuron: Filter by containing this neuron ID
            time_overlaps: Filter by time range overlap
            tags: Filter by having all these tags
            min_salience: Filter by minimum salience
            metadata_key: Filter by metadata containing this key (non-null value)
            limit: Maximum results

        Returns:
            List of matching fibers
        """
        ...

    @abstractmethod
    async def update_fiber(self, fiber: Fiber) -> None:
        """
        Update an existing fiber.

        Args:
            fiber: The updated fiber

        Raises:
            ValueError: If fiber doesn't exist
        """
        ...

    @abstractmethod
    async def delete_fiber(self, fiber_id: str) -> bool:
        """
        Delete a fiber.

        Args:
            fiber_id: The fiber ID to delete

        Returns:
            True if deleted, False if not found
        """
        ...

    async def find_fibers_batch(
        self,
        neuron_ids: list[str],
        limit_per_neuron: int = 10,
    ) -> list[Fiber]:
        """Find fibers containing ANY of the given neurons, deduplicated.

        Default implementation falls back to sequential find_fibers.
        Backends should override for batch efficiency.

        Args:
            neuron_ids: List of neuron IDs to search for
            limit_per_neuron: Max fibers per neuron in fallback

        Returns:
            Deduplicated list of fibers containing any of the neurons
        """
        seen: set[str] = set()
        result: list[Fiber] = []
        for nid in neuron_ids:
            for f in await self.find_fibers(contains_neuron=nid, limit=limit_per_neuron):
                if f.id not in seen:
                    seen.add(f.id)
                    result.append(f)
        return result

    @abstractmethod
    async def get_fibers(
        self,
        limit: int = 10,
        order_by: Literal["created_at", "salience", "frequency"] = "created_at",
        descending: bool = True,
    ) -> list[Fiber]:
        """
        Get fibers with ordering.

        Args:
            limit: Maximum results
            order_by: Field to order by
            descending: Sort descending if True

        Returns:
            List of fibers
        """
        ...

    # ========== Lifecycle ==========

    async def close(self) -> None:  # noqa: B027
        """Close storage connections. No-op by default."""

    # ========== Brain Operations ==========

    def set_brain(self, brain_id: str) -> None:
        """Set the active brain context.

        Args:
            brain_id: The brain ID to activate
        """
        raise NotImplementedError

    @abstractmethod
    async def save_brain(self, brain: Brain) -> None:
        """
        Save or update brain metadata.

        Args:
            brain: The brain to save
        """
        ...

    @abstractmethod
    async def get_brain(self, brain_id: str) -> Brain | None:
        """
        Get brain metadata by ID.

        Args:
            brain_id: The brain ID

        Returns:
            The brain if found, None otherwise
        """
        ...

    async def find_brain_by_name(self, name: str) -> Brain | None:
        """Find a brain by its name.

        Args:
            name: The brain name to search for

        Returns:
            The brain if found, None otherwise
        """
        raise NotImplementedError

    @abstractmethod
    async def export_brain(self, brain_id: str) -> BrainSnapshot:
        """
        Export entire brain as a snapshot.

        Args:
            brain_id: The brain ID to export

        Returns:
            Complete snapshot of the brain

        Raises:
            ValueError: If brain doesn't exist
        """
        ...

    @abstractmethod
    async def import_brain(
        self,
        snapshot: BrainSnapshot,
        target_brain_id: str | None = None,
    ) -> str:
        """
        Import a brain snapshot.

        Args:
            snapshot: The snapshot to import
            target_brain_id: Optional ID for the imported brain

        Returns:
            The ID of the imported brain
        """
        ...

    # ========== Statistics ==========

    @abstractmethod
    async def get_stats(self, brain_id: str) -> dict[str, int]:
        """
        Get statistics for a brain.

        Args:
            brain_id: The brain ID

        Returns:
            Dict with keys: neuron_count, synapse_count, fiber_count
        """
        ...

    @abstractmethod
    async def get_enhanced_stats(self, brain_id: str) -> dict[str, Any]:
        """
        Get enhanced statistics for a brain.

        Includes hot neurons, DB size, daily activity, synapse stats,
        neuron type breakdown, and memory time range.

        Args:
            brain_id: The brain ID

        Returns:
            Dict with enhanced statistics
        """
        ...

    # ========== Typed Memory Operations ==========

    async def add_typed_memory(self, typed_memory: TypedMemory) -> str:
        """Add a typed memory to storage.

        Args:
            typed_memory: The typed memory to add

        Returns:
            The fiber ID used as the typed memory key
        """
        raise NotImplementedError

    async def get_typed_memory(self, fiber_id: str) -> TypedMemory | None:
        """Get a typed memory by its fiber ID.

        Args:
            fiber_id: The fiber ID

        Returns:
            The typed memory if found, None otherwise
        """
        raise NotImplementedError

    async def find_typed_memories(
        self,
        memory_type: MemoryType | None = None,
        min_priority: Priority | None = None,
        include_expired: bool = False,
        project_id: str | None = None,
        tags: set[str] | None = None,
        limit: int = 100,
    ) -> list[TypedMemory]:
        """Find typed memories matching criteria.

        Args:
            memory_type: Filter by memory type
            min_priority: Filter by minimum priority
            include_expired: Include expired memories
            project_id: Filter by project ID
            tags: Filter by having all these tags
            limit: Maximum results to return

        Returns:
            List of matching typed memories
        """
        raise NotImplementedError

    async def update_typed_memory(self, typed_memory: TypedMemory) -> None:
        """Update an existing typed memory.

        Args:
            typed_memory: The updated typed memory

        Raises:
            ValueError: If typed memory doesn't exist
        """
        raise NotImplementedError

    async def delete_typed_memory(self, fiber_id: str) -> bool:
        """Delete a typed memory by its fiber ID.

        Args:
            fiber_id: The fiber ID to delete

        Returns:
            True if deleted, False if not found
        """
        raise NotImplementedError

    async def get_expired_memories(self, limit: int = 100) -> list[TypedMemory]:
        """Get expired typed memories for the current brain.

        Args:
            limit: Maximum number of expired memories to return.

        Returns:
            List of expired typed memories
        """
        raise NotImplementedError

    async def get_expired_memory_count(self) -> int:
        """Get count of expired typed memories for the current brain.

        Cheap COUNT query for health pulse — avoids materializing full objects.

        Returns:
            Number of expired typed memories
        """
        raise NotImplementedError

    async def get_expiring_memories_for_fibers(
        self,
        fiber_ids: list[str],
        within_days: int = 7,
    ) -> list[TypedMemory]:
        """Get typed memories expiring within N days for given fibers.

        Returns only memories that are NOT yet expired but WILL expire
        within the specified window.

        Args:
            fiber_ids: Fiber IDs to check
            within_days: Number of days from now to check

        Returns:
            List of TypedMemory objects expiring within the window
        """
        raise NotImplementedError

    async def get_expiring_memory_count(self, within_days: int = 7) -> int:
        """Count typed memories expiring within N days for current brain.

        Cheap COUNT query — avoids materializing full objects.

        Args:
            within_days: Days from now

        Returns:
            Count of soon-to-expire memories
        """
        raise NotImplementedError

    async def get_stale_fiber_count(self, brain_id: str, stale_days: int = 90) -> int:
        """Get count of fibers that are stale (unused for a long time).

        A fiber is stale if it was never conducted and created_at is older
        than stale_days, or if last_conducted is older than stale_days.

        Args:
            brain_id: The brain ID
            stale_days: Number of days after which a fiber is considered stale

        Returns:
            Number of stale fibers
        """
        raise NotImplementedError

    # ========== Maturation Operations ==========

    async def save_maturation(self, record: MaturationRecord) -> None:  # noqa: B027
        """Save or update a maturation record.

        Default no-op — backends that support maturation should override.

        Args:
            record: The maturation record to save
        """

    async def get_maturation(self, fiber_id: str) -> MaturationRecord | None:
        """Get a maturation record for a fiber.

        Args:
            fiber_id: The fiber ID

        Returns:
            The maturation record if found, None otherwise
        """
        return None

    async def find_maturations(
        self,
        stage: MemoryStage | None = None,
        min_rehearsal_count: int = 0,
    ) -> list[MaturationRecord]:
        """Find maturation records matching criteria.

        Args:
            stage: Optional stage filter
            min_rehearsal_count: Minimum rehearsal count filter

        Returns:
            List of matching maturation records
        """
        return []

    async def cleanup_orphaned_maturations(self) -> int:
        """Delete maturation records whose fiber no longer exists.

        Default no-op — backends that support maturation should override.

        Returns:
            Number of orphaned records removed.
        """
        return 0

    # ========== Co-Activation Operations ==========

    async def record_co_activation(
        self,
        neuron_a: str,
        neuron_b: str,
        binding_strength: float,
        source_anchor: str | None = None,
    ) -> str:
        """Record a co-activation event between two neurons.

        Implementations must store pairs in canonical order (a < b).

        Args:
            neuron_a: First neuron ID
            neuron_b: Second neuron ID
            binding_strength: Strength of the co-activation (0.0-1.0)
            source_anchor: Optional anchor neuron that triggered this

        Returns:
            The event ID
        """
        raise NotImplementedError

    async def get_co_activation_counts(
        self,
        since: datetime | None = None,
        min_count: int = 1,
    ) -> list[tuple[str, str, int, float]]:
        """Get aggregated co-activation counts for neuron pairs.

        Args:
            since: Only count events after this time
            min_count: Minimum co-activation count to include

        Returns:
            List of (neuron_a, neuron_b, count, avg_binding_strength) tuples
        """
        raise NotImplementedError

    async def prune_co_activations(self, older_than: datetime) -> int:
        """Remove co-activation events older than the given time.

        Args:
            older_than: Remove events created before this time

        Returns:
            Number of events pruned
        """
        raise NotImplementedError

    # ========== Action Event Operations ==========

    async def record_action(
        self,
        action_type: str,
        action_context: str = "",
        tags: tuple[str, ...] | list[str] = (),
        session_id: str | None = None,
        fiber_id: str | None = None,
    ) -> str:
        """Record an action event for habit learning.

        Args:
            action_type: Type of action (e.g., "remember", "recall")
            action_context: Optional context string
            tags: Tags for categorization
            session_id: Optional session grouping
            fiber_id: Optional associated fiber

        Returns:
            The action event ID
        """
        raise NotImplementedError

    async def get_action_sequences(
        self,
        session_id: str | None = None,
        since: datetime | None = None,
        limit: int = 1000,
    ) -> list[Any]:
        """Get action events ordered by time.

        Args:
            session_id: Filter by session
            since: Only events after this time
            limit: Maximum events to return

        Returns:
            List of ActionEvent objects ordered by created_at
        """
        raise NotImplementedError

    async def prune_action_events(self, older_than: datetime) -> int:
        """Remove action events older than the given time.

        Args:
            older_than: Remove events created before this time

        Returns:
            Number of events pruned
        """
        raise NotImplementedError

    # ========== Version Operations ==========

    async def save_version(
        self,
        brain_id: str,
        version: BrainVersion,
        snapshot_json: str,
    ) -> None:
        """Save a brain version with its snapshot data.

        Args:
            brain_id: Brain ID
            version: The version metadata
            snapshot_json: Serialized snapshot JSON
        """
        raise NotImplementedError

    async def get_version(
        self,
        brain_id: str,
        version_id: str,
    ) -> tuple[BrainVersion, str] | None:
        """Get a version and its snapshot JSON by ID.

        Args:
            brain_id: Brain ID
            version_id: Version ID

        Returns:
            Tuple of (BrainVersion, snapshot_json) or None
        """
        raise NotImplementedError

    async def list_versions(
        self,
        brain_id: str,
        limit: int = 20,
    ) -> list[BrainVersion]:
        """List versions for a brain, most recent first.

        Args:
            brain_id: Brain ID
            limit: Maximum versions to return

        Returns:
            List of BrainVersion, newest first
        """
        raise NotImplementedError

    async def get_next_version_number(self, brain_id: str) -> int:
        """Get the next version number for a brain.

        Args:
            brain_id: Brain ID

        Returns:
            Next auto-incrementing version number
        """
        raise NotImplementedError

    async def delete_version(self, brain_id: str, version_id: str) -> bool:
        """Delete a specific version.

        Args:
            brain_id: Brain ID
            version_id: Version ID

        Returns:
            True if deleted, False if not found
        """
        raise NotImplementedError

    # ========== Review Schedule Operations ==========

    async def add_review_schedule(self, schedule: ReviewSchedule) -> str:
        """Insert or update a review schedule (upsert by fiber_id + brain_id).

        Args:
            schedule: The review schedule to save

        Returns:
            The fiber ID
        """
        raise NotImplementedError

    async def get_review_schedule(self, fiber_id: str) -> ReviewSchedule | None:
        """Get a review schedule by fiber ID.

        Args:
            fiber_id: The fiber ID

        Returns:
            The review schedule if found, None otherwise
        """
        raise NotImplementedError

    async def get_due_reviews(self, limit: int = 20) -> list[ReviewSchedule]:
        """Get review schedules that are due (next_review <= now).

        Args:
            limit: Maximum results (capped at 100)

        Returns:
            List of due review schedules, oldest first
        """
        raise NotImplementedError

    async def delete_review_schedule(self, fiber_id: str) -> bool:
        """Delete a review schedule.

        Args:
            fiber_id: The fiber ID

        Returns:
            True if deleted, False if not found
        """
        raise NotImplementedError

    async def get_review_stats(self) -> dict[str, int]:
        """Get review statistics for the current brain.

        Returns:
            Dict with keys: total, due, box_1..box_5
        """
        raise NotImplementedError

    # ========== Depth Prior Operations ==========

    async def get_depth_priors_batch(
        self,
        entity_texts: list[str],
    ) -> dict[str, list[Any]]:
        """Batch-fetch Bayesian depth priors for multiple entities.

        Args:
            entity_texts: Entity text strings to look up

        Returns:
            Dict mapping entity_text to list of DepthPrior objects
        """
        raise NotImplementedError

    async def upsert_depth_prior(self, prior: Any) -> None:
        """Insert or update a single depth prior.

        Args:
            prior: DepthPrior instance to persist
        """
        raise NotImplementedError

    async def get_stale_priors(self, older_than: datetime) -> list[Any]:
        """Find depth priors not updated since a given date.

        Args:
            older_than: Cutoff datetime; priors with last_updated before this are returned

        Returns:
            List of stale DepthPrior objects
        """
        raise NotImplementedError

    async def delete_depth_priors(self, entity_text: str) -> int:
        """Delete all depth priors for an entity.

        Args:
            entity_text: The entity text whose priors should be removed

        Returns:
            Number of rows deleted
        """
        raise NotImplementedError

    # ========== Compression Backup Operations ==========

    async def save_compression_backup(
        self,
        fiber_id: str,
        original_content: str,
        compression_tier: int,
        original_token_count: int,
        compressed_token_count: int,
    ) -> None:
        """Save (upsert) a pre-compression content backup for a fiber.

        Only called for reversible tiers (1-2).  Backends that do not
        support compression may leave this as a no-op.

        Args:
            fiber_id: The fiber whose content is being backed up.
            original_content: Full original text before compression.
            compression_tier: The tier to which the fiber is being compressed.
            original_token_count: Approximate token count before compression.
            compressed_token_count: Approximate token count after compression.
        """
        raise NotImplementedError

    async def get_compression_backup(self, fiber_id: str) -> dict[str, Any] | None:
        """Retrieve the compression backup for a fiber, if any.

        Args:
            fiber_id: The fiber ID to look up.

        Returns:
            Dict with backup fields, or None if no backup exists.
        """
        raise NotImplementedError

    async def delete_compression_backup(self, fiber_id: str) -> bool:
        """Delete the compression backup for a fiber.

        Args:
            fiber_id: The fiber ID whose backup should be removed.

        Returns:
            True if a row was deleted, False if no backup existed.
        """
        raise NotImplementedError

    async def get_compression_stats(self) -> dict[str, Any]:
        """Return aggregate compression statistics for the current brain.

        Returns:
            Dict with keys: ``total_backups``, ``by_tier``, ``total_tokens_saved``.
        """
        raise NotImplementedError

    # ========== Change Log Operations ==========

    async def record_change(
        self,
        entity_type: str,
        entity_id: str,
        operation: str,
        device_id: str = "",
        payload: dict[str, Any] | None = None,
    ) -> int:
        """Append a change to the log. Returns the sequence number (id).

        Args:
            entity_type: Type of entity changed ("neuron", "synapse", "fiber").
            entity_id: ID of the changed entity.
            operation: Operation performed ("insert", "update", "delete").
            device_id: ID of the device that made the change.
            payload: Optional dict of changed field values.

        Returns:
            Auto-incremented sequence number (id) of the new log entry.
        """
        raise NotImplementedError

    async def get_changes_since(self, sequence: int = 0, limit: int = 1000) -> list[Any]:
        """Get changes after a given sequence number, ordered by id ASC.

        Args:
            sequence: Return only entries with id > this value (0 = all).
            limit: Maximum number of entries to return (capped server-side).

        Returns:
            List of ChangeEntry objects ordered by id ascending.
        """
        raise NotImplementedError

    async def get_unsynced_changes(self, limit: int = 1000) -> list[Any]:
        """Get all unsynced changes, ordered by id ASC.

        Args:
            limit: Maximum number of entries to return (capped server-side).

        Returns:
            List of unsynced ChangeEntry objects ordered by id ascending.
        """
        raise NotImplementedError

    async def mark_synced(self, up_to_sequence: int) -> int:
        """Mark all changes up to a sequence number as synced.

        Args:
            up_to_sequence: Mark all entries with id <= this value as synced.

        Returns:
            Count of rows updated.
        """
        raise NotImplementedError

    async def prune_synced_changes(self, older_than_days: int = 30) -> int:
        """Delete synced changes older than N days.

        Args:
            older_than_days: Delete synced entries whose changed_at is older
                than this many days from now.

        Returns:
            Count of rows deleted.
        """
        raise NotImplementedError

    async def get_change_log_stats(self) -> dict[str, Any]:
        """Get change log statistics for the current brain.

        Returns:
            Dict with keys: ``total``, ``pending``, ``synced``, ``last_sequence``.
        """
        raise NotImplementedError

    # ========== Device Registry Operations ==========

    async def register_device(self, device_id: str, device_name: str = "") -> Any:
        """Register a device for the current brain (upsert).

        Args:
            device_id: Unique device identifier.
            device_name: Human-readable device name.

        Returns:
            DeviceRecord for the registered device.
        """
        raise NotImplementedError

    async def get_device(self, device_id: str) -> Any | None:
        """Get device info for a specific device.

        Args:
            device_id: The device ID to look up.

        Returns:
            DeviceRecord if found, None otherwise.
        """
        raise NotImplementedError

    async def list_devices(self) -> list[Any]:
        """List all registered devices for the current brain.

        Returns:
            List of DeviceRecord objects ordered by registration time ascending.
        """
        raise NotImplementedError

    async def update_device_sync(self, device_id: str, last_sync_sequence: int) -> None:
        """Update the last sync timestamp and sequence for a device.

        Args:
            device_id: The device to update.
            last_sync_sequence: The highest change log sequence the device has synced.
        """
        raise NotImplementedError

    async def remove_device(self, device_id: str) -> bool:
        """Remove a device from the registry.

        Args:
            device_id: The device to remove.

        Returns:
            True if a row was deleted, False if device was not registered.
        """
        raise NotImplementedError

    # ========== Alert Operations ==========

    def _get_brain_id(self) -> str:
        """Return current brain ID, raising ValueError if not set."""
        raise NotImplementedError

    async def record_alert(self, alert: Alert) -> str:
        """Insert a new alert. Returns alert ID if inserted, empty string if suppressed."""
        raise NotImplementedError

    async def get_active_alerts(self, limit: int = 50) -> list[Alert]:
        """Get active/seen/acknowledged alerts (not resolved)."""
        raise NotImplementedError

    async def count_pending_alerts(self) -> int:
        """Count active + seen alerts (not acknowledged or resolved)."""
        raise NotImplementedError

    async def mark_alerts_seen(self, alert_ids: list[str]) -> int:
        """Mark alerts as seen. Returns count of updated rows."""
        raise NotImplementedError

    async def mark_alert_acknowledged(self, alert_id: str) -> bool:
        """Mark a single alert as acknowledged. Returns True if updated."""
        raise NotImplementedError

    async def resolve_alerts_by_type(self, alert_types: list[str]) -> int:
        """Resolve all active/seen alerts of given types. Returns count."""
        raise NotImplementedError

    # ========== Cleanup ==========

    @abstractmethod
    async def clear(self, brain_id: str) -> None:
        """
        Clear all data for a brain.

        Args:
            brain_id: The brain ID to clear
        """
        ...

    # ========== Cognitive State (optional, provided by SQLiteCognitiveMixin) ==========

    async def upsert_cognitive_state(
        self,
        neuron_id: str,
        *,
        confidence: float = 0.5,
        evidence_for_count: int = 0,
        evidence_against_count: int = 0,
        status: str = "active",
        predicted_at: str | None = None,
        resolved_at: str | None = None,
        schema_version: int = 1,
        parent_schema_id: str | None = None,
        last_evidence_at: str | None = None,
    ) -> None:
        """Insert or update a cognitive state record."""
        raise NotImplementedError

    async def get_cognitive_state(self, neuron_id: str) -> dict[str, Any] | None:
        """Get cognitive state for a neuron."""
        raise NotImplementedError

    async def list_cognitive_states(
        self,
        *,
        status: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """List cognitive states, optionally filtered by status."""
        raise NotImplementedError

    async def update_cognitive_evidence(
        self,
        neuron_id: str,
        *,
        confidence: float,
        evidence_for_count: int,
        evidence_against_count: int,
        status: str,
        resolved_at: str | None = None,
        last_evidence_at: str | None = None,
    ) -> None:
        """Update only evidence-related fields of a cognitive state."""
        raise NotImplementedError

    async def list_predictions(
        self,
        *,
        status: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """List predictions (cognitive states with predicted_at set)."""
        raise NotImplementedError

    async def get_calibration_stats(self) -> dict[str, int]:
        """Get prediction calibration statistics."""
        raise NotImplementedError

    async def refresh_hot_index(self, items: list[dict[str, Any]]) -> int:
        """Replace the hot index with freshly scored items."""
        raise NotImplementedError

    async def get_hot_index(self, limit: int = 10) -> list[dict[str, Any]]:
        """Get the current hot index items."""
        raise NotImplementedError

    async def add_knowledge_gap(
        self,
        *,
        topic: str,
        detection_source: str,
        priority: float = 0.5,
        related_neuron_ids: list[str] | None = None,
    ) -> str:
        """Create a new knowledge gap record."""
        raise NotImplementedError

    async def list_knowledge_gaps(
        self,
        *,
        include_resolved: bool = False,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """List knowledge gaps."""
        raise NotImplementedError

    async def get_knowledge_gap(self, gap_id: str) -> dict[str, Any] | None:
        """Get a single knowledge gap by ID."""
        raise NotImplementedError

    async def resolve_knowledge_gap(
        self,
        gap_id: str,
        *,
        resolved_by_neuron_id: str | None = None,
    ) -> bool:
        """Mark a knowledge gap as resolved."""
        raise NotImplementedError

    async def get_schema_history(
        self,
        neuron_id: str,
        *,
        max_depth: int = 20,
    ) -> list[dict[str, Any]]:
        """Walk the version chain for a hypothesis."""
        raise NotImplementedError
