#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Search Source: Decisions (N0 — Conversational Memory Recall)

Primary path: SQLite FTS5 query against the `decisions` table.
Fallback: flat-file read of Hippocampus/Decisions/auto_decisions.jsonl.

Scoring: choice_sim*3.0 + context_sim*1.5 + keyword_overlap*0.3
         Status penalty: superseded confidence *= 0.4

Dependencies: Python stdlib + brain_utils + brain_analysis + brain_db
"""

import json
import logging

from pulse.core.brain_utils import DECISIONS_JSONL
from pulse.core.brain_analysis import text_similarity
from pulse.core.brain_io import _acquire

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


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


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

    Raises _TableEmpty if decisions 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 decisions").fetchone()[0]
    if row_count == 0:
        raise _TableEmpty()

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

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

    # Use OR to get a broad candidate set, then score in Python 
    fts_expr = " OR ".join(words[:10])        
    try:
        candidates = conn.execute("""
            SELECT f.item_id, f.content
            FROM knowledge_fts f
            WHERE f.table_name = 'decisions'
              AND knowledge_fts MATCH ?
            LIMIT 50
        """, (fts_expr,)).fetchall()
    except Exception as e:
        log.debug("FTS5 decisions query failed: %s", e)
        return []

    if not candidates:
        return []

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

    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 ""
        validity = row["validity"] or "valid"

        # Map validity to decision status semantics
        # The decisions table stores choice text in content;
        # reasoning/context/tags are in metadata JSON
        meta = {}
        try:
            meta = json.loads(row["metadata"] or "{}")
        except (json.JSONDecodeError, TypeError):
            pass

        choice = meta.get("choice", content)
        reasoning = meta.get("reasoning", "")
        context_text = meta.get("context", "")
        tags = " ".join(meta.get("tags", []))
        status = meta.get("status", "active") if meta else "active"
        alts = meta.get("alternatives_rejected", [])
        scope = meta.get("scope", row["domain"] or "general")

        if status not in ("active", "superseded"):
            continue

        search_text = f"{choice} {reasoning} {context_text} {tags}".lower()

        # Similarity scoring
        score = 0.0

        # Choice match (strongest signal)
        choice_sim = text_similarity(query_lower[:80], choice.lower())
        score += choice_sim * 3.0

        # Reasoning/context match
        ctx_sim = text_similarity(query_lower[:80], search_text)
        score += ctx_sim * 1.5

        # Keyword overlap
        text_words = set(search_text.split())
        overlap = len(query_words & text_words)
        if overlap >= 2:
            score += overlap * 0.3

        if score < 0.5:
            continue

        # Status-based modifiers
        confidence = float(row["confidence"] or 0.65)
        if status == "superseded":
            confidence *= 0.4

        # Build display text
        status_tag = "[SUPERSEDED] " if status == "superseded" else ""
        alt_text = f" (over {', '.join(alts)})" if alts else ""
        reason_text = f" \u2014 {reasoning[:60]}" if reasoning else ""
        display = f"{status_tag}{choice}{alt_text}{reason_text}"

        hits.append({
            "text": display[:200],
            "title": f"[DECISION] {choice[:80]}",
            "id": row["id"],
            "salience": round(min(score, 3.0), 3),
            "confidence": confidence,
            "source": "decisions",
            "scope": scope,
            "status": status,
            "timestamp": row["timestamp"] or "",
        })

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


def _search_decisions_flatfile(query: str) -> list:
    """Fallback: JSONL streaming read (original implementation)."""
    if not DECISIONS_JSONL.exists():
        return []

    try:
        with _acquire(DECISIONS_JSONL):
            lines = DECISIONS_JSONL.read_text(encoding="utf-8").splitlines()
    except OSError:
        return []

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

    hits = []
    for line in lines:
        line = line.strip()
        if not line:
            continue
        try:
            dec = json.loads(line)
        except json.JSONDecodeError:
            continue

        if dec.get("status", "active") not in ("active", "superseded"):
            continue

        choice = dec.get("choice", "")
        reasoning = dec.get("reasoning", "")
        context = dec.get("context", "")
        tags = " ".join(dec.get("tags", []))

        search_text = f"{choice} {reasoning} {context} {tags}".lower()

        # Similarity scoring
        score = 0.0

        # Choice match (strongest signal)
        choice_sim = text_similarity(query_lower[:80], choice.lower())
        score += choice_sim * 3.0

        # Reasoning/context match (full text, no truncation)
        ctx_sim = text_similarity(query_lower[:80], search_text)
        score += ctx_sim * 1.5

        # Keyword overlap
        query_words = set(query_lower.split())
        text_words = set(search_text.split())
        overlap = len(query_words & text_words)
        if overlap >= 2:
            score += overlap * 0.3

        if score < 0.5:
            continue

        # Status-based modifiers
        confidence = dec.get("confidence", 0.65)
        status = dec.get("status", "active")
        if status == "superseded":
            confidence *= 0.4

        # Build display text with status tag
        status_tag = "[SUPERSEDED] " if status == "superseded" else ""
        alts = dec.get("alternatives_rejected", [])
        alt_text = f" (over {', '.join(alts)})" if alts else ""
        reason_text = f" \u2014 {reasoning[:60]}" if reasoning else ""
        display = f"{status_tag}{choice}{alt_text}{reason_text}"

        hits.append({
            "text": display[:200],
            "title": f"[DECISION] {choice[:80]}",
            "id": dec.get("id", ""),
            "salience": round(min(score, 3.0), 3),
            "confidence": confidence,
            "source": "decisions",
            "scope": dec.get("scope", "general"),
            "status": status,
            "timestamp": dec.get("timestamp", ""),
        })

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


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

    Primary: SQLite FTS5 against `decisions` table (fast indexed lookup).
    Fallback: JSONL streaming read if SQLite has 0 rows or errors.

    Matches against choice + reasoning + context + tags.
    Boosts active decisions, penalizes superseded.
    Returns list of hit dicts compatible with brain_search format.
    """
    try:
        return _search_decisions_sqlite(query)
    except _TableEmpty:
        log.debug("SQLite decisions table empty, falling back to JSONL")
    except Exception as e:
        log.debug("SQLite decisions search failed, falling back to JSONL: %s", e)

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