"""Fast embedding search using SQLite-stored BLOBs + numpy.

Replaces the 189MB JSON rule_embeddings.json with direct SQLite reads.
Matrix is loaded lazily and cached in memory (~56MB for 38K rows).
"""
from __future__ import annotations
import logging, time, threading

import numpy as np

log = logging.getLogger("pulse.sqlite_embedding_search")

KNOWLEDGE_TABLES = (
    "lessons", "failures", "patterns", "facts",
    "evidence", "intents", "decisions",
)
EMBED_DIM = 384
_CACHE_TTL = 300  # 5 min cache

_cache_lock = threading.Lock()
_cached_ids: list[str] = []
_cached_tables: list[str] = []
_cached_matrix: np.ndarray | None = None
_cache_time: float = 0.0


def _load_embedding_matrix() -> tuple[list[str], list[str], np.ndarray]:
    """Load all embeddings from SQLite into a normalized numpy matrix."""
    global _cached_ids, _cached_tables, _cached_matrix, _cache_time
    now = time.time()
    with _cache_lock:
        if _cached_matrix is not None and (now - _cache_time) < _CACHE_TTL:
            return _cached_ids, _cached_tables, _cached_matrix

    from pulse.core.brain_db import get_conn
    conn = get_conn()
    ids, tables, vecs = [], [], []
    for table in KNOWLEDGE_TABLES:
        try:
            rows = conn.execute(
                f"SELECT id, embedding, content FROM {table} "
                f"WHERE embedding IS NOT NULL AND LENGTH(embedding) = ?",
                (EMBED_DIM * 4,)
            ).fetchall()
            for r in rows:
                vec = np.frombuffer(r[1], dtype=np.float32).copy()
                if np.any(vec != 0):
                    ids.append(r[0])
                    tables.append(table)
                    vecs.append(vec)
        except Exception as e:
            log.debug("Failed loading embeddings from %s: %s", table, e)

    if not vecs:
        with _cache_lock:
            _cached_ids, _cached_tables = [], []
            _cached_matrix = np.array([])
            _cache_time = now
        return [], [], np.array([])

    matrix = np.stack(vecs)
    norms = np.linalg.norm(matrix, axis=1, keepdims=True)
    norms[norms == 0] = 1.0
    matrix = matrix / norms

    with _cache_lock:
        _cached_ids = ids
        _cached_tables = tables
        _cached_matrix = matrix
        _cache_time = now

    log.debug("Embedding matrix loaded: %d vectors, shape %s", len(ids), matrix.shape)
    return ids, tables, matrix


def search_embeddings(query: str, top_k: int = 10,
                      min_similarity: float = 0.3) -> list[dict]:
    """Search for similar items using SQLite-stored embeddings.

    Returns list of dicts with keys: id, table, text, confidence, type.
    """
    ids, tables, matrix = _load_embedding_matrix()
    if matrix is None or len(matrix) == 0:
        return []

    from pulse.brain.index.rule_embeddings.backends import embed_texts, _ST_MODEL, _ONNX_DIR
    # Skip only if BOTH ONNX and sentence-transformers are unavailable.
    # ONNX loads in ~200ms (vs 14s for PyTorch), so it's safe on cold starts.
    onnx_available = _ONNX_DIR.exists() and (_ONNX_DIR / "model.onnx").exists()
    if _ST_MODEL is None and not onnx_available:
        return []
    query_vec = np.array(embed_texts([query])[0], dtype=np.float32)
    query_norm = np.linalg.norm(query_vec)
    if query_norm == 0:
        return []
    query_vec = query_vec / query_norm

    # Cosine similarity via dot product (both sides normalized)
    similarities = matrix @ query_vec  # shape: (n,)

    # Get top-k indices
    if len(similarities) <= top_k:
        top_indices = np.argsort(similarities)[::-1]
    else:
        top_indices = np.argpartition(similarities, -top_k)[-top_k:]
        top_indices = top_indices[np.argsort(similarities[top_indices])[::-1]]

    # Fetch text content for matching items
    from pulse.core.brain_db import get_conn
    conn = get_conn()

    results = []
    for idx in top_indices:
        sim = float(similarities[idx])
        if sim < min_similarity:
            break
        item_id = ids[idx]
        table_name = tables[idx]
        # Retrieve title + content from the source table
        title, content = "", ""
        try:
            row = conn.execute(
                f"SELECT title, content FROM {table_name} WHERE id = ?",
                (item_id,)
            ).fetchone()
            if row:
                title = row[0] or ""
                content = row[1] or ""
        except Exception:
            pass
        results.append({
            "id": item_id,
            "table": table_name,
            "source": table_name,
            "title": title,
            "content": content[:500],
            "text": title or content[:200],
            "confidence": round(sim, 3),
            "similarity": round(sim, 4),
            "type": "embedding_match",
        })
    return results


def invalidate_cache():
    """Force cache refresh on next search."""
    global _cache_time
    _cache_time = 0.0
