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

Primary path: SQLite query against episodic table (when table exists).
Fallback: flat-file read of episodic_index.json.

Returns salience-scored results matching the brain_search contract.

Dependencies: Python stdlib + episodic_index + brain_db (Law 4)
"""

import hashlib
import logging

from pulse.brain.index.episodic_index import EPISODIC_INDEX_FILE, load_index

log = logging.getLogger("pulse.search.episodic")


class _TableEmpty(Exception):
    """Raised when the SQLite table has 0 rows — signals caller to fall back."""


def _search_episodic_sqlite(query: str) -> list:
    """Primary path: SQLite query against episodic table.

    Raises _TableEmpty if the table doesn't exist or has 0 rows.
    Phase 4 will create and populate the episodic table; until then
    this always raises _TableEmpty, triggering the flat-file fallback.
    """
    from pulse.core.brain_db import get_conn

    conn = get_conn()

    # Check if episodic table exists and has rows
    try:
        row_count = conn.execute(
            "SELECT COUNT(*) FROM episodic"
        ).fetchone()[0]
    except Exception:
        # Table doesn't exist yet — fall back
        raise _TableEmpty()

    if row_count == 0:
        raise _TableEmpty()

    query_lower = query.lower().strip()
    if not query_lower:
        return []

    words = query_lower.split()
    if not words:
        return []

    # FTS5 candidate retrieval (OR for broad recall, score in Python)
    fts_expr = " OR ".join(f'"{w}"' for w in words[:10])
    try:
        candidates = conn.execute("""
            SELECT f.item_id, f.content
            FROM knowledge_fts f
            WHERE f.table_name = 'episodic'
              AND knowledge_fts MATCH ?
            LIMIT 50
        """, (fts_expr,)).fetchall()
    except Exception as e:
        log.debug("FTS5 episodic query failed: %s", e)
        return []

    if not candidates:
        return []

    id_list = [c["item_id"] for c in candidates]
    placeholders = ",".join("?" for _ in id_list)
    rows = conn.execute(
        f"SELECT * FROM episodic WHERE id IN ({placeholders})", id_list
    ).fetchall()

    rows_by_id = {r["id"]: r for r in rows}
    hits = []

    for cand in candidates:
        row = rows_by_id.get(cand["item_id"])
        if not row:
            continue

        content = row["summary"] or ""
        agent = row["agent"] or ""
        ts = (row["timestamp"] or "")[:19]

        # Score using the same signals as flat-file path
        score = 0.0
        content_lower = content.lower()

        # Entity-like match (word in content)
        for word in words:
            if len(word) >= 3 and word in content_lower:
                score += 3.0
                break

        # Keyword overlap
        content_words = set(content_lower.split())
        overlap = len(set(words) & content_words)
        score += overlap * 0.5

        # Agent name match
        if agent.lower() in query_lower:
            score += 1.0

        if score < 0.5:
            continue

        # P47 FIX: Raised from 0.35 to 0.85 — recent conversations MUST surface.
        # Old weight (0.35) buried episodic results under lessons, causing session amnesia.
        _CONVERSATIONAL_WEIGHT = 0.85
        salience = round(min(score * 0.8, 10.0) * _CONVERSATIONAL_WEIGHT, 3)
        ep_key = (row["timestamp"] or content)[:40]
        hits.append({
            "id": f"EP-{hashlib.md5(ep_key.encode()).hexdigest()[:8]}",
            "text": f"[{ts}] {agent}: {content[:80]}",
            "title": f"[EPISODE] {content[:60]}",
            "salience": salience,
            "confidence": float(row["confidence"] or 0.8) if "confidence" in row.keys() else 0.85,
            "source": "episodic",
            "agent": agent,
            "timestamp": row["timestamp"] or "",
            "entities": [],
            "emotional_intensity": 0,
        })

    hits.sort(key=lambda h: h["salience"], reverse=True)
    return hits[:10]


def _search_episodic_flatfile(query: str) -> list:
    """Fallback: full JSON load from episodic_index.json (original implementation)."""
    if not EPISODIC_INDEX_FILE.exists():
        return []

    index = load_index()
    if not index:
        return []

    query_lower = query.lower().strip()
    if not query_lower:
        return []

    query_words = set(query_lower.split())
    hits = []

    for entry in index:
        score = 0.0
        entities = [e.lower() for e in entry.get("entities_mentioned", [])]
        keywords = entry.get("topic_keywords", [])
        summary = entry.get("summary", "").lower()
        agent = entry.get("agent", "")

        # Entity match (strongest)
        for entity in entities:
            if entity in query_lower or query_lower in entity:
                score += 3.0
                break

        # Keyword overlap
        kw_set = set(keywords)
        overlap = len(query_words & kw_set)
        score += overlap * 0.5

        # Summary text match
        for qw in query_words:
            if len(qw) > 2 and qw in summary:
                score += 0.3

        # Agent name match ("what did claude say about X")
        if agent in query_lower:
            score += 1.0

        if score < 0.5:
            continue

        # Build hit
        # P47 FIX: Raised from 0.35 to 0.85 (consistent with SQLite path)
        _CONVERSATIONAL_WEIGHT = 0.85
        salience = round(min(score * 0.8, 10.0) * _CONVERSATIONAL_WEIGHT, 3)
        entities_display = entry.get("entities_mentioned", [])[:5]
        ts = entry.get("timestamp", "")[:19]

        ep_key = (entry.get("timestamp", "") or entry.get("summary", ""))[:40]
        hits.append({
            "id": f"EP-{hashlib.md5(ep_key.encode()).hexdigest()[:8]}",
            "text": f"[{ts}] {agent}: {entry.get('summary', '')[:80]}",
            "title": f"[EPISODE] {entry.get('summary', '')[:60]}",
            "salience": salience,
            "confidence": 0.8,
            "source": "episodic",
            "agent": agent,
            "timestamp": entry.get("timestamp", ""),
            "entities": entities_display,
            "emotional_intensity": entry.get("emotional_intensity", 0),
        })

    hits.sort(key=lambda h: h["salience"], reverse=True)
    return hits[:10]


def search_episodic(query: str, config: dict = None) -> list:
    """Search episodic memory matching query.

    Primary: SQLite query against `episodic` table (fast indexed lookup).
    Fallback: flat-file read of episodic_index.json if SQLite table is empty/missing.

    Returns list of hit dicts compatible with brain_search format.
    """
    try:
        return _search_episodic_sqlite(query)
    except _TableEmpty:
        log.debug("SQLite episodic table empty/missing, falling back to flat-file")
    except Exception as e:
        log.debug("SQLite episodic search failed, falling back to flat-file: %s", e)

    # Fallback: flat-file read (graceful degradation)
    return _search_episodic_flatfile(query)
