"""
cortex/capture/activation_router.py — W3 Activation Policy + Storage Router.

Second-pass policy engine that runs after claim reconciliation. For each newly
stored claim, it derives an ``ActivationPolicy`` — a structured record of *how*
and *when* that memory should be activated during future sessions — and persists
it to the ``memory_activation`` table (schema v8).

All logic is deterministic (no LLM cost). Inputs come from the envelope fields
added in #357:
    - ``intrinsic_type``  — "fact" | "preference" | "goal"
    - ``human_family``    — "family" | "friend" | "colleague" | … | None
    - ``explicitness``    — "explicit" | "implied" | "inferred"
    - ``stability``       — "stable" | "mutable" | "volatile"
    - ``sensitivity``     — "normal" | "personal" | "sensitive"

Outputs (stored in ``memory_activation``):
    - ``scope``              — visibility scope of the activation record
    - ``trigger_pattern``    — comma-separated trigger tags (e.g. "session_boot")
    - ``suppressors_json``   — JSON array of suppressor tags
    - ``priority``           — int 0-10 (higher = surface first)
    - ``decay_factor``       — float 0.0-1.0 (lower = fades faster)

Design decisions:
    - Owner-scope is always propagated from the claim → write; never defaulted away.
    - Sensitive items get narrower scope and extra suppressors by default.
    - session_boot trigger is applied for stable/high-priority facts so they
      load at session start without an explicit query.
    - Storage errors are non-fatal: activation failure does NOT roll back the claim.

Data flow:
    run_extraction() (wake_pipeline.py)
      → reconciliation writes claims
      → for each ADD/UPDATE/SUPERSEDE result:
          → ActivationRouter.derive(candidate, claim_id, owner_id)
          → ActivationRouter.write(storage, policy)   [best-effort]
"""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field
from typing import List, Optional

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Output type
# ---------------------------------------------------------------------------

@dataclass
class ActivationPolicy:
    """Derived activation parameters for a single claim.

    All fields are computed deterministically from envelope fields.
    This struct is written to ``memory_activation`` once per stored claim.

    Attributes:
        claim_id:         FK → semantic_claim.id
        owner_id:         Multi-tenancy scope key (propagated from claim)
        scope:            Visibility scope — "private" | "user" | "session"
        trigger_pattern:  Comma-separated activation trigger tags, or None
        suppressors:      List of suppressor tag strings
        priority:         0-10 (higher surfaces first)
        decay_factor:     0.0-1.0 (1.0 = never decays; lower = faster decay)
        rationale:        Human-readable derivation explanation (for audit)
    """
    claim_id: str
    owner_id: str
    scope: str = "user"
    trigger_pattern: Optional[str] = None
    suppressors: List[str] = field(default_factory=list)
    priority: int = 3
    decay_factor: float = 0.85
    rationale: str = ""


# ---------------------------------------------------------------------------
# Routing tables (pure constants — no state)
# ---------------------------------------------------------------------------

# stability → base decay factor
_DECAY_BY_STABILITY: dict[str, float] = {
    "stable":   1.0,
    "mutable":  0.85,
    "volatile": 0.50,
}

# sensitivity → scope narrowing
_SCOPE_BY_SENSITIVITY: dict[str, str] = {
    "sensitive": "private",
    "personal":  "user",
    "normal":    "user",
}

# stability × explicitness → base priority
_PRIORITY_TABLE: dict[tuple[str, str], int] = {
    ("stable",   "explicit"): 8,
    ("stable",   "implied"):  6,
    ("stable",   "inferred"): 5,
    ("mutable",  "explicit"): 5,
    ("mutable",  "implied"):  4,
    ("mutable",  "inferred"): 3,
    ("volatile", "explicit"): 3,
    ("volatile", "implied"):  2,
    ("volatile", "inferred"): 1,
}

# intrinsic_type → priority bonus
_PRIORITY_BONUS: dict[str, int] = {
    "fact":       1,
    "preference": 1,
    "goal":       2,   # goals are action-relevant → surface earlier
}

# sensitivity-driven suppressors (narrowly scoped by default)
_SUPPRESSORS_BY_SENSITIVITY: dict[str, List[str]] = {
    "sensitive": ["low_trust_context", "group_context", "public_context"],
    "personal":  ["group_context", "public_context"],
    "normal":    [],
}

# Session-boot priority threshold.
# Claims with derived priority >= this will include "session_boot" in triggers.
_SESSION_BOOT_PRIORITY_THRESHOLD = 6


# ---------------------------------------------------------------------------
# Main router
# ---------------------------------------------------------------------------

