#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""Brain Search Sources (Core): anti-patterns, predictions, evidence."""

import logging
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent

from pulse.core.brain_utils import (load_json, load_json_dict, search_relevance,
                         EVIDENCE_DIR, ANTI_PATTERNS_DIR, STATE_DIR)
from pulse.brain.index.search_utils import compute_salience, days_since, get_retrieval_count
from .search_sources_ext import _search_sqlite

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

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

def search_predictions(query: str, config: dict) -> list:
    """Search active prediction alerts (Phase 8 feedback loop)."""
    results = []
    alerts_file = STATE_DIR / "prediction_alerts.json"
    if not alerts_file.exists():
        return results

    data = load_json_dict(alerts_file)
    for alert in data.get("alerts", []):
        if alert.get("status") != "active":
            continue
        
        # Match against task title + prediction text
        search_text = f"{alert.get('task_title', '')} {alert.get('prediction', '')}"
        sim = search_relevance(query, search_text)
        
        # High threshold (0.4) because predictions are specific to tasks
        # If matched, give HUGE salience boost (it's an active warning)
        if sim >= 0.4:
            days = days_since(alert.get("timestamp", ""))
            results.append({
                "source": "predictions",
                "title": f"[PREDICTION] {alert.get('task_title', '')[:40]}...",
                "text": alert.get("prediction", "")[:300],
                "relevance": round(sim, 3),
                "salience": compute_salience(sim, days, config, 10.0), # 10x boost
                "severity": "critical",
                "confidence": alert.get("confidence", 0.5),
                "task_id": alert.get("task_id", "")
            })
            
    return sorted(results, key=lambda x: -x["salience"])

def _search_anti_patterns_sqlite(query: str, config: dict) -> list:
    """Primary path: SQLite query against anti_patterns table.

    Raises _TableEmpty if the table doesn't exist or has 0 rows.
    Phase 4 will create and populate the anti_patterns 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 anti_patterns table exists and has rows
    try:
        row_count = conn.execute(
            "SELECT COUNT(*) FROM anti_patterns"
        ).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)
    _STOP = {"the","a","an","is","are","was","were","be","to","of","in","for","on","with","at","by","from","as","it","this","that","not","no","but","or","and","if","so"}
    words = [w for w in words if len(w) >= 3 and w.lower() not in _STOP]
    if not words: return []
    _joiner = " OR "
    fts_expr = _joiner.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 = 'anti_patterns'
              AND knowledge_fts MATCH ?
            LIMIT 50
        """, (fts_expr,)).fetchall()
    except Exception as e:
        log.debug("FTS5 anti_patterns 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 anti_patterns WHERE id IN ({placeholders})", id_list
    ).fetchall()

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

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

        desc = row["description"] or ""
        rule = row["rule"] or ""
        content = f"{desc} {rule}".strip() if rule else desc
        sim = search_relevance(query, content)
        if sim < 0.2:
            continue

        days = days_since(row["created"] or "")
        results.append({
            "source": "anti_patterns",
            "id": row["id"],
            "table_name": "anti_patterns",
            "title": f"[ACTIVE] {desc[:60]}",
            "text": (rule[:500] if rule else desc[:500]),
            "content": content[:500],
            "relevance": round(sim, 3),
            "salience": compute_salience(sim, days, config,
                                         retrieval_count=get_retrieval_count(content[:200])),
            "severity": "medium",
            "evidence_count": int(row["evidence_count"] or 0),
            "access_count": int(row["access_count"] or 0) if "access_count" in row.keys() else 0,
            "confidence": float(row["confidence"] or 0.5),
        })

    return sorted(results, key=lambda x: -x["salience"])

def _search_anti_patterns_flatfile(query: str, config: dict) -> list:
    results = []

    try:
        from pulse.core.brain_db import get_active_anti_patterns
        active = get_active_anti_patterns()
    except Exception:
        active = []
    if active:
        for ap in active:
            if ap.get("status") != "active":
                continue
            desc = ap.get("description", "")
            rule = ap.get("rule", "")
            keywords = " ".join(ap.get("keywords", []))
            search_text = f"{desc} {rule} {keywords}"
            sim = search_relevance(query, search_text)
            if sim >= 0.2:
                days = days_since(ap.get("created", ""))
                results.append({
                    "source": "anti_patterns",
                    "title": f"[ACTIVE] {desc[:60]}",
                    "text": rule[:500] if rule else desc[:500],
                    "relevance": round(sim, 3),
                    "salience": compute_salience(sim, days, config,
                                                 retrieval_count=get_retrieval_count(desc[:200])),
                    "severity": ap.get("severity", "medium"),
                    "evidence_count": ap.get("evidence_count", 0),
                })

    reflex_file = ANTI_PATTERNS_DIR / "reflex_patterns.json"
    if reflex_file.exists():
        reflex_data = load_json_dict(reflex_file)
        for rx in (reflex_data.get("entries", [])
                   if isinstance(reflex_data, dict) else []):
            if rx.get("status") != "active":
                continue
            desc = rx.get("description", "")
            sim = search_relevance(query, f"{desc} {rx.get('pattern_name', '')}")
            if sim >= 0.2:
                days = days_since(rx.get("timestamp", ""))
                results.append({
                    "source": "reflex_arc",
                    "title": f"[REFLEX] {desc[:60]}",
                    "text": desc[:500],
                    "relevance": round(sim, 3),
                    "salience": compute_salience(sim, days, config,
                                                 retrieval_count=get_retrieval_count(desc[:200])),
                    "severity": rx.get("severity", "medium"),
                })

    return sorted(results, key=lambda x: -x["salience"])

