"""
cortex/identity/cornerstone.py — Cornerstone Guardian (Module 5 / M5).

Protects identity-forming memories from drift and deletion. Cornerstones are
the agent's "soul" — facts like "I am Eva" or "My creator is Andrew" that
define who the agent is. They are sealed with SHA-256 hashes and constantly
monitored for integrity.

Design principles (non-negotiable):
    - Hash the STORED content (Mem0/LLMs may rewrite input — golden_copy is
      what's actually stored, not what was submitted)
    - Drift detected → ALERT ONLY, NEVER auto-correct (human must decide)
    - Max 10 cornerstones per owner (configurable via CortexConfig)
    - Token budget is reserved for cornerstone injection at session start
    - Cornerstones are always injected first in the context window (highest priority)

Lifecycle:
    1. Operator calls seal(label, content) → creates CornerstoneMemory
    2. DriftCalculator (M11) periodically checks current_hash vs golden_hash
    3. If drift detected → log alert, update drift_score (NEVER auto-restore)
    4. Operator can revise (bumps revision) or unseal (status='retired')
    5. get_for_injection() returns all active cornerstones for context injection

Dependencies:
    - cortex.storage.adapter.StorageAdapter (M1) — CRUD for cornerstone records
    - cortex.config.CortexConfig — max count, token budget
    - tiktoken (optional) — for token counting

Used by:
    - CortexPlugin (M13) — wake() calls get_for_injection()
    - DriftCalculator (M11) — check_all() reads and updates cornerstones
    - TokenBudgetManager (M9) — reserves cornerstone_token_budget
"""

from __future__ import annotations

import difflib
import hashlib
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Dict, List, Optional

try:
    import tiktoken
    _TIKTOKEN_AVAILABLE = True
except ImportError:  # pragma: no cover
    _TIKTOKEN_AVAILABLE = False

if TYPE_CHECKING:
    from cortex.storage.adapter import StorageAdapter
    from cortex.config import CortexConfig
    from cortex.llm.provider import LLMProvider

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------

@dataclass
class CornerstoneMemory:
    """
    Represents a single sealed cornerstone memory.
    Mirrors the `cornerstone_memory` table schema.
    """
    id: str
    owner_id: str
    label: str
    content: str
    golden_copy: str           # exact text at seal time — never modified
    golden_hash: str           # SHA-256 of golden_copy
    current_hash: str          # SHA-256 of current content (for drift detection)
    drift_score: float = 0.0
    last_drift_check: Optional[str] = None
    token_count: int = 0
    injection_order: int = 0
    sealed_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    sealed_by: str = "operator"
    revision: int = 1
    revision_history: List[dict] = field(default_factory=list)
    status: str = "active"     # 'active' | 'proposed' | 'retired'
    created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    updated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())


@dataclass
class DriftReport:
    """Result of a single cornerstone drift check."""
    cornerstone_id: str
    label: str
    drifted: bool
    golden_hash: str
    current_hash: str
    action_taken: str       # 'none' | 'alert'
    checked_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    details: str = ""


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

DEFAULT_MAX_CORNERSTONES = 10
DEFAULT_TIKTOKEN_MODEL = "gpt-4o"
_FALLBACK_CHARS_PER_TOKEN = 4  # rough fallback when tiktoken unavailable


def _compute_diff(original: str, revised: str, label: str = "") -> str:
    """Return a unified diff string between original and revised content."""
    original_lines = original.splitlines(keepends=True)
    revised_lines = revised.splitlines(keepends=True)
    diff_lines = list(difflib.unified_diff(
        original_lines,
        revised_lines,
        fromfile=f"cornerstone/{label} (original)",
        tofile=f"cornerstone/{label} (proposed)",
        lineterm="",
    ))
    return "".join(diff_lines) if diff_lines else "(no textual changes)"


def _sha256(text: str) -> str:
    """Compute SHA-256 hex digest of a UTF-8 string."""
    return hashlib.sha256(text.encode("utf-8")).digest().hex()