class ActivationRouter:
    """Derives and persists activation policies for stored claims.

    All methods are pure (derive) or write-only best-effort (write).
    No reads from storage; no LLM calls.

    Usage::

        router = ActivationRouter()
        policy = router.derive(candidate, claim_id="abc123", owner_id="default")
        await router.write(storage, policy)
    """

    def derive(
        self,
        candidate: object,
        claim_id: str,
        owner_id: str,
    ) -> ActivationPolicy:
        """Derive an ActivationPolicy from a candidate claim's envelope fields.

        All envelope fields are read with ``getattr(..., default)`` so this
        method is safe to call on any object that has some or all of the
        v1.2 envelope fields — including legacy candidates that pre-date #357.

        Args:
            candidate:  CandidateClaim (or compatible) with envelope fields.
            claim_id:   The persisted claim's ID (FK for memory_activation).
            owner_id:   Multi-tenancy scope key.

        Returns:
            ActivationPolicy with derived routing fields and rationale.
        """
        # Read envelope fields with safe defaults
        intrinsic_type = getattr(candidate, "intrinsic_type", "fact") or "fact"
        human_family   = getattr(candidate, "human_family",   None)
        explicitness   = getattr(candidate, "explicitness",   "inferred") or "inferred"
        stability      = getattr(candidate, "stability",      "volatile") or "volatile"
        sensitivity    = getattr(candidate, "sensitivity",    "normal")   or "normal"

        # ------------------------------------------------------------------
        # 1. Scope — sensitivity is the primary driver; narrow by default
        # ------------------------------------------------------------------
        scope = _SCOPE_BY_SENSITIVITY.get(sensitivity, "user")
        # Relationship data (human_family set) is always user-scoped minimum
        if human_family and scope == "session":
            scope = "user"

        # ------------------------------------------------------------------
        # 2. Priority — stability × explicitness base + intrinsic bonus
        # ------------------------------------------------------------------
        base_priority = _PRIORITY_TABLE.get((stability, explicitness), 2)
        priority = base_priority + _PRIORITY_BONUS.get(intrinsic_type, 0)
        priority = min(10, max(0, priority))

        # ------------------------------------------------------------------
        # 3. Decay factor — stability-driven; sensitive items decay faster
        # ------------------------------------------------------------------
        decay_factor = _DECAY_BY_STABILITY.get(stability, 0.85)
        if sensitivity == "sensitive":
            decay_factor = round(decay_factor * 0.9, 4)

        # ------------------------------------------------------------------
        # 4. Trigger pattern — which contexts should activate this memory
        # ------------------------------------------------------------------
        triggers: List[str] = []

        # session_boot: load stable/high-priority memories on every session start
        if priority >= _SESSION_BOOT_PRIORITY_THRESHOLD:
            triggers.append("session_boot")

        # type-specific triggers
        if intrinsic_type == "goal":
            triggers.append("task_start")
        elif intrinsic_type == "preference":
            triggers.append("preference_query")

        # relationship context trigger: surface when this entity is discussed
        if human_family:
            triggers.append(f"entity_context:{human_family}")

        trigger_pattern = ",".join(triggers) if triggers else None

        # ------------------------------------------------------------------
        # 5. Suppressors — contexts where this memory should NOT surface
        # ------------------------------------------------------------------
        suppressors = list(_SUPPRESSORS_BY_SENSITIVITY.get(sensitivity, []))

        # relationship data shouldn't leak into unrelated entity contexts
        if human_family and "unrelated_entity_context" not in suppressors:
            suppressors.append("unrelated_entity_context")

        # ------------------------------------------------------------------
        # 6. Rationale (audit trail — not stored in DB, attached to policy)
        # ------------------------------------------------------------------
        rationale = (
            f"scope={scope} from sensitivity={sensitivity!r}; "
            f"priority={priority} from stability={stability!r}+explicitness={explicitness!r}"
            f"+intrinsic_type={intrinsic_type!r}; "
            f"decay={decay_factor} from stability={stability!r}; "
            f"triggers={trigger_pattern!r}; "
            f"suppressors={suppressors}; "
            + (f"human_family={human_family!r}" if human_family else "no human_family")
        )

        logger.debug(
            "ActivationRouter.derive claim_id=%s: %s",
            claim_id[:8] if claim_id else "?",
            rationale,
        )

        return ActivationPolicy(
            claim_id=claim_id,
            owner_id=owner_id,
            scope=scope,
            trigger_pattern=trigger_pattern,
            suppressors=suppressors,
            priority=priority,
            decay_factor=decay_factor,
            rationale=rationale,
        )

    async def write(self, storage: object, policy: ActivationPolicy) -> bool:
        """Persist the activation policy to the memory_activation table.

        This is best-effort: any storage error is logged as a warning but
        does NOT propagate (the claim is already committed; activation is
        metadata). The caller should never treat a False return as a hard
        failure.

        Args:
            storage:  StorageAdapter instance (must have create_activation()).
            policy:   The policy returned by derive().

        Returns:
            True if the row was written, False on any error.
        """
        create_fn = getattr(storage, "create_activation", None)
        if create_fn is None:
            logger.debug(
                "ActivationRouter.write: storage has no create_activation — skipping "
                "(claim_id=%s)",
                policy.claim_id[:8] if policy.claim_id else "?",
            )
            return False

        try:
            await create_fn(
                claim_id=policy.claim_id,
                owner_id=policy.owner_id,
                scope=policy.scope,
                trigger_pattern=policy.trigger_pattern,
                suppressors_json=json.dumps(policy.suppressors),
                priority=policy.priority,
                decay_factor=policy.decay_factor,
            )
            logger.debug(
                "ActivationRouter.write: activation row created for claim_id=%s "
                "scope=%s priority=%d triggers=%r",
                policy.claim_id[:8] if policy.claim_id else "?",
                policy.scope,
                policy.priority,
                policy.trigger_pattern,
            )
            return True
        except Exception as exc:
            logger.warning(
                "ActivationRouter.write failed for claim_id=%s (non-fatal): %s",
                policy.claim_id[:8] if policy.claim_id else "?",
                exc,
            )
            return False


# ---------------------------------------------------------------------------
# Module-level singleton (stateless — safe to share)
# ---------------------------------------------------------------------------

_default_router = ActivationRouter()


def derive_activation(
    candidate: object,
    claim_id: str,
    owner_id: str,
) -> ActivationPolicy:
    """Module-level convenience wrapper around ActivationRouter.derive()."""
    return _default_router.derive(candidate, claim_id, owner_id)
