#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Search Source: Declarative Facts (Symposium P0)

Primary path: SQLite FTS5 query against the `facts` table.
Fallback: flat-file read of Hippocampus/Semantic/auto_facts.json.

Scoring: entity_match*3.0 + text_similarity*2.0 + domain_match*0.5
         + fact_type_match*0.3 + keyword_overlap*0.3

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

import logging

from pulse.core.brain_utils import FACTS_FILE
from pulse.core.brain_analysis import text_similarity

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


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


def _search_facts_sqlite(query: str) -> list:
    """Primary path: FTS5 candidate retrieval + Python scoring.

    Returns list of hits, or empty list.
    Raises _TableEmpty if facts table has 0 rows (caller should fall back).
    """
    from pulse.core.brain_db import get_conn

    conn = get_conn()

    # Quick row-count check — if table is empty, signal caller to fall back
    row_count = conn.execute("SELECT COUNT(*) FROM facts").fetchone()[0]
    if row_count == 0:
        raise _TableEmpty()

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

    # Build FTS5 match expression from query words
    # FTS5 uses implicit AND by default; quote each token to avoid syntax errors
    words = query_lower.split()
    if not words:
        return []

    # Use OR to get a broad candidate set, then score in Python
    fts_expr = " OR ".join(f'"{w}"' for w in words[:10])

    try:
        # Get candidate IDs via FTS5, limit to 50 for scoring
        candidates = conn.execute("""
            SELECT f.item_id, f.content, f.domain
            FROM knowledge_fts f
            WHERE f.table_name = 'facts'
              AND knowledge_fts MATCH ?
            LIMIT 50
        """, (fts_expr,)).fetchall()
    except Exception as e:
        log.debug("FTS5 facts query failed: %s", e)
        return []

    if not candidates:
        return []

    # Collect candidate IDs to fetch full rows from facts table
    id_list = [c["item_id"] for c in candidates]
    placeholders = ",".join("?" for _ in id_list)
    rows = conn.execute(
        f"SELECT * FROM facts WHERE id IN ({placeholders})", id_list
    ).fetchall()

    # Index by id for fast lookup
    rows_by_id = {r["id"]: r for r in rows}

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

        content = row["content"] or ""
        domain = row["domain"] or "general"
        # facts table doesn't store entities/fact_type separately;
        # they live in the original JSON. Use content-based matching.
        fact_type = row["type"] or ""

        score = 0.0

        # Entity-like match: check if query appears in content or vice versa
        # (approximation — entities aren't in SQLite columns)
        content_lower = content.lower()
        for word in words:
            if len(word) >= 4 and word in content_lower:
                score += 3.0
                break

        # Text similarity
        sim = text_similarity(query_lower[:80], content_lower[:80])
        score += sim * 2.0

        # Domain match
        if domain in query_lower:
            score += 0.5

        # Fact type keyword match
        if fact_type and fact_type in query_lower:
            score += 0.3

        # Keyword overlap (simple word intersection)
        fact_words = set(content_lower.split())
        overlap = len(query_words & fact_words)
        if overlap >= 2:
            score += overlap * 0.3

        if score < 0.5:
            continue

        # Apply salience boost (facts are reference knowledge = high value)
        salience = float(row["salience"] or 2.0) * 1.5
        confidence = float(row["confidence"] or 0.7)

        hits.append({
            "text": content[:200],
            "title": f"[{fact_type.upper()}] {content[:80]}",
            "salience": round(salience, 3),
            "confidence": confidence,
            "source": "facts",
            "domain": domain,
            "temporal": "current",
            "entities": [],
            "_score": score,
        })

    # Sort by relevance score, then salience as tiebreaker
    hits.sort(key=lambda h: (h["_score"], h["salience"]), reverse=True)
    # Remove internal score before returning
    for h in hits:
        h.pop("_score", None)
    return hits[:10]


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

    import json
    try:
        facts = json.loads(FACTS_FILE.read_text(encoding="utf-8"))
        if not isinstance(facts, list):
            return []
    except (json.JSONDecodeError, OSError):
        return []

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

    hits = []
    for fact in facts:
        fact_text = fact.get("fact", "")
        entities = fact.get("entities", [])
        fact_type = fact.get("fact_type", "")
        domain = fact.get("domain", "general")

        score = 0.0

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

        # Text similarity
        sim = text_similarity(query_lower[:80], fact_text[:80].lower())
        score += sim * 2.0

        # Domain match
        if domain in query_lower:
            score += 0.5

        # Fact type keyword match
        if fact_type in query_lower:
            score += 0.3

        # Keyword overlap (simple word intersection)
        query_words = set(query_lower.split())
        fact_words = set(fact_text.lower().split())
        overlap = len(query_words & fact_words)
        if overlap >= 2:
            score += overlap * 0.3

        if score < 0.5:
            continue

        # Apply salience boost (facts are reference knowledge = high value)
        salience = fact.get("salience", 2.0) * 1.5
        confidence = fact.get("confidence", 0.7)

        hits.append({
            "text": fact_text[:200],
            "title": f"[{fact_type.upper()}] {fact_text[:80]}",
            "salience": round(salience, 3),
            "confidence": confidence,
            "source": "facts",
            "domain": domain,
            "temporal": fact.get("temporal", "current"),
            "entities": entities,
        })

    # Sort by score (implicit via salience), return top results
    hits.sort(key=lambda h: h["salience"], reverse=True)
    return hits[:10]


def search_facts(query: str, config: dict = None) -> list:
    """
    Search facts matching query.

    Primary: SQLite FTS5 against `facts` table (fast indexed lookup).
    Fallback: flat-file read of auto_facts.json if SQLite has 0 rows.

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

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