"""
cortex/storage/adapter.py — Abstract Storage Interface (Module 1 / M1).

The single gateway for ALL persistent state in Cortex. Every module that reads
or writes data does so through this ABC — no module touches the database directly.

This pattern provides:
    - Backend independence (SQLite now, Postgres in Phase 5)
    - Testability (mock the adapter, test business logic in isolation)
    - Transaction boundaries (implementations control atomicity)
    - Migration safety (all schema changes go through initialize())

The ABC defines ~50 methods organized by entity type:
    - Lifecycle: initialize(), close(), get_config(), set_config()
    - Sessions: create_session(), update_session()
    - Episodes: log_episode() (append-only)
    - Claims: create_claim(), update_claim(), get_claims_by_*()
    - Entities: create_entity(), find_entity_by_alias(), merge_entities()
    - Cornerstones: get_cornerstones(), seal_cornerstone(), unseal_cornerstone()
    - Search: fts_search() (BM25), vector_search() (embeddings)
    - Feedback: create_feedback(), get_pending_feedback(), apply_feedback()
    - Brain Graph: create_graph(), add_node(), add_edge()

Current implementation: SQLiteStorageAdapter (cortex/storage/sqlite_adapter.py).
Planned: PostgresStorageAdapter (Phase 5 — multi-instance deployment).

Dependencies: cortex.types (all data types).
Dependents: Every module that does I/O — all of them.
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import List, Optional

from cortex.types import (
    BrainGraphEdge,
    BrainGraphIndex,
    BrainGraphNode,
    ClaimProvenance,
    CornerstoneMemory,
    Entity,
    EntityAlias,
    EpisodeEvent,
    EvolutionProposal,
    FTSResult,
    MemoryEdge,
    MemoryFeedback,
    ProcedureMemory,
    SemanticClaim,
    SessionRecord,
    StorageAdapter as _StorageAdapterBase,
    VectorResult,
)


class StorageAdapter(_StorageAdapterBase):
    """Abstract storage interface — all modules interact with storage through this ABC.

    Every method is async. Implementations are free to use aiosqlite,
    asyncpg, or run sync calls inside an executor — the interface is
    identical regardless of backend.

    Method naming convention:
        create_*  → INSERT, returns the new record
        get_*     → SELECT, returns record(s) or None
        update_*  → UPDATE, returns the modified record
        delete_*  → DELETE (or soft-delete to 'archived' status)
        log_*     → Append-only INSERT (episodes, retrieval log)
        *_search  → Full-text or vector search

    Implementors must handle:
        - Table creation/migration in initialize()
        - Transaction boundaries for multi-step operations (e.g., merge_entities)
        - Graceful handling of missing optional methods (some methods have
          default implementations that raise NotImplementedError)

    See Also:
        cortex.storage.sqlite_adapter.SQLiteStorageAdapter for the reference implementation.
    """

    # ------------------------------------------------------------------ #
    # Lifecycle
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def initialize(self) -> None:
        """
        Create tables if not exist; run migrations; set PRAGMAs.
        Must be idempotent (safe to call on an existing DB).
        """

    @abstractmethod
    async def close(self) -> None:
        """Close the underlying connection / pool cleanly."""

    # ------------------------------------------------------------------ #
    # Entity operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def create_entity(
        self,
        canonical_name: str,
        entity_type: str = "unknown",
        owner_id: str = "default",
        **kwargs,
    ) -> Entity:
        """Insert a new entity row and return it."""

    @abstractmethod
    async def get_entity(self, entity_id: str) -> Optional[Entity]:
        """Return entity by primary key, or None."""

    @abstractmethod
    async def find_entity_by_alias(
        self, alias: str, owner_id: str = "default"
    ) -> Optional[Entity]:
        """Case-insensitive alias lookup; returns the canonical Entity or None."""

    @abstractmethod
    async def add_entity_alias(
        self,
        entity_id: str,
        alias: str,
        alias_type: str = "name",
        **kwargs,
    ) -> EntityAlias:
        """Attach an alias to an existing entity."""

    @abstractmethod
    async def get_entity_by_id(self, entity_id: str) -> Optional[Entity]:
        """Return entity by primary key (alias for get_entity)."""

    @abstractmethod
    async def get_all_entity_aliases(
        self, owner_id: str = "default"
    ) -> List[EntityAlias]:
        """Return all aliases for all entities belonging to owner_id."""

    @abstractmethod
    async def merge_entities(self, keep_id: str, merge_id: str) -> Entity:
        """
        Merge *merge_id* into *keep_id*.
        Re-point all aliases, claims, edges from merge_id → keep_id,
        then delete merge_id.
        """

    # ------------------------------------------------------------------ #
    # Cornerstone operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def get_cornerstones(
        self, owner_id: str = "default", status: str = "active"
    ) -> List[CornerstoneMemory]:
        """Return active cornerstones ordered by injection_order."""

    @abstractmethod
    async def seal_cornerstone(
        self,
        label: str,
        content: str,
        owner_id: str = "default",
        **kwargs,
    ) -> CornerstoneMemory:
        """Seal a new cornerstone (compute golden_hash + current_hash)."""

    @abstractmethod
    async def update_cornerstone_drift(
        self, cs_id: str, drift_score: float
    ) -> None:
        """Persist a new drift_score for a cornerstone."""

    @abstractmethod
    async def restore_cornerstone(self, cs_id: str) -> CornerstoneMemory:
        """Reset content to golden_copy and zero out drift_score."""

    # ------------------------------------------------------------------ #
    # Session operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def create_session(
        self, session_id: str, owner_id: str = "default"
    ) -> SessionRecord:
        """Open a new session record (status='awake')."""

    @abstractmethod
    async def update_session(self, session_id: str, **kwargs) -> SessionRecord:
        """Patch arbitrary columns on a session by session_id (not PK)."""

    @abstractmethod
    async def get_active_session(
        self, owner_id: str = "default"
    ) -> Optional[SessionRecord]:
        """Return the most-recent 'awake' session, or None."""

    @abstractmethod
    async def get_orphaned_sessions(
        self, idle_seconds: int
    ) -> List[SessionRecord]:
        """Return awake sessions that haven't been updated in idle_seconds."""

    @abstractmethod
    async def get_interrupted_sessions(
        self, owner_id: Optional[str] = None
    ) -> List[SessionRecord]:
        """Return sessions stuck in 'sleeping' or 'dreaming' with no ended_at.

        These are consolidation sessions interrupted mid-run by a process crash.
        """

    async def create_handoff_packet(
        self,
        owner_id: str = "default",
        session_id: Optional[str] = None,
        status: str = "active",
        summary_text: Optional[str] = None,
        payload_json: Optional[str] = None,
        generated_at: Optional[str] = None,
    ) -> str:
        """Insert a handoff_packet row and return its id (Issue #360).

        Default raises NotImplementedError; SQLiteAdapter overrides via
        SessionQueryMixin.create_handoff_packet().
        """
        raise NotImplementedError

    async def get_handoff_packets(
        self,
        owner_id: str = "default",
        status: Optional[str] = None,
        limit: int = 10,
    ) -> List[dict]:
        """Return recent handoff_packet rows as plain dicts (Issue #360).

        Default raises NotImplementedError; SQLiteAdapter overrides via
        SessionQueryMixin.get_handoff_packets().
        """
        raise NotImplementedError

    # ------------------------------------------------------------------ #
    # Episode operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def log_episode(
        self,
        session_id: str,
        actor_id: str,
        kind: str,
        raw_text: str,
        owner_id: str = "default",
        **kwargs,
    ) -> EpisodeEvent:
        """Append an immutable episode event."""

    # ------------------------------------------------------------------ #
    # Semantic claim operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def create_claim(
        self,
        subject: str,
        predicate: str,
        object_value: str,
        owner_id: str = "default",
        **kwargs,
    ) -> SemanticClaim:
        """Insert a new semantic claim."""

    @abstractmethod
    async def get_claim(self, claim_id: str) -> Optional[SemanticClaim]:
        """Return claim by PK, or None."""

    @abstractmethod
    async def update_claim(self, claim_id: str, **kwargs) -> SemanticClaim:
        """Patch arbitrary columns on a claim by PK."""

    @abstractmethod
    async def get_claims_by_subject(
        self, subject: str, owner_id: str = "default"
    ) -> List[SemanticClaim]:
        """Return all active claims for a given subject string."""

    @abstractmethod
    async def get_claims_by_entity(
        self,
        entity_id: str,
        owner_id: Optional[str] = None,
    ) -> List[SemanticClaim]:
        """Return claims whose subject_entity_id = entity_id.

        ``owner_id`` should be provided for externally reachable/API paths.
        """

    @abstractmethod
    async def get_claims_by_status(
        self, status: str, owner_id: str = "default"
    ) -> List[SemanticClaim]:
        """Return claims filtered by lifecycle status."""

    @abstractmethod
    async def get_session_claims(self, session_id: str) -> List[SemanticClaim]:
        """Return claims sourced from a specific session."""

    # ------------------------------------------------------------------ #
    # Provenance
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def add_provenance(
        self, claim_id: str, episode_event_id: str, **kwargs
    ) -> ClaimProvenance:
        """Link a claim to an episode_event as its evidence."""

    @abstractmethod
    async def get_provenance(self, claim_id: str) -> List[ClaimProvenance]:
        """Return all provenance records for a given claim."""

    # ------------------------------------------------------------------ #
    # Embedding operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def store_embedding(
        self,
        item_type: str,
        item_id: str,
        embedding: List[float],
        model_id: str,
    ) -> None:
        """Persist an embedding vector for an item."""

    @abstractmethod
    async def vector_search(
        self,
        query_embedding: List[float],
        item_type: Optional[str] = None,
        top_k: int = 50,
        owner_id: str = "default",
    ) -> List[VectorResult]:
        """
        Return the *top_k* most similar items by cosine similarity.

        Uses sqlite-vec if available; falls back to brute-force numpy
        cosine similarity over the embedding_index table.
        """

    # ------------------------------------------------------------------ #
    # FTS operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def fts_search(
        self,
        query: str,
        table: str = "claims",
        top_k: int = 50,
        owner_id: str = "default",
    ) -> List[FTSResult]:
        """
        BM25 full-text search via FTS5.
        *table* must be 'claims' or 'episodes'.
        """

    # ------------------------------------------------------------------ #
    # Memory edge operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def create_edge(
        self,
        source_type: str,
        source_id: str,
        target_type: str,
        target_id: str,
        edge_type: str,
        owner_id: str = "default",
        **kwargs,
    ) -> MemoryEdge:
        """Create a directed edge between two memory items."""

    @abstractmethod
    async def get_neighbors(
        self,
        item_type: str,
        item_id: str,
        edge_types: Optional[List[str]] = None,
        max_depth: int = 1,
    ) -> List[MemoryEdge]:
        """Return outgoing edges from an item (1-hop by default)."""

    # ------------------------------------------------------------------ #
    # Brain Graph operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def create_brain_graph(
        self, name: str, source_type: str, owner_id: str = "default", **kwargs
    ) -> BrainGraphIndex:
        """Create a new brain graph index record."""

    @abstractmethod
    async def add_brain_graph_node(
        self, graph_id: str, node_type: str, label: str, **kwargs
    ) -> BrainGraphNode:
        """Add a node to an existing brain graph."""

    @abstractmethod
    async def add_brain_graph_edge(
        self,
        graph_id: str,
        source_id: str,
        target_id: str,
        edge_type: str,
        **kwargs,
    ) -> BrainGraphEdge:
        """Add a directed edge between two brain graph nodes."""

    @abstractmethod
    async def get_brain_graph_nodes(
        self, graph_id: str, node_type: Optional[str] = None
    ) -> List[BrainGraphNode]:
        """Return all nodes in a graph, optionally filtered by type."""

    @abstractmethod
    async def get_brain_graph(self, graph_id: str) -> Optional[BrainGraphIndex]:
        """Return a single brain graph index by ID, or None."""

    @abstractmethod
    async def find_brain_graph_by_hash(
        self, source_hash: str, owner_id: str = "default"
    ) -> Optional[BrainGraphIndex]:
        """Return an active brain graph with matching source_hash."""

    @abstractmethod
    async def list_brain_graphs(
        self, owner_id: str = "default"
    ) -> List[BrainGraphIndex]:
        """Return all brain graphs for an owner, newest first."""

    @abstractmethod
    async def update_brain_graph(self, graph_id: str, **kwargs) -> BrainGraphIndex:
        """Update fields on a brain graph index record."""

    @abstractmethod
    async def delete_brain_graph(self, graph_id: str) -> None:
        """Delete a brain graph and all its nodes and edges."""

    @abstractmethod
    async def get_brain_graph_node(self, node_id: str) -> Optional[BrainGraphNode]:
        """Return a single brain graph node by ID, or None."""

    @abstractmethod
    async def update_brain_graph_node(
        self, node_id: str, **kwargs
    ) -> BrainGraphNode:
        """Update fields on a brain graph node."""

    @abstractmethod
    async def delete_brain_graph_node(self, node_id: str) -> None:
        """Delete a single brain graph node."""

    @abstractmethod
    async def get_brain_graph_edges(
        self, graph_id: str
    ) -> List[BrainGraphEdge]:
        """Return all edges for a graph."""

    @abstractmethod
    async def get_brain_graph_edge(self, edge_id: str) -> Optional[BrainGraphEdge]:
        """Return a single brain graph edge by ID."""

    @abstractmethod
    async def update_brain_graph_edge(
        self, edge_id: str, **kwargs
    ) -> BrainGraphEdge:
        """Update fields on a brain graph edge."""

    @abstractmethod
    async def delete_brain_graph_edge(self, edge_id: str) -> None:
        """Delete a single brain graph edge."""


    # ------------------------------------------------------------------ #
    # Feedback operations
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def create_feedback(
        self,
        target_type: str,
        target_id: str,
        feedback_type: str,
        owner_id: str = "default",
        **kwargs,
    ) -> MemoryFeedback:
        """Record user/operator feedback on a memory item."""

    @abstractmethod
    async def get_pending_feedback(
        self, owner_id: str = "default"
    ) -> List[MemoryFeedback]:
        """Return all unprocessed feedback entries."""

    @abstractmethod
    async def apply_feedback(self, feedback_id: str) -> MemoryFeedback:
        """Mark feedback as applied and set applied_at timestamp."""

    # ------------------------------------------------------------------ #
    # Log operations (observability)
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def log_retrieval(self, **kwargs) -> None:
        """Append a row to retrieval_log."""

    @abstractmethod
    async def log_drift(self, **kwargs) -> None:
        """Append a row to drift_log."""

    @abstractmethod
    async def log_deprecation(self, **kwargs) -> None:
        """Append a row to deprecation_log."""

    # ------------------------------------------------------------------ #
    # Config key-value store
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def get_config(self, key: str) -> Optional[str]:
        """Return a value from cortex_config, or None."""

    @abstractmethod
    async def set_config(self, key: str, value: str) -> None:
        """Upsert a key in cortex_config."""

    # ------------------------------------------------------------------ #
    # Session lifecycle helpers
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def close_session(self, session_id: str, **kwargs) -> None:
        """Mark a session as closed and set ended_at timestamp."""

    # ------------------------------------------------------------------ #
    # Claim deletion
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def delete_claim(
        self, claim_id: str, owner_id: str, hard: bool = False
    ) -> None:
        """Delete or archive a semantic claim.

        If hard=True, permanently removes the row.
        If hard=False (default), soft-deletes by setting status='archived'.
        """

    # ------------------------------------------------------------------ #
    # Health check
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def ping(self) -> bool:
        """Verify DB connection with a lightweight SELECT 1. Returns True/False."""

    # ------------------------------------------------------------------ #
    # Cornerstone evolution proposals (M5 — propose_evolution / apply_evolution)
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def create_evolution_proposal(
        self,
        cornerstone_id: str,
        owner_id: str,
        new_evidence: str,
        proposed_content: str,
        diff: str,
        reasoning: str,
        confidence: float = 0.0,
    ) -> EvolutionProposal:
        """Insert a new evolution proposal and return it (status='pending')."""

    @abstractmethod
    async def get_evolution_proposals(
        self,
        cornerstone_id: str,
        status: str = "pending",
    ) -> List[EvolutionProposal]:
        """Return proposals for a cornerstone, filtered by status."""

    @abstractmethod
    async def get_evolution_proposal(
        self, proposal_id: str
    ) -> Optional[EvolutionProposal]:
        """Return a single evolution proposal by ID, or None."""

    @abstractmethod
    async def update_evolution_proposal_status(
        self,
        proposal_id: str,
        status: str,
        applied_at: Optional[str] = None,
    ) -> EvolutionProposal:
        """Update the status of an evolution proposal (applied/rejected)."""

    # ------------------------------------------------------------------ #
    # Event log (M19 — In-process event bus)
    # ------------------------------------------------------------------ #

    @abstractmethod
    async def log_event(
        self,
        event_type: str,
        owner_id: str = "default",
        timestamp: Optional[str] = None,
        payload_json: Optional[str] = None,
        session_id: Optional[str] = None,
    ) -> str:
        """Insert an event into the event_log table; return the generated id."""

    @abstractmethod
    async def get_events(
        self,
        owner_id: str = "default",
        event_type: Optional[str] = None,
        limit: int = 50,
        after: Optional[str] = None,
    ) -> list:
        """Return events for owner_id, newest-first, with optional filters."""
