#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Dimension D: Temporal Reasoning for Brain Knowledge.

Type-aware decay curves replace the old flat 0.5%/day model.
Each knowledge type has a half-life tuned to its expected lifespan.

Public API (5 functions per SoC):
  - temporal_confidence() — exponential decay with type-aware half-life
  - get_half_life() — lookup half-life for a knowledge type
  - validity_transition() — state machine for knowledge validity
  - reinforce() — reset last_reinforced on retrieval/merge
  - stamp_temporal_fields() — initialize D-fields on new items
"""
import math
from datetime import datetime, timezone, timedelta

# === HALF-LIFE TABLE (days) ===
# Each knowledge type decays at its own rate.
# Half-life = time for confidence to drop to 50% of its base value.
HALF_LIVES: dict[str, float] = {
    "fact": 60,         # Technology changes fast
    "lesson": 180,      # Methodological knowledge evolves slowly
    "failure": 180,     # Failure lessons persist like lessons
    "pattern": 365,     # Structural insights are long-lived
    "anti_pattern": 365, # What NOT to do is stable
    "procedure": 120,   # Workflows change with tooling
    "decision": 90,     # Context that drove decisions shifts
    "prediction": 3,    # Already handled by prediction_daemon
    "episodic": 30,     # Raw recall fades, extracted lessons survive
    "schema": 365,      # Emergent structural knowledge is durable
    "private": 180,     # Same as lessons
}

DEFAULT_HALF_LIFE = 180  # Fallback for unknown types

# === VALIDITY STATES ===
VALID = "valid"
STABLE = "stable"       # Item 49 (P13): E-LTP->L-LTP intermediate; 2x half-life, supersedable
SUPERSEDED = "superseded"
EXPIRED = "expired"
HISTORICAL = "historical"
CONTESTED = "contested"

_VALID_STATES = {VALID, STABLE, SUPERSEDED, EXPIRED, HISTORICAL, CONTESTED}

# Legal state transitions
_TRANSITIONS: dict[str, set[str]] = {
    VALID: {STABLE, SUPERSEDED, EXPIRED, HISTORICAL, CONTESTED},
    STABLE: {SUPERSEDED, HISTORICAL, CONTESTED},  # Item 49: stable -> superseded allowed
    CONTESTED: {VALID, HISTORICAL},
    SUPERSEDED: {HISTORICAL},
    EXPIRED: {VALID},      # Can be revived by reinforcement
    HISTORICAL: set(),     # Terminal state
}


def get_half_life(knowledge_type: str) -> float:
    """Return half-life in days for a knowledge type."""
    return HALF_LIVES.get(knowledge_type, DEFAULT_HALF_LIFE)


def temporal_confidence(base_confidence: float, days_old: float = 0.0,
                        half_life_days: float | None = None,
                        knowledge_type: str = "lesson",
                        timestamp: str | None = None,
                        retrieval_count: int = 0,
                        arousal: float = 0.0,
                        **kwargs) -> float:
    """Exponential decay with type-aware half-life.

    Args:
        base_confidence: Original confidence value (0.0-1.0).
        days_old: Days since creation or last reinforcement.
        half_life_days: Override half-life (uses type lookup if None).
        knowledge_type: Used for half-life lookup when half_life_days is None.
        timestamp: ISO 8601 timestamp — auto-computes days_old if provided.
        retrieval_count: Number of times retrieved. Increases half-life.
        arousal: Emotional arousal (0.0-1.0). High arousal extends half-life
                 by up to 1.5x (McGaugh 2004: emotional memories persist longer).

    Returns:
        Decayed confidence value, floored at 0.01.
    """
    if timestamp and days_old <= 0:
        days_old = _days_since(timestamp)
    if days_old <= 0:
        return base_confidence
        
    hl = half_life_days if half_life_days is not None else get_half_life(knowledge_type)
    
    # Phase 2C: Forgetting Curve Redesign
    retrieval_count = max(0, int(retrieval_count))
    hl_effective = hl * (1.0 + (retrieval_count ** 0.5))

    # Item 28: Overlearning resistance — well-practiced knowledge (ec>=10) doubles half-life
    ec = kwargs.get("evidence_count", 1)
    if ec >= 10:
        hl_effective *= 2  # Overlearning resistance

    # Phase 4+: Emotional arousal extends half-life (McGaugh 2004)
    if arousal > 0.5:
        hl_effective *= 1.0 + (arousal - 0.5) * 1.0  # 1.0x at 0.5, 1.5x at 1.0

    if hl_effective <= 0:
        return 0.01
    # N42: Power-law decay (Wixted & Ebbesen 1991) — slower initial fade,
    # more gradual long-term decay than Ebbinghaus exponential.
    # decay_factor = (1 + days_old / hl_effective) ** -0.5
    decay_factor = (1.0 + days_old / hl_effective) ** (-0.5)
    return max(0.01, round(base_confidence * decay_factor, 4))


def _temporal_salience(salience: float, last_accessed: str,
                      half_life_hours: float = 48.0) -> float:
    """Ebbinghaus forgetting curve applied to salience (memory F26).

    Decays salience based on time since last access. Items not recently
    accessed fade from working relevance, regardless of confidence.

    Args:
        salience: Current salience value.
        last_accessed: ISO 8601 timestamp of last access.
        half_life_hours: Hours until salience halves. Default 48h.

    Returns:
        Decayed salience, floored at 0.1.
    """
    try:
        days = _days_since(last_accessed)
        if days <= 0:
            return salience
        hours_elapsed = days * 24.0
        decay = math.exp(-0.693 * hours_elapsed / half_life_hours)  # ln(2)=0.693
        return max(0.1, round(salience * decay, 4))
    except Exception:
        return salience


def _days_since(iso_timestamp: str) -> float:
    """Calculate days elapsed since an ISO 8601 timestamp."""
    try:
        ts = iso_timestamp.replace("Z", "+00:00")
        dt = datetime.fromisoformat(ts)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        delta = datetime.now(timezone.utc) - dt
        return max(0.0, delta.total_seconds() / 86400)
    except (ValueError, TypeError, AttributeError):
        return 0.0


def _compute_expiry(created_at: str, half_life_days: float) -> str:
    """Compute expiration date: created + 2 * half_life.

    Items not reinforced within 2x their half-life are candidates
    for expiration (confidence will be ~25% of original).
    """
    try:
        ts = created_at.replace("Z", "+00:00")
        dt = datetime.fromisoformat(ts)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        expiry = dt + timedelta(days=2 * half_life_days)
        return expiry.isoformat()
    except (ValueError, TypeError, AttributeError):
        return ""


def _is_expired(item: dict) -> bool:
    """Check if an item should transition to expired state.

    Criteria: temporal confidence < 0.15 AND no retrieval in 2x half-life.
    """
    validity = item.get("validity", VALID)
    if validity != VALID:
        return False

    knowledge_type = item.get("type", "lesson")
    hl = item.get("half_life_days", get_half_life(knowledge_type))

    # Use last_reinforced if available, else timestamp
    anchor = item.get("last_reinforced") or item.get("timestamp", "")
    if not anchor:
        return False

    age = _days_since(anchor)
    base_conf = item.get("confidence", 0.5)
    current = temporal_confidence(base_conf, age, hl)

    return current < 0.15 and age > 2 * hl


def validity_transition(current_state: str, target_state: str) -> str | None:
    """Attempt a validity state transition.

    Returns the new state if the transition is legal, None otherwise.
    """
    if current_state not in _VALID_STATES or target_state not in _VALID_STATES:
        return None
    if target_state in _TRANSITIONS.get(current_state, set()):
        return target_state
    return None


def reinforce(item: dict) -> dict:
    """Mark an item as reinforced (resets decay clock).

    Called on retrieval (search hit) or consolidation merge.
    If the item was expired, revive it back to valid.
    """
    now = datetime.now(timezone.utc).isoformat()
    item["last_reinforced"] = now

    # Revive expired items on reinforcement
    if item.get("validity") == EXPIRED:
        item["validity"] = VALID

    # Recompute expiry from reinforcement point
    hl = item.get("half_life_days")
    if hl:
        item["expires_at"] = _compute_expiry(now, hl)

    return item


temporal_salience = _temporal_salience

def _promote_to_stable(item: dict) -> dict:
    """Item 49 (P13): E-LTP to L-LTP -- promote valid items to stable when strongly consolidated.

    Criteria: validity == valid AND evidence_count >= 5 AND confidence >= 0.7.
    Effect: sets validity = stable; caller should double half_life_days.
    Fail-open: returns item unchanged on any error.
    """
    try:
        if (item.get("validity") == VALID
                and int(item.get("evidence_count", 0)) >= 5
                and float(item.get("confidence", 0)) >= 0.7):
            item["validity"] = STABLE
            # 2x half-life for stable items
            hl = item.get("half_life_days")
            if hl:
                item["half_life_days"] = float(hl) * 2.0
    except Exception:
        pass
    return item


promote_to_stable = _promote_to_stable

def stamp_temporal_fields(item: dict, knowledge_type: str | None = None) -> dict:
    """Add temporal metadata to a new knowledge item.

    Called by save_writers at write time to initialize all D-fields.
    """
    ktype = knowledge_type or item.get("type", "lesson")
    hl = get_half_life(ktype)
    now = datetime.now(timezone.utc).isoformat()
    created = item.get("timestamp", now)

    item.setdefault("validity", VALID)
    item["half_life_days"] = hl
    item.setdefault("last_reinforced", created)
    item.setdefault("superseded_by", None)
    item["expires_at"] = _compute_expiry(created, hl)

    return item