def _count_tokens(text: str, model: str = DEFAULT_TIKTOKEN_MODEL) -> int:
    """
    Count tokens using tiktoken.
    Falls back to character-count heuristic if tiktoken is unavailable.
    """
    if _TIKTOKEN_AVAILABLE:
        try:
            enc = tiktoken.encoding_for_model(model)
            return len(enc.encode(text))
        except KeyError:
            # Unknown model — try cl100k_base as sane default
            enc = tiktoken.get_encoding("cl100k_base")
            return len(enc.encode(text))
    # Fallback: ~4 chars per token (GPT-style rough estimate)
    return max(1, len(text) // _FALLBACK_CHARS_PER_TOKEN)


# ---------------------------------------------------------------------------
# CornerstoneGuardian
# ---------------------------------------------------------------------------

class CornerstoneGuardian:
    """
    Manages cornerstone memories: sealing, integrity verification, and drift detection.

    Cornerstones are identity-forming memories that NEVER drift.
    If drift is detected, an alert is raised — the system NEVER auto-corrects.
    """

    def __init__(
        self,
        storage: "StorageAdapter",
        config: "CortexConfig",
        llm: Optional["LLMProvider"] = None,
        event_emitter: object = None,
    ) -> None:
        self.storage = storage
        self.config = config
        self.llm = llm  # optional — used by propose_evolution() for LLM-assisted proposals
        self.event_emitter = event_emitter
        self._max_cornerstones: int = getattr(config, "max_cornerstones", DEFAULT_MAX_CORNERSTONES)
        self._tiktoken_model: str = getattr(config, "embedding_model", DEFAULT_TIKTOKEN_MODEL)

    # ------------------------------------------------------------------
    # Core API (matches task requirements)
    # ------------------------------------------------------------------

    async def seal(
        self,
        label: str,
        content: str,
        owner_id: str = "default",
        sealed_by: str = "operator",
    ) -> CornerstoneMemory:
        """
        Seal a new cornerstone.

        Stores content as golden copy, computes SHA-256 hash, counts tokens,
        and assigns injection order. Enforces the configured max cornerstone cap.

        The hash is computed on the STORED content (what storage actually saves),
        not the raw input — because Mem0/LLM layers may rewrite content on ingest.

        Args:
            label:     Human-readable identifier (e.g. "I am Eva").
            content:   The identity text to protect.
            owner_id:  Scoping key (default = 'default').
            sealed_by: Who sealed it ('operator', 'user', 'dream_cycle').

        Returns:
            CornerstoneMemory with status='active'.

        Raises:
            ValueError: If the cornerstone cap would be exceeded.
        """
        existing = await self.get_all(owner_id=owner_id)
        if len(existing) >= self._max_cornerstones:
            raise ValueError(
                f"Cornerstone cap reached ({self._max_cornerstones}). "
                "Unseal an existing cornerstone before sealing a new one."
            )

        # Persist first; hash what is ACTUALLY stored (storage may normalise)
        cs = await self.storage.seal_cornerstone(
            label=label,
            content=content,
            owner_id=owner_id,
            sealed_by=sealed_by,
        )

        # Hash the stored content — NOT the input (guard against Mem0 rewrites)
        stored_hash = _sha256(cs.content)
        token_count = _count_tokens(cs.content, model=self._tiktoken_model)
        injection_order = len(existing)  # append at end

        # Back-fill computed fields; update if storage didn't set them
        cs.golden_copy = cs.content
        cs.golden_hash = stored_hash
        cs.current_hash = stored_hash
        cs.token_count = token_count
        cs.injection_order = injection_order

        logger.info(
            "Cornerstone sealed: id=%s label=%r tokens=%d hash=%s",
            cs.id, cs.label, cs.token_count, cs.golden_hash[:12],
        )

        # Emit CORNERSTONE_SEALED event
        if self.event_emitter is not None:
            try:
                from cortex.events.emitter import CortexEvent, EventType
                await self.event_emitter.emit(CortexEvent(
                    event_type=EventType.CORNERSTONE_SEALED,
                    owner_id=owner_id,
                    payload={
                        "cornerstone_id": cs.id,
                        "label": cs.label,
                        "token_count": cs.token_count,
                    },
                ))
            except Exception as emit_exc:
                logger.debug("Event emit failed (CORNERSTONE_SEALED): %s", emit_exc)

        return cs

    async def unseal(self, cs_id: str) -> CornerstoneMemory:
        """
        Unseal (retire) a cornerstone — removes it from the active set.

        Sets status='retired'. This is a deliberate, explicit operation;
        it is NOT triggered automatically by drift detection.

        Args:
            cs_id: Cornerstone ID to retire.

        Returns:
            Updated CornerstoneMemory with status='retired'.

        Raises:
            ValueError: If the cornerstone doesn't exist or is already retired.
        """
        # Look up the cornerstone by ID. We need to find it regardless of owner_id,
        # so use get_cornerstone_by_id if available, otherwise fall back to
        # scanning all active cornerstones for the configured owner.
        target = None
        if hasattr(self.storage, "get_cornerstone_by_id"):
            target = await self.storage.get_cornerstone_by_id(cs_id)
        else:
            owner = "default"
            if hasattr(self, "config") and hasattr(self.config, "owner_id"):
                owner = self.config.owner_id
            cornerstones = await self.storage.get_cornerstones(owner_id=owner)
            target = next((c for c in cornerstones if c.id == cs_id), None)

        if target is None:
            raise ValueError(f"Cornerstone not found: {cs_id}")
        if target.status == "retired":
            raise ValueError(f"Cornerstone {cs_id!r} is already retired.")

        # Retire the cornerstone (set status='retired' in DB)
        if hasattr(self.storage, "retire_cornerstone"):
            updated = await self.storage.retire_cornerstone(cs_id)
        else:
            # Fallback: restore then manually update status
            updated = await self.storage.restore_cornerstone(cs_id)
            updated.status = "retired"
        logger.info("Cornerstone unsealed (retired): id=%s label=%r", cs_id, updated.label)
        return updated

    async def verify_integrity(self, owner_id: str = "default") -> List[DriftReport]:
        """
        Verify integrity of ALL active cornerstones for an owner.

        Recomputes SHA-256 of current stored content and compares against the
        sealed golden_hash. Reports any discrepancies — but NEVER auto-corrects.

        Returns:
            List of DriftReport, one per cornerstone checked.
        """
        return await self.check_drift(owner_id=owner_id)

    async def get_all(self, owner_id: str = "default") -> List[CornerstoneMemory]:
        """
        Return all ACTIVE cornerstones for an owner, ordered by injection_order.

        Args:
            owner_id: Owner scope.

        Returns:
            List[CornerstoneMemory] sorted by injection_order.
        """
        all_cs = await self.storage.get_cornerstones(owner_id=owner_id)
        active = [c for c in all_cs if c.status == "active"]
        return sorted(active, key=lambda c: c.injection_order)

    async def check_drift(
        self,
        cs_id: Optional[str] = None,
        owner_id: str = "default",
    ) -> List[DriftReport]:
        """
        Check for hash drift in cornerstones.

        Computes SHA-256 of each cornerstone's CURRENT stored content and compares
        it against the sealed golden_hash. If they differ → ALERT (never auto-correct).

        Args:
            cs_id:    If provided, check only this specific cornerstone.
            owner_id: Owner scope (used when cs_id is None).

        Returns:
            List of DriftReport, one per cornerstone checked.
        """
        if cs_id is not None:
            all_cs = await self.storage.get_cornerstones(owner_id="")
            targets = [c for c in all_cs if c.id == cs_id]
        else:
            targets = await self.get_all(owner_id=owner_id)

        reports: List[DriftReport] = []
        now = datetime.now(timezone.utc).isoformat()

        for cs in targets:
            current_hash = _sha256(cs.content)
            drifted = current_hash != cs.golden_hash

            if drifted:
                action = "alert"
                # ALERT — but NEVER auto-correct. That's the contract.
                logger.warning(
                    "CORNERSTONE DRIFT DETECTED: id=%s label=%r "
                    "golden_hash=%s current_hash=%s — NO auto-correction applied.",
                    cs.id, cs.label,
                    cs.golden_hash[:12], current_hash[:12],
                )
                # Record drift score in storage (hash mismatch = full drift)
                await self.storage.update_cornerstone_drift(cs.id, drift_score=1.0)
            else:
                action = "none"
                await self.storage.update_cornerstone_drift(cs.id, drift_score=0.0)

            reports.append(
                DriftReport(
                    cornerstone_id=cs.id,
                    label=cs.label,
                    drifted=drifted,
                    golden_hash=cs.golden_hash,
                    current_hash=current_hash,
                    action_taken=action,
                    checked_at=now,
                    details=(
                        f"Content hash mismatch for cornerstone '{cs.label}'. "
                        "Manual review required."
                        if drifted
                        else ""
                    ),
                )
            )

        return reports

    # ------------------------------------------------------------------
    # Architecture spec methods (additional)
    # ------------------------------------------------------------------

    async def load_all(self, owner_id: str = "default") -> List[CornerstoneMemory]:
        """Alias for get_all() — matches architecture spec naming."""
        return await self.get_all(owner_id=owner_id)

    async def format_injection_block(self, owner_id: str = "default") -> str:
        """
        Format all active cornerstones into a prompt injection block.

        Returns text suitable for prepending to an LLM context window:
            [CORNERSTONES — WHO I AM]
            ...content...
            [END CORNERSTONES — N tokens]
        """
        cornerstones = await self.get_all(owner_id=owner_id)
        if not cornerstones:
            return ""

        lines = ["[CORNERSTONES — WHO I AM]"]
        for cs in cornerstones:
            lines.append(f"{cs.label}: {cs.content}")
        total_tokens = sum(c.token_count for c in cornerstones)
        lines.append(f"[END CORNERSTONES — {total_tokens} tokens]")
        return "\n".join(lines)

    async def is_cornerstone_subject(
        self, subject: str, owner_id: str = "default"
    ) -> bool:
        """
        Check whether a subject string would touch an active cornerstone.

        Used by the extraction pipeline (M2) to guard against overwriting
        cornerstone-protected facts.

        Args:
            subject: Subject string to check (case-insensitive substring match).
            owner_id: Owner scope.

        Returns:
            True if the subject overlaps with any cornerstone label or content.
        """
        cornerstones = await self.get_all(owner_id=owner_id)
        subject_lower = subject.lower()
        for cs in cornerstones:
            if subject_lower in cs.label.lower() or subject_lower in cs.content.lower():
                logger.debug(
                    "Extraction guard: subject %r touches cornerstone %r",
                    subject, cs.label,
                )
                return True
        return False

    async def revise(
        self,
        cs_id: str,
        new_content: str,
        reason: str,
        revised_by: str = "operator",
    ) -> CornerstoneMemory:
        """
        Explicitly revise a cornerstone.

        Bumps revision number, stores old revision in history, and recomputes
        the golden copy and hash. This is a deliberate operator action — NOT
        triggered by drift detection.

        Args:
            cs_id:       Cornerstone ID to revise.
            new_content: Replacement content.
            reason:      Why the revision was made.
            revised_by:  Who authorised the change.

        Returns:
            Updated CornerstoneMemory with new golden copy and hash.

        Raises:
            ValueError: If cornerstone not found or is retired.
        """
        all_cs = await self.storage.get_cornerstones(owner_id="")
        target = next((c for c in all_cs if c.id == cs_id), None)

        if target is None:
            raise ValueError(f"Cornerstone not found: {cs_id}")
        if target.status == "retired":
            raise ValueError(f"Cannot revise a retired cornerstone: {cs_id!r}")

        # Capture revision number BEFORE storage mutates the object
        old_revision = target.revision

        # Append old version to history
        history_entry = {
            "version": old_revision,
            "text": target.golden_copy,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "reason": reason,
            "revised_by": revised_by,
        }
        history = list(target.revision_history) + [history_entry]
        new_revision = old_revision + 1

        new_hash = _sha256(new_content)
        new_token_count = _count_tokens(new_content, model=self._tiktoken_model)

        # Persist via storage (seal a new revision)
        revised = await self.storage.seal_cornerstone(
            label=target.label,
            content=new_content,
            owner_id=target.owner_id,
            sealed_by=revised_by,
            cs_id=cs_id,                # storage updates in-place if supported
            revision=new_revision,
            revision_history=history,
        )

        # Patch computed fields
        revised.golden_copy = new_content
        revised.golden_hash = new_hash
        revised.current_hash = new_hash
        revised.token_count = new_token_count
        revised.revision = new_revision
        revised.revision_history = history

        logger.info(
            "Cornerstone revised: id=%s label=%r revision=%d reason=%r",
            cs_id, revised.label, revised.revision, reason,
        )
        return revised

    async def propose_evolution(
        self,
        cornerstone_id: str,
        new_evidence: str,
        source: str = "operator",
    ) -> Dict[str, Any]:
        """
        Propose an evolution for a cornerstone based on new evidence.

        Uses the LLM (if available) to generate a revised golden copy that
        incorporates ``new_evidence`` while preserving the core identity of the
        cornerstone.  The proposal is stored in ``cornerstone_evolution_proposal``
        and returned with the diff and reasoning.  It does NOT auto-apply unless
        ``config.cornerstone_auto_apply_threshold`` is set and the LLM confidence
        meets or exceeds that threshold.

        Args:
            cornerstone_id: ID of the cornerstone to evolve.
            new_evidence:   Claims / observations that suggest an update is needed.
            source:         Who triggered the proposal ('operator', 'dream_cycle', …).

        Returns:
            dict with keys:
                - action: 'auto_applied' | 'queued'
                - proposal_id: str (always present)
                - proposed_content: str
                - diff: str (unified diff)
                - reasoning: str
                - confidence: float
                - requires_operator_review: bool
                - cornerstone: CornerstoneMemory (only when action='auto_applied')

        Raises:
            ValueError: If the cornerstone is not found or is retired.
        """
        # 1. Load the target cornerstone ----------------------------------------
        target = await self._get_cornerstone_by_id(cornerstone_id)
        if target is None:
            raise ValueError(f"Cornerstone not found: {cornerstone_id}")
        if target.status == "retired":
            raise ValueError(
                f"Cannot propose evolution for a retired cornerstone: {cornerstone_id!r}"
            )

        original_content = target.golden_copy or target.content

        # 2. Use LLM to generate proposed revision + reasoning -------------------
        proposed_content, reasoning, confidence = await self._llm_propose_revision(
            label=target.label,
            original_content=original_content,
            new_evidence=new_evidence,
        )

        # 3. Compute unified diff ------------------------------------------------
        diff = _compute_diff(original_content, proposed_content, label=target.label)

        # 4. Store the proposal in DB -------------------------------------------
        owner_id = target.owner_id
        proposal = await self.storage.create_evolution_proposal(
            cornerstone_id=cornerstone_id,
            owner_id=owner_id,
            new_evidence=new_evidence,
            proposed_content=proposed_content,
            diff=diff,
            reasoning=reasoning,
            confidence=confidence,
        )
        proposal_id = proposal.id

        logger.info(
            "Cornerstone evolution proposal created: proposal_id=%s cs_id=%s "
            "confidence=%.2f source=%r",
            proposal_id, cornerstone_id, confidence, source,
        )

        # 5. Auto-apply check ---------------------------------------------------
        threshold: Optional[float] = getattr(
            self.config, "cornerstone_auto_apply_threshold", None
        )
        should_auto_apply = (
            threshold is not None
            and confidence >= threshold
            and proposed_content.strip()
        )

        if should_auto_apply:
            revised = await self.revise(
                cs_id=cornerstone_id,
                new_content=proposed_content,
                reason=(
                    f"Auto-applied evolution (proposal={proposal_id}, "
                    f"confidence={confidence:.2f}, source={source})"
                ),
                revised_by=source,
            )
            # Mark proposal as applied
            await self.storage.update_evolution_proposal_status(
                proposal_id=proposal_id,
                status="applied",
                applied_at=datetime.now(timezone.utc).isoformat(),
            )
            logger.info(
                "Cornerstone evolution auto-applied: cs_id=%s proposal_id=%s "
                "confidence=%.2f threshold=%.2f",
                cornerstone_id, proposal_id, confidence, threshold,
            )
            return {
                "action": "auto_applied",
                "proposal_id": proposal_id,
                "proposed_content": proposed_content,
                "diff": diff,
                "reasoning": reasoning,
                "confidence": confidence,
                "requires_operator_review": False,
                "cornerstone": revised,
            }

        # 6. Queue for manual review --------------------------------------------
        return {
            "action": "queued",
            "proposal_id": proposal_id,
            "proposed_content": proposed_content,
            "diff": diff,
            "reasoning": reasoning,
            "confidence": confidence,
            "requires_operator_review": True,
        }

    async def apply_evolution(
        self,
        cornerstone_id: str,
        proposal_id: str,
        applied_by: str = "operator",
    ) -> "CornerstoneMemory":
        """
        Apply an approved evolution proposal to a cornerstone.

        Fetches the proposal, validates it is still pending, then calls
        ``revise()`` to update the cornerstone's golden copy.  The proposal
        status is set to 'applied'.

        Args:
            cornerstone_id: The cornerstone to update.
            proposal_id:    ID of the proposal to apply.
            applied_by:     Who is authorizing the application.

        Returns:
            Updated CornerstoneMemory with the new golden copy.

        Raises:
            ValueError: If the proposal is not found, does not match the
                        cornerstone, or is not in 'pending' status.
        """
        proposal = await self.storage.get_evolution_proposal(proposal_id)
        if proposal is None:
            raise ValueError(f"Evolution proposal not found: {proposal_id}")
        if proposal.cornerstone_id != cornerstone_id:
            raise ValueError(
                f"Proposal {proposal_id!r} belongs to cornerstone "
                f"{proposal.cornerstone_id!r}, not {cornerstone_id!r}"
            )
        if proposal.status != "pending":
            raise ValueError(
                f"Cannot apply proposal {proposal_id!r}: status is {proposal.status!r} "
                "(only 'pending' proposals can be applied)"
            )

        # Apply the revision
        revised = await self.revise(
            cs_id=cornerstone_id,
            new_content=proposal.proposed_content,
            reason=(
                f"Applied evolution proposal {proposal_id} by {applied_by}. "
                f"Evidence: {proposal.new_evidence[:120]}"
            ),
            revised_by=applied_by,
        )

        # Mark proposal as applied
        await self.storage.update_evolution_proposal_status(
            proposal_id=proposal_id,
            status="applied",
            applied_at=datetime.now(timezone.utc).isoformat(),
        )

        logger.info(
            "Cornerstone evolution applied: cs_id=%s proposal_id=%s applied_by=%r",
            cornerstone_id, proposal_id, applied_by,
        )
        return revised

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    async def _get_cornerstone_by_id(self, cs_id: str) -> Optional["CornerstoneMemory"]:
        """Retrieve a cornerstone by ID regardless of status."""
        if hasattr(self.storage, "get_cornerstone_by_id"):
            return await self.storage.get_cornerstone_by_id(cs_id)
        # Fallback: scan all
        all_cs = await self.storage.get_cornerstones(owner_id="")
        return next((c for c in all_cs if c.id == cs_id), None)

    async def _llm_propose_revision(
        self,
        label: str,
        original_content: str,
        new_evidence: str,
    ) -> tuple:
        """
        Use the LLM to propose a revised golden copy given new evidence.

        Returns:
            (proposed_content: str, reasoning: str, confidence: float)

        Falls back to a simple append if no LLM is available.
        """
        if self.llm is None:
            # No LLM — simple additive fallback
            proposed = f"{original_content}\n\nAdditional context: {new_evidence}".strip()
            return proposed, "No LLM available — additive fallback used.", 0.5

        system_prompt = (
            "You are a careful identity guardian for an AI agent.  "
            "Your job is to propose a minimal, precise revision to a cornerstone memory "
            "that incorporates new evidence while preserving the agent's core identity.\n\n"
            "Rules:\n"
            "- Preserve the original meaning and all core identity facts.\n"
            "- Integrate new evidence as concisely as possible.\n"
            "- Do NOT remove any existing facts unless they directly contradict new evidence.\n"
            "- Return a JSON object with keys: "
            "  'proposed_content' (the full revised text), "
            "  'reasoning' (why this revision is appropriate, ≤100 words), "
            "  'confidence' (float 0.0–1.0, how certain you are the revision is correct)."
        )
        user_prompt = (
            f"Cornerstone label: {label}\n\n"
            f"Current golden copy:\n{original_content}\n\n"
            f"New evidence to incorporate:\n{new_evidence}\n\n"
            "Propose a revision.  Return ONLY valid JSON."
        )

        schema = {
            "type": "object",
            "properties": {
                "proposed_content": {"type": "string"},
                "reasoning": {"type": "string"},
                "confidence": {"type": "number"},
            },
            "required": ["proposed_content", "reasoning", "confidence"],
        }

        try:
            from cortex.llm.router import resolve_routed_model
            _routed_model = resolve_routed_model(self.llm, "cornerstone_evolution")
            resp = await self.llm.extract(
                system=system_prompt,
                user=user_prompt,
                schema=schema,
                temperature=0.1,
                **({"model": _routed_model} if _routed_model else {}),
            )
            data = resp.data if hasattr(resp, "data") else resp
            proposed = str(data.get("proposed_content", original_content)).strip()
            reasoning = str(data.get("reasoning", "")).strip()
            confidence = float(data.get("confidence", 0.5))
            confidence = max(0.0, min(1.0, confidence))
            return proposed, reasoning, confidence
        except Exception as exc:
            logger.warning(
                "LLM proposal generation failed, using additive fallback: %s", exc
            )
            proposed = f"{original_content}\n\nAdditional context: {new_evidence}".strip()
            return proposed, f"LLM failed ({exc}); additive fallback used.", 0.4

    async def get_total_token_count(self, owner_id: str = "default") -> int:
        """
        Sum of token counts across all active cornerstones.

        Used by the token budget manager (M9) to reserve space for cornerstone
        injection at session start.

        Args:
            owner_id: Owner scope.

        Returns:
            Total token count (int).
        """
        cornerstones = await self.get_all(owner_id=owner_id)
        return sum(c.token_count for c in cornerstones)
