"""
cortex/types.py — Shared types for Cortex v3.

This is the foundational type module for the entire Cortex memory system.
Every other module imports from here. Types are organized into layers that
mirror the five-layer memory architecture:

    L1: Storage primitives (VectorResult, FTSResult)
    L2: Session tracking (SessionRecord, EpisodeEvent)
    L3: Immutable evidence (EpisodeEvent — append-only)
    L4: Semantic claims (SemanticClaim, CandidateClaim, ClaimProvenance)
    L5: Compiled knowledge (ProcedureMemory, BrainGraph*)

Cross-cutting:
    Entity, EntityAlias — entity resolution (M3)
    CornerstoneMemory — identity protection (M5)
    MemoryEdge — inter-memory graph
    MemoryFeedback — retrieval quality tracking (M12)
    MemoryLayer, BudgetAllocation — token budget management (M9)

Design decisions:
    - Every type maps 1-to-1 with a DB table row (schema v3)
    - Optional fields use None rather than empty strings so callers can
      distinguish "not set" from "empty"
    - All IDs are hex UUIDs (generated by SQLite: lower(hex(randomblob(16))))
    - All timestamps are ISO 8601 UTC strings
    - StorageAdapter ABC is defined here to avoid circular imports (storage.adapter
      imports from types; the reverse would create a cycle).
    - LLMProvider is re-exported from cortex.llm.provider (that module has no
      cortex imports, so it is safe). This eliminates the duplicate stub class.

Dependencies: cortex.llm.provider (leaf — no further cortex imports).
Dependents: Every other cortex module.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, Optional


# ---------------------------------------------------------------------------
# Canonical category / memory-type constants
# ---------------------------------------------------------------------------

# Valid claim categories. "misc" is fallback for missing/unknown.
VALID_CATEGORIES = frozenset({
    "identity", "personal", "preferences", "engineering", "goals", "misc",
})

# Alias map: normalise common LLM variants to canonical category names.
# Keys are lowercase; values must be in VALID_CATEGORIES.
CATEGORY_ALIASES: dict[str, str] = {
    # engineering
    "technical": "engineering",
    "tech": "engineering",
    "code": "engineering",
    # personal
    "bio": "personal",
    "biographical": "personal",
    "family": "personal",
    # preferences
    "preference": "preferences",
    "pref": "preferences",
    "setting": "preferences",
    # goals
    "goal": "goals",
    "plan": "goals",
    "strategy": "goals",
    # identity
    "self": "identity",
    "personality": "identity",
    "core": "identity",
}

# Memory types that are exempt from deprecation/decay.
EXEMPT_MEMORY_TYPES = frozenset({"cornerstone", "immutable", "core"})

# Semantic-tier memory types (for consolidation queries).
SEMANTIC_MEMORY_TYPES = frozenset({"semantic", "core"})

# Intrinsic types — normalized knowledge classes used by v1.2 wake-path classification.
INTRINSIC_TYPES = frozenset({
    "fact", "preference", "goal",
})

# Valid entity types — canonical set for entity classification.
VALID_ENTITY_TYPES = frozenset({
    "person", "organization", "technology", "project",
    "concept", "location", "event", "other",
})

# Entity type aliases — normalize LLM variations to canonical types.
ENTITY_TYPE_ALIASES: dict[str, str] = {
    "place": "location",
    "loc": "location",
    "city": "location",
    "country": "location",
    "org": "organization",
    "company": "organization",
    "tech": "technology",
    "tool": "technology",
    "framework": "technology",
    "language": "technology",
    "lib": "technology",
    "library": "technology",
    "human": "person",
    "individual": "person",
    "user": "person",
    "idea": "concept",
    "topic": "concept",
    "proj": "project",
    "repo": "project",
    "meeting": "event",
    "conference": "event",
    "incident": "event",
}

# Human relationship families — used for entity resolution weighting.
HUMAN_FAMILIES = frozenset({
    "family", "friend", "colleague", "partner", "mentor", "mentee",
})

# Maps user-facing categories to normalized intrinsic type labels.
CATEGORY_TO_INTRINSIC_MAP: dict[str, str] = {
    "identity": "fact",
    "personal": "fact",
    "preferences": "preference",
    "goals": "goal",
    "engineering": "fact",
    "misc": "fact",
}

# ---------------------------------------------------------------------------
# Cortex deployment tier
# ---------------------------------------------------------------------------

class CortexTier(str, Enum):
    """Deployment tier for Cortex feature-flagging.

    LITE  — entry-level tier (default); limited feature set.
    FULL  — advanced tier; all features enabled.

    Used to gate functionality throughout the codebase via is_full_tier().
    """

    LITE = "lite"
    FULL = "full"


# ---------------------------------------------------------------------------
# Token Budget types (M9)
# ---------------------------------------------------------------------------

class MemoryLayer(str, Enum):
    """Priority-ordered memory layers for budget allocation.

    Order determines injection priority:
      CORNERSTONE (highest) → SESSION → SEMANTIC → PROCEDURAL (lowest)
    When budget is exceeded, lowest-priority layers are truncated first.
    """

    CORNERSTONE = "cornerstone"   # Identity-forming memories — always reserved
    SESSION = "session"           # Recent conversational context
    SEMANTIC = "semantic"         # Long-term factual/semantic memories
    PROCEDURAL = "procedural"     # How-to knowledge, procedures

    # Priority order: lower number = higher priority (truncated last)
    @property
    def priority(self) -> int:
        _priority = {
            MemoryLayer.CORNERSTONE: 0,
            MemoryLayer.SESSION: 1,
            MemoryLayer.SEMANTIC: 2,
            MemoryLayer.PROCEDURAL: 3,
        }
        return _priority[self]

    @classmethod
    def by_priority(cls) -> list["MemoryLayer"]:
        """Return layers ordered from highest to lowest priority."""
        return sorted(cls, key=lambda l: l.priority)

    @classmethod
    def truncation_order(cls) -> list["MemoryLayer"]:
        """Return layers ordered from lowest to highest priority (truncate first)."""
        return sorted(cls, key=lambda l: -l.priority)


@dataclass
class BudgetAllocation:
    """Result of a budget allocation pass.

    Records how many tokens were allocated per layer and what was truncated.
    """

    total_budget: int
    """Total token budget available."""

    cornerstone_reserved: int
    """Tokens reserved exclusively for cornerstone memories."""

    allocations: Dict[str, int] = field(default_factory=dict)
    """Tokens allocated per layer key (MemoryLayer.value)."""

    truncated: Dict[str, int] = field(default_factory=dict)
    """Tokens that were truncated per layer key due to budget exhaustion."""

    @property
    def used(self) -> int:
        """Total tokens consumed (reserved + allocated)."""
        return self.cornerstone_reserved + sum(self.allocations.values())

    @property
    def remaining(self) -> int:
        """Tokens still available for allocation."""
        return max(0, self.total_budget - self.used)

    @property
    def utilization(self) -> float:
        """Fraction of budget consumed (0.0–1.0+)."""
        if self.total_budget == 0:
            return 0.0
        return self.used / self.total_budget

    def is_over_budget(self) -> bool:
        """True if allocations exceeded the total budget."""
        return self.used > self.total_budget


# ---------------------------------------------------------------------------
# Bundle Compression types (Issue #373)
# ---------------------------------------------------------------------------

class CompressionLevel(int, Enum):
    """Bundle compression levels for memory injection.

    Three levels control how much an injected memory item is compressed
    when the token budget is under pressure.

    POINTER  (1) — Very short label; maximum compression.
                   Example: "Project Atlas investor deck context active"
    COMPACT  (2) — One bullet summary; moderate compression.
                   Example: "Atlas deck is executive-facing and budget-sensitive."
    EVIDENCE (3) — 2-4 bullets with brief support; minimal compression.
                   Used for high-relevance items when budget allows.

    Higher integer value = less compressed (more detail).
    Sorted ascending: POINTER < COMPACT < EVIDENCE.
    """

    POINTER = 1
    COMPACT = 2
    EVIDENCE = 3


class InjectionSlot(str, Enum):
    """Priority-ordered memory injection slots for slot-based budget allocation.

    Maps to the 6 memory slot priorities defined in Issue #374.
    Priority order: lower number = higher priority (allocated first, truncated last).

      TASK_CONTEXT    (0) — Active task context; highest priority
      BOOT_CONTEXT    (1) — Boot/session-start context; session-only
      OPEN_LOOPS      (2) — Open loops and commitments
      SEMANTIC_CLAIMS (3) — Relevant long-term semantic claims
      PROCEDURAL      (4) — Procedural / how-to memories
      EPISODIC        (5) — Episodic / historical context; lowest priority
    """

    TASK_CONTEXT = "task_context"
    BOOT_CONTEXT = "boot_context"
    OPEN_LOOPS = "open_loops"
    SEMANTIC_CLAIMS = "semantic_claims"
    PROCEDURAL = "procedural"
    EPISODIC = "episodic"

    @property
    def priority(self) -> int:
        _priority = {
            InjectionSlot.TASK_CONTEXT: 0,
            InjectionSlot.BOOT_CONTEXT: 1,
            InjectionSlot.OPEN_LOOPS: 2,
            InjectionSlot.SEMANTIC_CLAIMS: 3,
            InjectionSlot.PROCEDURAL: 4,
            InjectionSlot.EPISODIC: 5,
        }
        return _priority[self]

    @classmethod
    def by_priority(cls) -> list["InjectionSlot"]:
        """Return slots ordered from highest to lowest priority."""
        return sorted(cls, key=lambda s: s.priority)

    @classmethod
    def default_priority_names(cls) -> list[str]:
        """Return slot value strings in default priority order."""
        return [s.value for s in cls.by_priority()]


@dataclass
class CompressedItem:
    """A memory item at a specific compression level, with token accounting.

    Produced by TokenBudgetManager.compress_to_fit() and consumed by the
    injection layer (CortexPlugin.assemble_context). Carries both the
    compressed content and metadata for budget tracking.

    Attributes:
        content: The text content at the given compression level.
        compression: The applied CompressionLevel (POINTER, COMPACT, EVIDENCE).
        token_count: Tokens consumed by ``content`` at this compression level.
        original_tokens: Token count of the original (uncompressed) content.
    """

    content: str
    compression: CompressionLevel
    token_count: int
    original_tokens: int

    @property
    def compression_ratio(self) -> float:
        """Fraction of original tokens retained (0.0 = nothing, 1.0 = full)."""
        if self.original_tokens == 0:
            return 1.0
        return self.token_count / self.original_tokens

    @property
    def tokens_saved(self) -> int:
        """Tokens saved relative to original content."""
        return max(0, self.original_tokens - self.token_count)


# ---------------------------------------------------------------------------
# Entity
# ---------------------------------------------------------------------------

@dataclass
class Entity:
    """A resolved real-world entity (person, project, concept, etc.).

    Entities are the nodes in Cortex's knowledge graph. Claims reference
    entities via subject_entity_id / object_entity_id foreign keys.

    Created by: EntityResolver (M3) during extraction pipeline.
    Used by: ReconciliationEngine (M4) for claim dedup, RetrievalPipeline (M8)
             for entity-scoped queries.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        canonical_name: The "official" name after resolution (e.g., "Andrew").
        entity_type: Classification — 'person', 'project', 'concept', 'tool', etc.
        description: Optional human-readable description.
        metadata_json: Arbitrary JSON blob for extension data.
        first_seen: ISO 8601 timestamp of first mention.
        last_seen: ISO 8601 timestamp of most recent mention.
        mention_count: Running count of how often this entity appears.
        created_at: Row creation timestamp.
        updated_at: Last modification timestamp.
    """
    id: str
    owner_id: str
    canonical_name: str
    entity_type: str
    description: Optional[str] = None
    metadata_json: Optional[str] = None
    first_seen: Optional[str] = None
    last_seen: Optional[str] = None
    mention_count: int = 1
    created_at: Optional[str] = None
    updated_at: Optional[str] = None


@dataclass
class EntityAlias:
    """An alternative name/spelling that maps to a canonical Entity.

    Aliases enable fuzzy entity resolution — "Andy", "andrew", "@andrew"
    all resolve to the same Entity with canonical_name="Andrew".

    Created by: EntityResolver (M3) when new name variants are encountered.
    Used by: StorageAdapter.find_entity_by_alias() for case-insensitive lookup.

    Attributes:
        id: Hex UUID primary key.
        entity_id: FK → Entity.id (the canonical entity this alias points to).
        alias: The alternative name string.
        alias_type: 'name' (default), 'abbreviation', 'handle', etc.
        source: How this alias was created — 'extracted' | 'manual' | 'merged'.
        confidence: How confident we are this alias belongs to the entity (0-1).
        created_at: Row creation timestamp.
    """
    id: str
    entity_id: str
    alias: str
    alias_type: str = 'name'
    source: str = 'extracted'
    confidence: float = 1.0
    created_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Cornerstone Memory
# ---------------------------------------------------------------------------

@dataclass
class CornerstoneMemory:
    """An identity-forming memory sealed with hash-based integrity protection.

    Cornerstones are the "soul" of the agent — facts like "I am Eva" or
    "My creator is Andrew" that must never drift or be accidentally overwritten.
    They are always injected at the top of the context window (highest priority
    in the token budget).

    Lifecycle:
        1. Operator calls CornerstoneGuardian.seal() → status='active'
        2. DriftCalculator periodically recomputes current_hash vs golden_hash
        3. If drift detected → alert (NEVER auto-correct)
        4. Operator can revise (bumps revision) or unseal (status='retired')

    Created by: CornerstoneGuardian.seal() (M5).
    Used by: TokenBudgetManager (M9) for reserved allocation,
             RetrievalPipeline (M8) for always-inject content,
             DriftCalculator (M11) for integrity checks.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        label: Human-readable identifier (e.g., "I am Eva").
        content: Current content text (may drift from golden_copy).
        golden_copy: Exact text at seal time — the immutable reference.
        golden_hash: SHA-256 of golden_copy — the integrity anchor.
        current_hash: SHA-256 of current content — compared against golden_hash.
        drift_score: 0.0 (identical) to 1.0 (completely different).
        last_drift_check: ISO 8601 timestamp of last integrity check.
        token_count: Tokens consumed by this cornerstone (for budget planning).
        injection_order: Position in context window (0 = first injected).
        sealed_at: When this cornerstone was sealed.
        sealed_by: Who sealed it — 'operator', 'dream_cycle', etc.
        revision: Monotonically increasing version number.
        revision_history: JSON array of previous revision records.
        status: 'active' | 'proposed' | 'retired'.
    """
    id: str
    owner_id: str
    label: str
    content: str
    golden_copy: str
    golden_hash: str
    current_hash: str
    drift_score: float = 0.0
    last_drift_check: Optional[str] = None
    token_count: int = 0
    injection_order: int = 0
    sealed_at: Optional[str] = None
    sealed_by: str = 'operator'
    revision: int = 1
    revision_history: str = '[]'
    status: str = 'active'
    created_at: Optional[str] = None
    updated_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Session
# ---------------------------------------------------------------------------

@dataclass
class EvolutionProposal:
    """A proposed evolution for a cornerstone memory, pending operator review.

    Created by CornerstoneGuardian.propose_evolution() when new evidence suggests
    a cornerstone should be updated. The proposal captures the LLM-generated
    revision, the diff, and the reasoning — but does NOT auto-apply.

    Lifecycle:
        1. propose_evolution() creates proposal with status='pending'
        2. Operator reviews the proposed_content and reasoning
        3. apply_evolution() applies the proposal → status='applied'
        4. Or operator rejects → status='rejected'

    Attributes:
        id: Hex UUID primary key.
        cornerstone_id: FK to cornerstone_memory.id.
        owner_id: Multi-tenancy scope key.
        new_evidence: The raw evidence / claims that triggered the proposal.
        proposed_content: The LLM-generated revised golden copy.
        diff: Text diff between original and proposed content.
        reasoning: LLM explanation of why this change is appropriate.
        confidence: LLM-reported confidence (0.0–1.0).
        status: 'pending' | 'applied' | 'rejected'.
        created_at: ISO 8601 UTC timestamp.
        applied_at: ISO 8601 UTC timestamp when applied (or None).
    """
    id: str
    cornerstone_id: str
    owner_id: str
    new_evidence: str
    proposed_content: str
    diff: str
    reasoning: str
    confidence: float = 0.0
    status: str = "pending"
    created_at: Optional[str] = None
    applied_at: Optional[str] = None


@dataclass
class SessionRecord:
    """Tracks an agent wake/sleep lifecycle (L2 — session layer).

    A session spans from wake() to sleep(). During that time, all episode
    events and extracted claims are tagged with the session_id for provenance.
    The fatigue_score drives automatic nap/sleep triggers.

    Lifecycle: awake → (optional nap) → sleeping → closed.

    Created by: CortexPlugin.wake() (M13).
    Updated by: CortexPlugin.sleep(), dream cycle orchestrator.

    Attributes:
        id: Hex UUID primary key (DB-generated, used as FK in other tables).
        owner_id: Multi-tenancy scope key.
        session_id: Application-level UUID (e.g., from the host platform).
        started_at: ISO 8601 timestamp of session start.
        ended_at: ISO 8601 timestamp of session end (None while awake).
        status: 'awake' | 'napping' | 'sleeping' | 'closed'.
        duration_seconds: Computed on close.
        fatigue_score: 0.0 (fresh) to 1.0 (exhausted) — triggers nap/sleep.
        compaction_count: How many context compactions occurred this session.
        turn_count: Number of conversation turns.
        summary: LLM-generated session summary (created during sleep).
        sleep_report: JSON or text report from the sleep process.
        dream_report: JSON or text report from the dream cycle.
        memories_created: Count of new claims created during this session.
        memories_promoted: Count of session claims promoted to long-term.
        memories_pruned: Count of claims pruned during this session's dream.
    """
    id: str
    owner_id: str
    session_id: str
    started_at: str
    ended_at: Optional[str] = None
    status: str = 'awake'
    duration_seconds: Optional[int] = None
    fatigue_score: float = 0.0
    compaction_count: int = 0
    turn_count: int = 0
    summary: Optional[str] = None
    sleep_report: Optional[str] = None
    dream_report: Optional[str] = None
    memories_created: int = 0
    memories_promoted: int = 0
    memories_pruned: int = 0
    created_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Episode Event (L3 — immutable evidence)
# ---------------------------------------------------------------------------

@dataclass
class EpisodeEvent:
    """An immutable record of something that happened (L3 — evidence layer).

    Episode events are append-only — they can never be modified or deleted.
    They serve as the provenance chain for semantic claims: every claim
    links back to one or more episode events via ClaimProvenance.

    Think of these as the "raw logs" of the agent's experience.

    Created by: StorageAdapter.log_episode() during conversation processing.
    Used by: ExtractionPipeline (M2) as input for claim extraction,
             ClaimProvenance for evidence linking.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        actor_id: Who produced this event — 'user', 'assistant', 'system', etc.
        ts: ISO 8601 timestamp of when the event occurred.
        kind: Event type — 'message', 'tool_call', 'observation', etc.
        session_id: FK → SessionRecord.id (which session this belongs to).
        raw_text: The verbatim text content.
        tool_trace_ref: Reference to a tool execution trace (if kind='tool_call').
        source_uri: External source URL (e.g., file path, web URL).
        metadata_json: Arbitrary JSON blob for extension data.
    """
    id: str
    owner_id: str
    actor_id: str
    ts: str
    kind: str
    session_id: Optional[str] = None
    raw_text: Optional[str] = None
    tool_trace_ref: Optional[str] = None
    source_uri: Optional[str] = None
    metadata_json: Optional[str] = None
    created_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Semantic Claim (L4 — entity-linked, lifecycle-managed)
# ---------------------------------------------------------------------------

@dataclass
class SemanticClaim:
    """A single fact stored as a subject-predicate-object triple (L4).

    This is the primary unit of long-term memory in Cortex. Claims are
    extracted from conversations by the ExtractionPipeline (M2), deduplicated
    by the ReconciliationEngine (M4), and served by the RetrievalPipeline (M8).

    Lifecycle: session → (promoted to) long-term → (possibly) superseded → archived.

    Example claim:
        subject="Andrew", predicate="prefers", object_value="dark mode"

    Created by: ReconciliationEngine.reconcile() (M4).
    Read by: RetrievalPipeline (M8), FeedbackSystem (M12).
    Updated by: ReconciliationEngine (confidence bumps, superseding).
    Deleted by: CortexPlugin.forget() (soft-delete to 'archived' by default).

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        subject: The entity or concept the claim is about.
        predicate: The relationship or property (verb/adjective).
        object_value: The value or target of the relationship.
        memory_type: 'session' (short-term) | 'long-term' (promoted) | 'procedural'.
        subject_entity_id: FK → Entity.id (if entity-resolved).
        object_entity_id: FK → Entity.id (if object is also an entity).
        datatype: 'text' | 'number' | 'date' | 'boolean' | 'json'.
        confidence: 0.0-1.0 — higher means more reliable. Starts at 0.5 for
                    session claims, bumped to 0.8+ on promotion.
        sensitivity: 'normal' | 'private' | 'secret' — controls retrieval filtering.
        scope: 'user' | 'system' | 'global' — visibility scope.
        valid_from: Temporal validity start (ISO 8601).
        valid_to: Temporal validity end (None = still valid).
        status: 'active' | 'superseded' | 'archived' | 'deprecated'.
        superseded_by: FK → SemanticClaim.id (the newer claim that replaces this).
        last_accessed: Timestamp of last retrieval hit (for deprecation scoring).
        access_count: How many times this claim has been retrieved.
        source_session: FK → SessionRecord.id (which session created this claim).
    """
    id: str
    owner_id: str
    subject: str
    predicate: str
    object_value: str
    memory_type: str = 'session'
    subject_entity_id: Optional[str] = None
    object_entity_id: Optional[str] = None
    datatype: str = 'text'
    confidence: float = 0.5
    sensitivity: str = 'normal'
    scope: str = 'user'
    valid_from: Optional[str] = None
    valid_to: Optional[str] = None
    status: str = 'active'
    superseded_by: Optional[str] = None
    last_accessed: Optional[str] = None
    access_count: int = 0
    source_session: Optional[str] = None
    created_at: Optional[str] = None
    updated_at: Optional[str] = None
    # Temporal fields (#573, v12)
    temporal: str = 'current'             # current|past|future|permanent
    temporal_marker: Optional[str] = None # raw time phrase from text ("last week", "March 2026")
    # LoCoMo extraction fields (v10, #516)
    memory_class: str = 'semantic'        # semantic|episodic|procedural|open_loop|transient
    event_date: Optional[str] = None      # ISO YYYY-MM-DD
    sequence_order: Optional[int] = None  # within-conversation ordering
    salience: str = 'medium'              # high|medium|low
    # Causal linking (#518)
    cause: Optional[str] = None
    effect: Optional[str] = None
    # State transitions (#519)
    state_before: Optional[str] = None
    state_after: Optional[str] = None


# ---------------------------------------------------------------------------
# Candidate Claim (pre-storage claim from extraction pipeline)
# ---------------------------------------------------------------------------

@dataclass
class CandidateClaim:
    """A claim extracted from raw text, not yet persisted to storage.

    Produced by the ExtractionPipeline (M2) and consumed by the
    ReconciliationEngine (M4) which decides whether to ADD, UPDATE,
    SUPERSEDE, or NOOP against existing SemanticClaims.
    """
    subject: str
    predicate: str
    object_value: str
    confidence: float = 0.5
    source_text: Optional[str] = None
    entity_type: Optional[str] = None
    claim_type: str = "semantic"
    metadata: Optional[dict] = None
    intrinsic_type: str = "fact"
    human_family: Optional[str] = None
    explicitness: str = "inferred"
    stability: str = "volatile"
    sensitivity: str = "normal"
    category: str = "misc"               # identity | personal | preferences | engineering | goals | misc
    # W4: recency precedence — True when the claim comes directly from an
    # explicit user statement (not inferred from context). Explicit current
    # statements supersede older inferences when the predicates match.
    is_user_explicit: bool = False
    # Temporal marker preserved from extraction (e.g. "yesterday", "March 2026").
    # Accessed via getattr() in reconciliation — formalised here so the contract
    # is explicit and IDE/type-checkers can see it.
    temporal: str = "current"             # current|past|future|permanent
    temporal_marker: Optional[str] = None # raw time phrase ("last week", "March 2026")
    # --- LoCoMo-aligned fields (v1.3) ---
    memory_class: str = "semantic"        # semantic|episodic|procedural|open_loop|transient
    status: str = "active"               # active|completed|superseded|blocked
    event_date: Optional[str] = None     # ISO 8601 YYYY-MM-DD when parseable
    sequence_order: Optional[int] = None  # within-conversation ordering (1,2,3...)
    salience: str = "medium"             # high|medium|low
    # Causal linking (#518)
    cause: Optional[str] = None           # reason/trigger for this fact
    effect: Optional[str] = None          # outcome/result of the cause
    # State transitions (#519)
    state_before: Optional[str] = None    # previous state being replaced
    state_after: Optional[str] = None     # new state after the change
    # Real-time supersession (#514)
    # Extraction-time hint for the reconciliation engine.  When set to "UPDATE",
    # the reconciler will supersede any matching active claim immediately (exact
    # subject+predicate match) without waiting for dream-cycle reconciliation or
    # spending an LLM call.  "ADD" (default) preserves existing behaviour.
    action: str = "ADD"                   # ADD|UPDATE|DELETE


# ---------------------------------------------------------------------------
# Claim Provenance
# ---------------------------------------------------------------------------

@dataclass
class ClaimProvenance:
    """Links a SemanticClaim to the EpisodeEvent(s) that produced it.

    This is the evidence chain — every claim can be traced back to the
    raw conversation text that originated it. Essential for auditing
    and debugging extraction quality.

    Created by: ReconciliationEngine (M4) after storing a new claim.
    Used by: Auditing, debugging extraction pipeline issues.

    Attributes:
        id: Hex UUID primary key.
        claim_id: FK → SemanticClaim.id.
        episode_event_id: FK → EpisodeEvent.id (the source evidence).
        span_start: Character offset in raw_text where the claim was found.
        span_end: Character offset end.
        extraction_method: 'llm' | 'rule' | 'heuristic'.
        extractor_model_id: Which LLM model performed extraction.
        extraction_prompt_ver: Version of the extraction prompt used.
    """
    id: str
    claim_id: str
    episode_event_id: str
    span_start: Optional[int] = None
    span_end: Optional[int] = None
    extraction_method: Optional[str] = None
    extractor_model_id: Optional[str] = None
    extraction_prompt_ver: Optional[str] = None
    created_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Procedure Memory (L5)
# ---------------------------------------------------------------------------

@dataclass
class ProcedureMemory:
    """How-to knowledge — repeatable procedures and workflows (L5).

    Procedures capture "how to do X" knowledge that doesn't fit the
    subject-predicate-object triple format. Examples: deployment steps,
    debugging workflows, user-specific routines.

    Created by: Dream cycle (future — not yet implemented in v3.0).
    Used by: RetrievalPipeline (M8) when MemoryLayer.PROCEDURAL is queried.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        name: Human-readable procedure name (e.g., "Deploy to production").
        kind: 'workflow' | 'recipe' | 'troubleshooting' | 'routine'.
        trigger_desc: When to use this procedure (natural language).
        content_text: Plain-text procedure steps.
        content_json: Structured JSON representation (alternative to text).
        success_count: How many times this procedure was used successfully.
        failure_count: How many times it failed.
        last_used_at: ISO 8601 timestamp of last usage.
        source_type: How this procedure was created — 'extracted' | 'manual'.
    """
    id: str
    owner_id: str
    name: str
    kind: str
    trigger_desc: Optional[str] = None
    content_text: Optional[str] = None
    content_json: Optional[str] = None
    success_count: int = 0
    failure_count: int = 0
    last_used_at: Optional[str] = None
    source_type: Optional[str] = None
    created_at: Optional[str] = None
    updated_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Brain Graph (L5 — versioned compiled knowledge)
# ---------------------------------------------------------------------------

@dataclass
class BrainGraphIndex:
    """Top-level index for a compiled knowledge graph (L5).

    A brain graph is a versioned, structured representation of a document
    or knowledge domain. It's built from source documents (markdown, code)
    by decomposing them into nodes and edges. Supports content-hash-based
    deduplication to avoid re-indexing unchanged documents.

    Lifecycle: building → active → (superseded by newer version).

    Created by: Brain graph builder (M10c watcher + auto-registration).
    Used by: Agentic retrieval for structured knowledge navigation.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        name: Human-readable graph name.
        source_type: 'markdown' | 'code' | 'mixed'.
        source_uri: File path or URL of the source document.
        source_hash: Content hash for dedup (don't re-index if unchanged).
        embedding_model_id: Which model generated node embeddings.
        builder_prompt_ver: Version of the decomposition prompt.
        supersedes_id: FK → BrainGraphIndex.id (previous version).
        node_count: Number of nodes in this graph.
        edge_count: Number of edges in this graph.
        last_indexed: When the graph was last rebuilt.
        index_config: JSON config used during indexing.
        status: 'building' | 'active' | 'superseded' | 'error'.
    """
    id: str
    owner_id: str
    name: str
    source_type: str
    source_uri: Optional[str] = None
    source_hash: Optional[str] = None
    embedding_model_id: Optional[str] = None
    builder_prompt_ver: Optional[str] = None
    supersedes_id: Optional[str] = None
    node_count: int = 0
    edge_count: int = 0
    last_indexed: Optional[str] = None
    index_config: Optional[str] = None
    status: str = 'building'
    created_at: Optional[str] = None


@dataclass
class BrainGraphNode:
    """A single node in a brain graph — a chunk of structured knowledge.

    Nodes can be hierarchical (depth > 0, parent_id set) to represent
    document structure like sections → subsections → paragraphs.

    Attributes:
        id: Hex UUID primary key.
        graph_id: FK → BrainGraphIndex.id.
        node_type: 'section' | 'concept' | 'definition' | 'example' | etc.
        label: Short title or heading for this node.
        content: Full text content of this node.
        compact_repr: Abbreviated representation for budget-constrained retrieval.
        depth: Hierarchy depth (0 = root).
        parent_id: FK → BrainGraphNode.id (parent in hierarchy).
        metadata_json: Arbitrary JSON for extension data.
    """
    id: str
    graph_id: str
    node_type: str
    label: str
    content: Optional[str] = None
    compact_repr: Optional[str] = None
    depth: int = 0
    parent_id: Optional[str] = None
    metadata_json: Optional[str] = None
    created_at: Optional[str] = None


@dataclass
class BrainGraphEdge:
    """A directed relationship between two brain graph nodes.

    Edges encode relationships like "references", "depends_on", "contradicts",
    "elaborates" between knowledge nodes.

    Attributes:
        id: Hex UUID primary key.
        graph_id: FK → BrainGraphIndex.id.
        source_id: FK → BrainGraphNode.id (edge origin).
        target_id: FK → BrainGraphNode.id (edge destination).
        edge_type: Relationship type — 'references', 'contains', etc.
        weight: Strength of the relationship (default 1.0).
        metadata_json: Arbitrary JSON for extension data.
    """
    id: str
    graph_id: str
    source_id: str
    target_id: str
    edge_type: str
    weight: float = 1.0
    metadata_json: Optional[str] = None
    created_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Memory Edge (inter-memory graph)
# ---------------------------------------------------------------------------

@dataclass
class MemoryEdge:
    """A directed edge in the inter-memory graph linking any two memory items.

    Unlike BrainGraphEdge (which links nodes within a single brain graph),
    MemoryEdge links across memory types: claim→entity, entity→entity,
    claim→claim, etc. This enables graph-walk retrieval.

    Created by: ReconciliationEngine (M4) when linking related claims,
                EntityResolver (M3) for entity relationships.
    Used by: RetrievalPipeline (M8) graph-walk expansion.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        source_type: Type of source item — 'claim', 'entity', 'cornerstone', etc.
        source_id: UUID of the source item.
        target_type: Type of target item.
        target_id: UUID of the target item.
        edge_type: Relationship — 'related_to', 'supersedes', 'contradicts', etc.
        confidence: Strength of the relationship (0-1).
        metadata_json: Arbitrary JSON for extension data.
    """
    id: str
    owner_id: str
    source_type: str
    source_id: str
    target_type: str
    target_id: str
    edge_type: str
    confidence: float = 1.0
    metadata_json: Optional[str] = None
    created_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Memory Feedback
# ---------------------------------------------------------------------------

@dataclass
class MemoryFeedback:
    """User/operator feedback on a retrieved memory's quality.

    Feedback is queued as 'pending' and applied during dream cycles.
    The running helpful/harmful ratio influences future retrieval ranking.

    Lifecycle: pending → applied (by FeedbackSystem.process_pending()).

    Created by: FeedbackSystem.submit_feedback() (M12).
    Consumed by: FeedbackSystem.process_pending() during dream cycles.
    Read by: FeedbackSystem.get_score() for retrieval ranking influence.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Multi-tenancy scope key.
        target_type: What kind of memory — 'claim', 'cornerstone', 'procedure', etc.
        target_id: UUID of the memory being rated.
        feedback_type: 'helpful' | 'harmful' | 'wrong' | 'sensitive'.
        feedback_value: Optional payload (e.g., correction text).
        source: Who submitted — 'operator', 'agent', 'user', 'system'.
        status: 'pending' (queued) | 'applied' (processed) | 'dismissed'.
        applied_at: Timestamp when feedback was applied (None while pending).
    """
    id: str
    owner_id: str
    target_type: str
    target_id: str
    feedback_type: str
    feedback_value: Optional[str] = None
    source: str = 'operator'
    status: str = 'pending'
    created_at: Optional[str] = None
    applied_at: Optional[str] = None


# ---------------------------------------------------------------------------
# Search result types (not persisted, returned from queries)
# ---------------------------------------------------------------------------

@dataclass
class VectorResult:
    """A single result from vector similarity search (L1 — storage primitive).

    Returned by: StorageAdapter.vector_search().
    Consumed by: RetrievalPipeline (M8) for hybrid scoring.

    Attributes:
        item_type: 'claim', 'episode', 'cornerstone', etc.
        item_id: UUID of the matched item.
        score: Cosine similarity — higher = more similar (range 0-1 typically).
        model_id: Which embedding model produced the vectors.
    """
    item_type: str
    item_id: str
    score: float          # cosine similarity, higher = more similar
    model_id: Optional[str] = None


@dataclass
class FTSResult:
    """A single result from BM25 full-text search (L1 — storage primitive).

    Returned by: StorageAdapter.fts_search() via FTS5 virtual tables.
    Consumed by: RetrievalPipeline (M8) for hybrid scoring.

    Attributes:
        item_id: UUID of the matched claim or episode event.
        content: The matched text content.
        rank: BM25 rank from FTS5. NOTE: negative values — lower (more negative)
              = better match. This is an FTS5 convention.
    """
    item_id: str          # claim_id or event_id depending on table searched
    content: Optional[str]
    rank: float           # BM25 rank from FTS5 (negative, lower = better match)


# ---------------------------------------------------------------------------
# Entity Resolution types (M3)
# ---------------------------------------------------------------------------

@dataclass
class ResolvedEntity:
    """Result of entity resolution for a mention."""
    entity_id: str
    canonical_name: str
    entity_type: str
    confidence: float
    is_new: bool
    matched_alias: Optional[str] = None


@dataclass
class CandidateEntity:
    """An entity mention candidate (before resolution)."""
    name: str
    entity_type: str
    aliases: list = field(default_factory=list)
    is_new: bool = False


@dataclass
class Peer:
    """A tracked interaction partner with a learned representation.

    Peers are created automatically from entity mentions (when mention_count
    reaches the configured threshold) or manually via the API.

    Attributes:
        id: Hex UUID primary key.
        owner_id: Scoping owner (matches entity.owner_id).
        entity_id: Optional FK to entity table — links peer to resolved entity.
        display_name: Human-readable name for the peer.
        relationship: Relationship label (e.g. 'friend', 'colleague', 'unknown').
        first_interaction: ISO 8601 UTC timestamp of first interaction.
        last_interaction: ISO 8601 UTC timestamp of most recent interaction.
        interaction_count: Running count of interactions.
        representation: JSON-serialised dict of learned traits (communication
            style, topics of interest, preferences, relationship dynamics).
        created_at: Row creation timestamp (ISO 8601 UTC).
        updated_at: Last row mutation timestamp (ISO 8601 UTC).
    """
    id: str
    owner_id: str
    entity_id: Optional[str]
    display_name: str
    relationship: str
    first_interaction: str
    last_interaction: str
    interaction_count: int
    representation: str  # JSON blob
    created_at: str
    updated_at: str


@dataclass
class MergeSuggestion:
    """A candidate entity merge suggestion for human/operator review."""
    keep_id: str
    merge_id: str
    keep_name: str
    merge_name: str
    similarity_score: float
    reason: str


# ---------------------------------------------------------------------------
# Abstract provider interfaces (defined here to avoid circular imports)
# ---------------------------------------------------------------------------

from abc import ABC, abstractmethod
from typing import List as _List, Any as _Any

# LLMProvider canonical definition lives in cortex.llm.provider (no cortex
# imports there, so re-exporting here is safe and eliminates the dual-ABC).
# All callers importing LLMProvider from cortex.types continue to work
# unchanged — this is a drop-in re-export, not a stub shim.
from cortex.llm.provider import LLMProvider  # noqa: F401


class StorageAdapter(ABC):
    """Minimal storage interface defined in types.py to avoid circular imports.

    The full StorageAdapter ABC with all CRUD operations lives in
    cortex.storage.adapter. This minimal version defines only the subset
    needed by EntityResolver (M3), which imports both storage types and
    the adapter protocol from cortex.types.

    This pattern avoids the circular import:
        types.py → adapter.py → types.py (would fail)

    See Also:
        cortex.storage.adapter.StorageAdapter for the complete interface.
    """

    @abstractmethod
    async def create_entity(
        self,
        canonical_name: str,
        entity_type: str,
        owner_id: str = "default",
        **kwargs: _Any,
    ) -> "Entity": ...

    @abstractmethod
    async def find_entity_by_alias(
        self, alias: str, owner_id: str = "default"
    ) -> Optional["Entity"]: ...

    @abstractmethod
    async def get_entity_by_id(self, entity_id: str) -> Optional["Entity"]: ...

    @abstractmethod
    async def get_all_entity_aliases(
        self, owner_id: str = "default"
    ) -> _List["EntityAlias"]: ...

    @abstractmethod
    async def add_entity_alias(
        self,
        entity_id: str,
        alias: str,
        alias_type: str = "name",
        **kwargs: _Any,
    ) -> "EntityAlias": ...

    @abstractmethod
    async def merge_entities(self, keep_id: str, merge_id: str) -> "Entity": ...

    @abstractmethod
    async def get_claims_by_entity(
        self,
        entity_id: str,
        owner_id: Optional[str] = None,
    ) -> _List["SemanticClaim"]: ...

    @abstractmethod
    async def update_claim(
        self, claim_id: str, **kwargs: _Any
    ) -> "SemanticClaim": ...
