"""Proactive Memory Primer (PMP) — in-memory dict cache.

Adapted from Redis-based implementation to use a module-level dict cache.
Memory loading uses SQLite-adapted episodic and semantic memory modules.
"""

import json
import logging
import time
from datetime import datetime, timezone
from pydantic import BaseModel

from soagila.db.event_bus import publish_event
from soagila.core.tcmg import episodic_memory, semantic_memory
from soagila.core.tcmg.memory_models import EpisodicMemory, SemanticMemory
from soagila.core.tcmg.embedding_service import generate_embedding

logger = logging.getLogger("soagila.pmp.primer")

CHANNEL = "soagila:metrics"
PRIME_DAYS_LOOKBACK = 7           # Days of episodic history to load


def _pmp_config():
    """Get PMP settings from config (avoids hardcoded constants)."""
    from soagila.config import get_settings
    s = get_settings()
    return s.pmp_cache_ttl, s.pmp_relevance_threshold, s.pmp_max_episodic

# In-memory cache: user_id -> (cache_data_dict, expires_at_timestamp)
_prime_cache: dict[str, tuple[dict, float]] = {}

# Utilization tracking: which primed memories were actually recalled
_primed_ids: set[str] = set()
_recalled_primed: set[str] = set()


def mark_primed_ids(memory_ids: list[str]):
    """Called when memories are loaded into prime cache."""
    _primed_ids.update(memory_ids)


def mark_recalled_from_prime(memory_ids: list[str]):
    """Called by recall when PMP cache is used."""
    for mid in memory_ids:
        if mid in _primed_ids:
            _recalled_primed.add(mid)


def get_prime_utilization() -> float:
    """What fraction of primed memories were actually recalled?"""
    if not _primed_ids:
        return 0.0
    return len(_recalled_primed) / len(_primed_ids)


def reset_utilization():
    """Reset utilization tracking (called after metrics persist)."""
    _primed_ids.clear()
    _recalled_primed.clear()


class PrimingResult(BaseModel):
    user_id: str
    primed: bool = False
    episodic_count: int = 0
    semantic_count: int = 0
    total_primed: int = 0
    from_cache: bool = False
    trigger: str = "manual"  # manual / session_start / context_hint


async def check_if_primed(user_id: str) -> bool:
    """Check if user's memories are already primed in cache."""
    entry = _prime_cache.get(user_id)
    if entry is None:
        return False
    _, expires_at = entry
    ttl, _, _ = _pmp_config()
    if time.time() > expires_at:
        del _prime_cache[user_id]
        return False
    return True


async def prime_for_user(user_id: str, trigger: str = "manual") -> PrimingResult:
    """
    Proactively load relevant memories into in-memory cache.
    - Recent episodic memories (last 7 days, relevance > 0.3)
    - All semantic memories for user
    """
    result = PrimingResult(user_id=user_id, trigger=trigger)

    ttl, threshold, max_ep = _pmp_config()

    # Check if already primed
    if await check_if_primed(user_id):
        result.primed = True
        result.from_cache = True
        cache_data, _ = _prime_cache[user_id]
        _prime_cache[user_id] = (cache_data, time.time() + ttl)
        logger.debug(f"User {user_id} already primed (cache hit, TTL refreshed)")
        return result

    # Load episodic memories
    episodic_mems = await episodic_memory.get_user_memories(
        user_id,
        limit=max_ep,
        min_relevance=threshold,
    )
    result.episodic_count = len(episodic_mems)

    # Load semantic memories
    semantic_mems = await semantic_memory.get_user_profile(user_id)
    result.semantic_count = len(semantic_mems)

    result.total_primed = result.episodic_count + result.semantic_count

    if result.total_primed == 0:
        result.primed = False
        return result

    # Build cache payload
    cache_data = {
        "user_id": user_id,
        "primed_at": datetime.now(timezone.utc).isoformat(),
        "trigger": trigger,
        "episodic": [m.model_dump(mode="json") for m in episodic_mems],
        "semantic": [m.model_dump(mode="json") for m in semantic_mems],
    }

    # Store in dict cache with TTL
    _prime_cache[user_id] = (cache_data, time.time() + ttl)

    # Track primed memory IDs for utilization metrics
    all_ids = [m.id for m in episodic_mems] + [m.id for m in semantic_mems]
    mark_primed_ids(all_ids)

    result.primed = True
    result.from_cache = False

    # Publish priming event
    event = json.dumps({
        "type": "memory_primed",
        "user_id": user_id,
        "episodic_count": result.episodic_count,
        "semantic_count": result.semantic_count,
        "trigger": trigger,
        "timestamp": datetime.now(timezone.utc).isoformat(),
    })
    await publish_event(CHANNEL, event)

    logger.info(
        f"Primed user {user_id}: {result.episodic_count} episodic, "
        f"{result.semantic_count} semantic (trigger={trigger})"
    )

    return result


