"""
cortex/deprecation/decay.py — Ebbinghaus Forgetting Curve

Implements time-based confidence decay using the Ebbinghaus forgetting curve
model. Used by DeprecationPipeline during dream cycle to reduce confidence
scores for memories that have not been accessed recently.

Formula:
    R(t) = e^(-t / S)

Where:
    R(t) = retention at time t (days since last access)
    S    = stability constant (related to how many times memory was reinforced)

The stability S grows with each reinforcement (access_count), implementing
the spaced repetition principle.

Reference: Ebbinghaus, H. (1885). Über das Gedächtnis.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

# Base stability in days for a memory with no reinforcements.
# A memory with stability=1.0 loses ~63% of its original confidence after 1 day.
_BASE_STABILITY: float = 1.0

# Each reinforcement multiplies stability by this factor.
# With 3 accesses → stability = 1.0 * 1.5^3 = 3.375 days.
_REINFORCEMENT_FACTOR: float = 1.5

# Maximum stability cap (days). Prevents over-accumulation.
# After ~20 reinforcements, stability plateaus near this value.
_MAX_STABILITY: float = 365.0

# Minimum retention below which confidence is set to 0 (dead memory).
_ZERO_RETENTION_THRESHOLD: float = 0.01


# ---------------------------------------------------------------------------
# Public dataclasses
# ---------------------------------------------------------------------------

@dataclass
class DecayResult:
    """Result of applying forgetting curve to a single memory."""
    claim_id: str
    original_confidence: float
    new_confidence: float
    retention: float            # 0.0–1.0, fraction of original confidence retained
    days_since_access: float
    stability: float            # effective stability S used for calculation
    decayed: bool               # True if confidence actually decreased


@dataclass
class ReinforcementResult:
    """Result of reinforcing a single memory (access event)."""
    claim_id: str
    original_confidence: float
    new_confidence: float
    new_access_count: int
    stability_before: float
    stability_after: float


# ---------------------------------------------------------------------------
# Core forgetting curve functions
# ---------------------------------------------------------------------------

def compute_stability(access_count: int) -> float:
    """
    Compute the stability S for a memory based on how many times it's been
    accessed (reinforced).

    S = BASE_STABILITY * (REINFORCEMENT_FACTOR ^ access_count)
    Capped at MAX_STABILITY.
    """
    raw = _BASE_STABILITY * (_REINFORCEMENT_FACTOR ** max(0, access_count))
    return min(raw, _MAX_STABILITY)


def compute_retention(days_since_access: float, stability: float) -> float:
    """
    Apply Ebbinghaus forgetting curve.

    R(t) = e^(-t / S)

    Returns retention in [0.0, 1.0].
    A retention of 1.0 means no forgetting. 0.0 means fully forgotten.
    """
    if days_since_access <= 0.0:
        return 1.0
    if stability <= 0.0:
        return 0.0
    retention = math.exp(-days_since_access / stability)
    return max(0.0, min(1.0, retention))


def apply_decay(
    claim_id: str,
    current_confidence: float,
    access_count: int,
    last_accessed: Optional[str],
    now: Optional[datetime] = None,
) -> DecayResult:
    """
    Apply Ebbinghaus forgetting curve to a single claim.

    Parameters
    ----------
    claim_id:           Claim identifier (for logging/reporting).
    current_confidence: Current confidence score (0.0–1.0).
    access_count:       How many times this claim has been accessed.
    last_accessed:      ISO-8601 timestamp of last access, or None.
    now:                Reference datetime (defaults to utcnow).

    Returns
    -------
    DecayResult with new_confidence and metadata.
    """
    if now is None:
        now = datetime.now(timezone.utc)

    # Compute days since last access
    if last_accessed is None:
        # Never accessed — treat as a very long time ago (worst-case stability)
        days_since_access = float("inf")
    else:
        try:
            last_dt = datetime.fromisoformat(last_accessed)
            if last_dt.tzinfo is None:
                last_dt = last_dt.replace(tzinfo=timezone.utc)
            delta = now - last_dt
            days_since_access = max(0.0, delta.total_seconds() / 86400.0)
        except (ValueError, TypeError):
            days_since_access = 0.0

    stability = compute_stability(access_count)

    if days_since_access == float("inf"):
        retention = 0.0
    else:
        retention = compute_retention(days_since_access, stability)

    # Apply retention to current confidence
    if retention <= _ZERO_RETENTION_THRESHOLD:
        new_confidence = 0.0
    else:
        new_confidence = current_confidence * retention

    # Round to avoid floating point drift
    new_confidence = round(new_confidence, 6)

    return DecayResult(
        claim_id=claim_id,
        original_confidence=current_confidence,
        new_confidence=new_confidence,
        retention=retention,
        days_since_access=days_since_access if days_since_access != float("inf") else -1.0,
        stability=stability,
        decayed=(new_confidence < current_confidence),
    )


def compute_reinforcement(
    claim_id: str,
    current_confidence: float,
    access_count: int,
    promoted_confidence: float = 0.8,
) -> ReinforcementResult:
    """
    Compute the new confidence after reinforcement (access event).

    Reinforcement:
    - Increments access_count
    - Restores confidence toward promoted_confidence if it has decayed
    - Uses max() so confidence never decreases from a reinforcement

    Parameters
    ----------
    claim_id:             Claim identifier.
    current_confidence:   Confidence before reinforcement.
    access_count:         Current access count (BEFORE this reinforcement).
    promoted_confidence:  Target confidence for promoted memories.
    """
    new_access_count = access_count + 1
    stability_before = compute_stability(access_count)
    stability_after = compute_stability(new_access_count)

    # Restore confidence: push toward promoted threshold if currently below it
    restoration_target = min(promoted_confidence, 1.0)
    new_confidence = max(current_confidence, restoration_target * 0.9)
    new_confidence = round(min(new_confidence, 1.0), 6)

    return ReinforcementResult(
        claim_id=claim_id,
        original_confidence=current_confidence,
        new_confidence=new_confidence,
        new_access_count=new_access_count,
        stability_before=stability_before,
        stability_after=stability_after,
    )
