"""Internal helpers shared across read pipeline modules.

Utility functions used by spreading activation, query expansion,
and future full retrieval (Part 12). Prefixed with ``_`` to indicate
this is an internal module — not part of the public API.
"""

from __future__ import annotations

from datetime import UTC, datetime

__all__ = ["compute_effective_confidence", "is_fact_active", "is_fact_stale", "is_key_allowed"]

DECAY_RATE_PER_WEEK = 0.95
DECAY_FLOOR = 0.10


def compute_effective_confidence(
    base_confidence: float,
    valid_from: datetime | None,
    now: datetime | None = None,
) -> float:
    """Apply temporal decay to fact confidence.

    Formula: base_confidence * 0.95^weeks_since_valid_from.
    ~23% loss after 30 days, ~50% after 90 days. Floor at 0.10.

    Args:
        base_confidence: Original confidence value (0.0-1.0).
        valid_from: When the fact was first recorded.
        now: Current timestamp (defaults to UTC now).

    Returns:
        Decayed confidence, floored at 0.10.
    """
    if now is None:
        now = datetime.now(UTC)
    if valid_from is None:
        return base_confidence
    if valid_from.tzinfo is None:
        valid_from = valid_from.replace(tzinfo=UTC)
    days_since = max(0, (now - valid_from).total_seconds() / 86400)
    weeks = days_since / 7
    decay = DECAY_RATE_PER_WEEK**weeks
    return float(max(DECAY_FLOOR, base_confidence * decay))


def is_fact_active(fact: object, now: datetime | None = None) -> bool:
    """Check whether a memory fact is currently active (not expired/stale).

    A fact is active if:
    - ``valid_to`` is None (no expiry set), AND
    - ``is_stale`` is False (not marked stale by memify)

    Args:
        fact: A MemoryFact-like object with ``valid_to`` and ``is_stale`` attrs.
        now: Current timestamp (used for future TTL checks).

    Returns:
        True if the fact is active.
    """
    valid_to = getattr(fact, "valid_to", None)
    if valid_to is not None:
        if now is None:
            now = datetime.now(UTC)
        if valid_to <= now:
            return False

    is_stale = getattr(fact, "is_stale", False)
    if is_stale:
        return False

    return True


def is_fact_stale(
    fact: object,
    now: datetime,
    *,
    stale_keys: set[str] | None = None,
    stale_threshold_days: int = 90,
) -> bool:
    """Check whether a memory fact should be considered stale.

    A fact is stale if:
    - ``is_stale`` flag is True, OR
    - ``attribute_key`` is in ``stale_keys``, OR
    - ``valid_from`` is older than ``stale_threshold_days``

    Args:
        fact: A MemoryFact-like object.
        now: Current timestamp.
        stale_keys: Set of attribute keys considered always-stale.
        stale_threshold_days: Days after which a fact is stale.

    Returns:
        True if the fact is stale.
    """
    if getattr(fact, "is_stale", False):
        return True

    attr_key = getattr(fact, "attribute_key", None)
    if stale_keys and attr_key and attr_key in stale_keys:
        return True

    valid_from = getattr(fact, "valid_from", None)
    if valid_from is not None:
        if valid_from.tzinfo is None:
            valid_from = valid_from.replace(tzinfo=UTC)
        age = (now - valid_from).days
        if age > stale_threshold_days:
            return True

    return False


def is_key_allowed(attribute_key: str | None, allowed_keys: set[str] | None) -> bool:
    """Check whether an attribute key is in the allowed set.

    If ``allowed_keys`` is None or empty, all keys are allowed (open mode).

    Args:
        attribute_key: The attribute key to check.
        allowed_keys: Set of allowed keys, or None for open mode.

    Returns:
        True if the key is allowed.
    """
    if not allowed_keys:
        return True
    if not attribute_key:
        return True
    return attribute_key in allowed_keys