def search_anti_patterns(query: str, config: dict) -> list:
    """Search anti-patterns matching query.

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

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

    # Fallback: flat-file read (graceful degradation)
    return _search_anti_patterns_flatfile(query, config)

_SKIP_EVIDENCE_FILES = {"wants.json", "dont_wants.json", "strategic_intents.json"}
_MAX_EVIDENCE_FILES = 50
_MAX_ITEMS_PER_FILE = 2000

def search_evidence(query: str, config: dict) -> list:
    """Search raw evidence files (wants, dont_wants, sessions, learnings, etc.).

    SQLite FTS5 fast-path with flat-file rglob fallback.
    The evidence table content column stores the full text, so FTS5 MATCH
    covers the same key fallback chain that the JSON scan builds manually.
    """
    # --- SQLite fast-path (activates once evidence table is populated) ---
    sqlite_results = _search_sqlite("evidence", "evidence", "observation", query, config)
    if sqlite_results:
        return sqlite_results

    # --- Flat-file fallback (primary path until migration) ---
    results = []
    if not EVIDENCE_DIR.exists():
        return results
    file_count = 0
    for ef in sorted(EVIDENCE_DIR.rglob("*.json")):
        if ef.name in _SKIP_EVIDENCE_FILES:
            continue
        if file_count >= _MAX_EVIDENCE_FILES:
            break
        file_count += 1
        items = load_json(ef)
        if not isinstance(items, list):
            continue
        for item in items[:_MAX_ITEMS_PER_FILE]:
            # Extended fallback chain — covers all known key formats
            text = (item.get("evidence", "")
                    or item.get("want", "")
                    or item.get("dont_want", "")
                    or item.get("description", "")
                    or item.get("text", "")
                    or item.get("lesson", "")
                    or item.get("discovery", "")
                    or item.get("decision", "")
                    or item.get("issue", "")
                    or item.get("user_insight", ""))
            # Domain learnings: combine prompt + response summaries
            if not text and item.get("prompt_summary"):
                ps = item.get("prompt_summary", "")
                rs = item.get("response_summary", "")
                text = f"{ps} {rs}".strip() if rs else ps
            # Agent sessions: combine learnings + failures lists
            if not text:
                parts = []
                if isinstance(item.get("learnings"), list):
                    parts.extend(item["learnings"])
                elif isinstance(item.get("learnings"), str) and item["learnings"]:
                    parts.append(item["learnings"])
                if isinstance(item.get("failures"), list):
                    parts.extend(item["failures"])
                elif isinstance(item.get("failures"), str) and item["failures"]:
                    parts.append(item["failures"])
                if parts:
                    text = " ".join(parts)
            # Always prepend domain for topic matching
            domain = item.get("domain", "")
            if domain and text:
                text = f"{domain} {text}"
            # Context envelope matching (Phase 4): boost if query matches branch/files
            ctx = item.get("context", {})
            ctx_text = f"{ctx.get('git_branch', '')} {' '.join(ctx.get('recent_files', []))}"
            if ctx_text.strip():
                text = f"{text} {ctx_text}"
            sim = search_relevance(query, text)
            if sim >= 0.2:
                days = days_since(item.get("date") or item.get("timestamp"))
                item_sal = item.get("salience", 1.0)
                results.append({
                    "source": "evidence", "file": ef.name,
                    "text": text[:500], "relevance": round(sim, 3),
                    "salience": compute_salience(sim, days, config, item_sal,
                                                 retrieval_count=get_retrieval_count(text[:200])),
                    "confidence": item.get("confidence", 0.5),
                    "type": item.get("type", "observation"),
                    "context": ctx if ctx else None,
                })
    return sorted(results, key=lambda x: -x["salience"])
