#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Episodic Index: Lightweight conversation index (Symposium P1 — Phase 3)

Every bridge-processed interaction gets a one-line entry in the index.
Enables: "When did we last discuss X?", temporal navigation, re-extraction.

Storage: Hippocampus/Evidence/episodic_index.json
Dependencies: Python stdlib + brain_io (Law 4)
"""

import hashlib
import json
import logging
import re
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional

from pulse.core.brain_io import _acquire
from pulse.core.brain_utils import EPISODIC_DIR

logger = logging.getLogger("pulse.brain.episodic_index")

EPISODIC_INDEX_FILE = EPISODIC_DIR / "episodic_index.json"
MAX_ENTRIES = 2000       # Hard cap
ROLLING_DAYS = 30        # Graduate (not delete) entries older than this

# Stop words for keyword extraction
_STOP_WORDS = frozenset({
    "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
    "have", "has", "had", "do", "does", "did", "will", "would", "could",
    "should", "may", "might", "shall", "can", "need", "must", "to", "of",
    "in", "for", "on", "with", "at", "by", "from", "as", "into", "about",
    "that", "this", "it", "its", "and", "or", "but", "not", "no", "if",
    "so", "then", "than", "too", "very", "just", "also", "now", "here",
    "there", "when", "where", "how", "what", "who", "which", "all", "each",
    "both", "few", "more", "most", "other", "some", "such", "only", "own",
    "same", "up", "out", "we", "you", "i", "me", "my", "our", "your",
    "he", "she", "they", "them", "his", "her", "ok", "okay", "yeah",
    "yes", "bro", "lol", "like", "well", "sure", "right", "got", "get",
    "let", "lets", "go", "going", "make", "see", "thing", "things",
    "want", "dont", "please", "thanks", "good", "one", "two",
})

# Conversational starters to strip from summaries
_FILLER_START = re.compile(
    r"^(?:ok|okay|sure|yeah|yes|bro|well|so|hmm|hm|ah|oh|"
    r"alright|fine|good|great|nice|cool|yep|nope|nah|right|lol|lmao)[,.\s!]*",
    re.IGNORECASE
)

# Entity pattern (capitalized words, URLs, services)
_ENTITY = re.compile(r"(?:[A-Z][a-zA-Z0-9]{2,}|https?://\S+|\w+\.\w+\.\w+)")


def _extract_keywords(text: str, top_n: int = 8) -> List[str]:
    """Extract top keywords by frequency (simple TF, no library)."""
    words = re.findall(r"[a-z][a-z0-9_]+", text.lower())
    words = [w for w in words if w not in _STOP_WORDS and len(w) > 2]
    freq = {}
    for w in words:
        freq[w] = freq.get(w, 0) + 1
    ranked = sorted(freq, key=freq.get, reverse=True)
    return ranked[:top_n]


def _extract_entities(text: str) -> List[str]:
    """Extract named entities (capitalized words, URLs)."""
    entities = []
    for m in _ENTITY.finditer(text):
        e = m.group(0).strip(".,;:()")
        if len(e) >= 3 and e.lower() not in _STOP_WORDS:
            entities.append(e)
    return list(dict.fromkeys(entities))[:10]  # dedup, cap at 10


# P47: Prompt signatures that indicate LLM task prompts, not real user text
_PROMPT_SIGNATURES = (
    "You are a ", "You are the ", "You are an ",
    "Analyze this ", "Extract ", "Given the following",
    "[EXTRACT_KNOWLEDGE]", "[CLEAN_INTENTS]",
    "SYSTEM:", "<system",
    "<task-notification>", "<task-", "<tool-use-id>",
)

def _clean_summary(user_text: str) -> str:
    """Create a clean summary from user text. Filters LLM prompt echoes."""
    text = user_text.strip()
    # P47 FIX: Skip LLM prompt echoes that pollute episodic index
    if any(text.startswith(sig) for sig in _PROMPT_SIGNATURES):
        return ""
    summary = _FILLER_START.sub("", text)
    return summary[:120].strip() or text[:120].strip()


def _entry_fingerprint(agent: str, offset: int, timestamp: str) -> str:
    """Dedup key for episodic entries."""
    key = f"{agent}:{offset}:{timestamp[:19]}"
    return hashlib.md5(key.encode()).hexdigest()[:12]


def load_index() -> List[Dict]:
    """Load episodic index. Returns empty list if missing."""
    if not EPISODIC_INDEX_FILE.exists():
        return []
    try:
        data = json.loads(EPISODIC_INDEX_FILE.read_text(encoding="utf-8"))
        return data if isinstance(data, list) else []
    except (json.JSONDecodeError, OSError):
        return []


def _graduate_episodes(entries: list):
    """Move valuable expired episodes to graduated_episodes.jsonl instead of deleting.

    Episodic-to-semantic graduation: high-emotion or entity-rich entries
    survive past the rolling window as compressed records.
    """
    grad_file = EPISODIC_DIR / "graduated_episodes.jsonl"
    grad_file.parent.mkdir(parents=True, exist_ok=True)
    try:
        with open(grad_file, "a", encoding="utf-8") as f:
            for e in entries:
                compact = {
                    "timestamp": e.get("timestamp", ""),
                    "agent": e.get("agent", ""),
                    "summary": e.get("summary", "")[:300],
                    "topic_keywords": e.get("topic_keywords", [])[:10],
                    "entities_mentioned": e.get("entities_mentioned", [])[:10],
                    "emotional_intensity": e.get("emotional_intensity", 0),
                    "graduated_at": datetime.now(timezone.utc).isoformat(),
                }
                f.write(json.dumps(compact, ensure_ascii=False) + "\n")
    except Exception:
        pass  # Never block episodic append over graduation failure


def _also_write_episodic_sqlite(agent, summary, keywords, entities, timestamp, session_id):
    """P47: Mirror episodic index entry to SQLite episodic table."""
    try:
        from pulse.brain.index.episodic_writer import save_episode
        # Dedup and merge keywords + entities
        key_events = list(dict.fromkeys(
            (keywords[:5] if keywords else []) + (entities[:5] if entities else [])
        ))
        save_episode(
            agent=agent or "unknown",
            session_summary=summary[:500] if summary else "",
            key_events=key_events[:10] if key_events else None,
            domain="general",
            session_id=session_id or agent or "unknown",
            timestamp=timestamp,
        )
    except Exception as e:
        logger.warning("Episodic SQLite mirror failed: %s", e)


def append_entry(
    agent: str,
    user_text: str,
    agent_text: str,
    wants_count: int = 0,
    facts_count: int = 0,
    emotional_intensity: float = 0.0,
    capture_log_offset: int = 0,
    context: Optional[Dict] = None,
) -> Optional[Dict]:
    """Append one episodic entry. Returns the entry or None if deduped."""
    EPISODIC_INDEX_FILE.parent.mkdir(parents=True, exist_ok=True)
    now = datetime.now(timezone.utc).isoformat()
    combined = f"{user_text}\n{agent_text}"

    entry = {
        "timestamp": now,
        "agent": agent.lower(),
        "topic_keywords": _extract_keywords(combined),
        "entities_mentioned": _extract_entities(combined),
        "emotional_intensity": round(emotional_intensity, 2),
        "wants_count": wants_count,
        "facts_count": facts_count,
        "capture_log_offset": capture_log_offset,
        "summary": _clean_summary(user_text),
        "_fp": _entry_fingerprint(agent, capture_log_offset, now),
    }
    if context:
        entry["context"] = context

    with _acquire(EPISODIC_INDEX_FILE):
        index = load_index()
        existing_fps = {e.get("_fp") for e in index}
        if entry["_fp"] in existing_fps:
            return None

        index.append(entry)

        # P47: mirror to SQLite episodic table (single call, inside lock)
        session_id = context.get("session_id") if context else agent
        _also_write_episodic_sqlite(
            agent, entry.get("summary", ""),
            entry.get("topic_keywords", []),
            entry.get("entities_mentioned", []),
            now, session_id)

        # Rolling window: graduate valuable old entries instead of deleting
        cutoff = (datetime.now(timezone.utc) - timedelta(days=ROLLING_DAYS)).isoformat()
        kept = []
        graduated = []
        for e in index:
            if e.get("timestamp", "") >= cutoff:
                kept.append(e)
            elif (e.get("emotional_intensity", 0) > 0.3
                  or len(e.get("entities_mentioned", [])) > 2
                  or len(e.get("topic_keywords", [])) > 3):
                graduated.append(e)
            # else: truly low-value, discard
        if graduated:
            _graduate_episodes(graduated)
        index = kept

        # Quality-based cap: evict lowest-quality when over MAX_ENTRIES
        # (keeps high-emotion, high-entity entries over low-value filler)
        if len(index) > MAX_ENTRIES:
            def _quality(e):
                return (e.get("emotional_intensity", 0) * 2.0
                        + (e.get("wants_count", 0) + e.get("facts_count", 0)) * 0.5
                        + len(e.get("entities_mentioned", [])) * 0.2
                        + len(e.get("topic_keywords", [])) * 0.1)
            index.sort(key=_quality, reverse=True)
            index = index[:MAX_ENTRIES]
            index.sort(key=lambda e: e.get("timestamp", ""))  # restore chronological order

        EPISODIC_INDEX_FILE.write_text(
            json.dumps(index, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )

    return entry