async def prime_for_context(
    user_id: str,
    context_hint: str,
    trigger: str = "context_hint",
) -> PrimingResult:
    """
    Context-aware priming: load memories relevant to a specific topic.
    Uses embedding similarity to select the most relevant memories.
    """
    result = PrimingResult(user_id=user_id, trigger=trigger)
    ttl, threshold, _ = _pmp_config()

    # Generate embedding for context hint
    hint_embedding = generate_embedding(context_hint)

    # Load all user memories and filter by similarity
    episodic_mems = await episodic_memory.get_user_memories(
        user_id, limit=50, min_relevance=0.1,
    )

    # Score by similarity to context hint
    scored_episodic = []
    for mem in episodic_mems:
        if mem.embedding:
            from soagila.core.tcmg.embedding_service import cosine_similarity
            sim = cosine_similarity(hint_embedding, mem.embedding)
            if sim > threshold:
                mem.metadata["context_similarity"] = sim
                scored_episodic.append(mem)

    scored_episodic.sort(
        key=lambda m: m.metadata.get("context_similarity", 0),
        reverse=True,
    )
    top_episodic = scored_episodic[:15]

    # Load semantic memories
    semantic_mems = await semantic_memory.get_user_profile(user_id)

    result.episodic_count = len(top_episodic)
    result.semantic_count = len(semantic_mems)
    result.total_primed = result.episodic_count + result.semantic_count

    if result.total_primed == 0:
        result.primed = False
        return result

    # Build cache payload
    cache_data = {
        "user_id": user_id,
        "primed_at": datetime.now(timezone.utc).isoformat(),
        "trigger": trigger,
        "context_hint": context_hint,
        "episodic": [m.model_dump(mode="json") for m in top_episodic],
        "semantic": [m.model_dump(mode="json") for m in semantic_mems],
    }

    # Store in dict cache (overwrite any existing prime)
    _prime_cache[user_id] = (cache_data, time.time() + ttl)

    result.primed = True
    result.from_cache = False

    logger.info(
        f"Context-primed user {user_id} for '{context_hint[:50]}': "
        f"{result.episodic_count} episodic, {result.semantic_count} semantic"
    )

    return result


async def get_primed_context(user_id: str) -> dict | None:
    """
    Get primed memories from cache.
    Returns dict with 'episodic' and 'semantic' lists, or None if not primed.
    """
    entry = _prime_cache.get(user_id)
    if entry is None:
        return None

    cache_data, expires_at = entry
    ttl, _, _ = _pmp_config()
    if time.time() > expires_at:
        del _prime_cache[user_id]
        return None

    # Refresh TTL on access
    _prime_cache[user_id] = (cache_data, time.time() + ttl)

    # Reconstruct memory objects
    episodic_mems = [EpisodicMemory(**m) for m in cache_data.get("episodic", [])]
    semantic_mems = [SemanticMemory(**m) for m in cache_data.get("semantic", [])]

    return {
        "episodic": episodic_mems,
        "semantic": semantic_mems,
        "primed_at": cache_data.get("primed_at"),
        "trigger": cache_data.get("trigger"),
        "context_hint": cache_data.get("context_hint"),
    }


async def invalidate_prime(user_id: str):
    """Remove primed cache for a user."""
    if user_id in _prime_cache:
        del _prime_cache[user_id]
    logger.debug(f"Invalidated prime cache for user {user_id}")
