#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Brain Search Sources (Extended 2): Schemas, graph, procedures, meta-schemas.

Extracted from search_sources_ext.py to keep both files under 300 lines.
Contains: _search_schemas_sqlite, search_schemas, search_graph, search_procedures,
          search_meta_schemas
"""

import hashlib
import json
from pathlib import Path

from pulse.core.brain_utils import (load_json, load_json_dict, search_relevance,
                         PATTERNS_DIR, HIPPOCAMPUS_DIR)
from pulse.brain.index.search_utils import compute_salience, get_retrieval_count


def _sanitize_fts_query(query: str) -> str:
    """Sanitize a free-text query for FTS5 MATCH safety."""
    import re as _re
    _STOP_WORDS = {
        'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
        'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
        'should', 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for',
        'on', 'with', 'at', 'by', 'from', 'as', 'into', 'about', 'it', 'its',
        'this', 'that', 'these', 'those', 'i', 'we', 'you', 'he', 'she', 'they',
        'me', 'us', 'him', 'her', 'them', 'my', 'our', 'your', 'his', 'their',
        'what', 'which', 'who', 'when', 'where', 'how', 'not', 'no', 'nor',
        'but', 'or', 'and', 'if', 'then', 'so', 'up', 'out', 'just', 'also',
    }
    words = _re.findall(r'[a-zA-Z0-9_]+', query)
    words = [w for w in words if len(w) >= 3 and w.lower() not in _STOP_WORDS]
    if not words:
        return ''
    # Unquoted so porter stemmer activates (QR8 P12 fix)
    return ' OR '.join(words[:10])


def _search_schemas_sqlite(query: str, config: dict) -> list:
    """SQLite primary path for schemas (dedicated table, not knowledge_fts)."""
    fts_query = _sanitize_fts_query(query)
    if not fts_query:
        return []
    try:
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        max_results = config.get("max_per_source", 50) if config else 50
        rows = conn.execute("""
            SELECT id, name, description, domain, confidence
            FROM schemas
            WHERE id IN (
                SELECT item_id FROM knowledge_fts
                WHERE knowledge_fts MATCH ? AND table_name = 'schemas'
            )
            ORDER BY confidence DESC
            LIMIT ?
        """, (fts_query, max_results)).fetchall()
        if not rows:
            return []
        results = []
        for row in rows:
            name = row["name"] or ""
            desc = row["description"] or ""
            search_text = f"{name} {desc}"
            sim = search_relevance(query, search_text)
            if sim < 0.3:
                continue
            conf = float(row["confidence"]) if row["confidence"] else 0.5
            sal = compute_salience(sim, 0, config, conf * 10,
                                   retrieval_count=get_retrieval_count(search_text[:200]))
            sid = row["id"] or hashlib.md5(name.encode()).hexdigest()[:8]
            results.append({
                "id": f"SCH-{sid}",
                "source": "schemas", "title": name,
                "text": desc[:200], "relevance": round(sim, 3),
                "salience": round(sal * 2.0, 4),
                "confidence": conf,
                "schema_id": sid,
                "evidence_count": 0,
                "type": "proven_schema",
            })
        return sorted(results, key=lambda x: -x["salience"])
    except Exception:
        return []


def search_schemas(query: str, config: dict) -> list:
    """Search proven schemas (2x weight — these are battle-tested patterns).

    SQLite primary path with flat-file fallback.
    """
    # --- SQLite primary path ---
    results = _search_schemas_sqlite(query, config)
    if results:
        return results

    # --- Flat-file fallback ---
    results = []
    schemas_file = PATTERNS_DIR / "schemas.json"
    if not schemas_file.exists():
        return results
    data = load_json_dict(schemas_file)
    for schema in data.get("schemas", []):
        name = schema.get("name", "")
        desc = schema.get("description", "")
        search_text = f"{name} {desc}"
        sim = search_relevance(query, search_text)
        if sim >= 0.3:
            conf = schema.get("confidence", 0.5)
            # Schemas get 2x salience boost (proven patterns)
            sal = compute_salience(sim, 0, config, conf * 10,
                                  retrieval_count=get_retrieval_count(search_text[:200]))
            sid = schema.get("schema_id", "") or hashlib.md5(name.encode()).hexdigest()[:8]
            results.append({
                "id": f"SCH-{sid}",
                "source": "schemas", "title": name,
                "text": desc[:200], "relevance": round(sim, 3),
                "salience": round(sal * 2.0, 4),
                "confidence": conf,
                "schema_id": sid,
                "evidence_count": schema.get("evidence_count", 0),
                "type": "proven_schema",
            })
    return sorted(results, key=lambda x: -x["salience"])


def search_graph(query: str, config: dict) -> list:
    """Search knowledge graph for causal relationships (Phase 7).

    FIX: query_related uses LIKE which fails on multi-word queries.
    Try full query first, then individual significant words as fallback.
    """
    results = []
    seen_texts = set()
    try:
        from pulse.graph.knowledge_graph import query_related
        hits = query_related(query, max_results=5)
        # Fallback: if full query returns nothing, try individual words
        if not hits:
            words = [w for w in query.lower().split() if len(w) > 3]
            for word in words[:4]:
                word_hits = query_related(word, max_results=3)
                hits.extend(word_hits)
        for h in hits:
            text = h.get("text", "")[:200]
            if text in seen_texts:
                continue
            seen_texts.add(text)
            results.append({
                "source": "graph",
                "text": text,
                "type": h.get("type", "CONCEPT"),
                "relationship": h.get("relationship", ""),
                "relevance": h.get("confidence", 0.5),
                "salience": compute_salience(
                    h.get("confidence", 0.5), 0, config,
                    retrieval_count=get_retrieval_count(text)
                ),
                "confidence": h.get("confidence", 0.5),
            })
    except ImportError:
        pass
    except Exception:
        pass
    return sorted(results, key=lambda x: -x.get("salience", 0))


def search_procedures(query: str, config: dict) -> list:
    """Search procedural log for relevant proven workflows."""
    results = []
    proc_file = HIPPOCAMPUS_DIR / "Evidence" / "Metrics" / "procedural_log.json"
    if not proc_file.exists():
        return results
    data = load_json_dict(proc_file)
    for proc in data.get("procedures", []):
        name = proc.get("name", "")
        desc = proc.get("description", "")
        domain = proc.get("domain", "")
        search_text = f"{name} {desc} {domain}"
        sim = search_relevance(query, search_text)
        if sim >= 0.3 and proc.get("confidence", 0) >= 0.3:
            results.append({
                "id": f"PROC-{hashlib.md5(name.encode()).hexdigest()[:8]}",
                "source": "procedures", "title": name,
                "text": f"{desc} (success: {proc.get('success_rate', 0):.0%}, "
                        f"{proc.get('executions', 0)} runs)",
                "relevance": round(sim, 3),
                "salience": compute_salience(sim, 0, config, proc.get("confidence", 0.5) * 5,
                                             retrieval_count=get_retrieval_count(search_text[:200])),
                "confidence": proc.get("confidence", 0),
                "type": "proven_procedure",
            })
    return sorted(results, key=lambda x: -x["salience"])


def search_meta_schemas(query: str, config: dict) -> list:
    """Search Level 2 meta-schema clusters (Phase 4, Architecture 7).

    Meta-schemas compress 418 schemas into ~8 clusters by type.
    Returns matching clusters with constituent schema counts.
    """
    from pulse.core.paths import get_state_dir
    meta_file = get_state_dir() / "memory" / "meta_schemas.json"
    if not meta_file.exists():
        return []
    try:
        data = json.loads(meta_file.read_text(encoding="utf-8"))
    except Exception:
        return []
    if not isinstance(data, list):
        return []

    q_lower = query.lower()
    q_words = set(q_lower.split())
    results = []
    for ms in data:
        desc = (ms.get("description", "") + " " + ms.get("type", "")).lower()
        overlap = sum(1 for w in q_words if w in desc)
        if overlap == 0:
            continue
        sim = min(1.0, overlap / max(len(q_words), 1))
        schema_count = ms.get("schema_count", len(ms.get("schema_ids", [])))
        results.append({
            "text": f"[META-SCHEMA:{ms.get('type', '?')}] {ms.get('description', '')[:200]} "
                    f"({schema_count} schemas, confidence={ms.get('avg_confidence', 0):.2f})",
            "salience": round(sim * ms.get("avg_confidence", 0.5) * 5, 3),
            "confidence": ms.get("avg_confidence", 0.5),
            "type": "meta_schema",
        })
    return sorted(results, key=lambda x: -x["salience"])
