#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Extended search scoring — P34/P35 neuroscience items 143, 145, 146.

Kept separate from search_scoring_ext.py (already 415 lines) to respect
the 300-line modular ceiling.

  - Item 143: Associative strength weighting (graph edge bonus)
  - Item 145: Knowledge freshness indicator
  - Item 146: Selective attention filter (task-relevant penalty)
"""
import logging
import re
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, List, Any

log = logging.getLogger("pulse.brain_search.search_scoring_p34")

# ---------------------------------------------------------------------------
# Item 143: Associative strength weighting (Search)
# ---------------------------------------------------------------------------
_ASSOC_BONUS = 0.12  # confidence bonus per strong-edge neighbour hit
_ASSOC_EDGE_THRESHOLD = 0.7  # only edges with confidence > this count


def apply_associative_strength(results: Dict[str, List[Any]]) -> None:
    """Boost items connected to the top result by strong graph edges (conf > 0.7).

    Associative strength = proxy for topical relevance via the knowledge graph.
    Top result is taken from the highest-confidence item that has a node/id field.
    Fails open if graph is unavailable.
    """
    try:
        all_items: List[Any] = [h for hits in results.values()
                                if isinstance(hits, list) for h in hits]
        if not all_items:
            return

        # Find the top result (highest confidence with a node_id)
        top_item = None
        for h in sorted(all_items, key=lambda x: x.get("confidence", 0), reverse=True):
            if h.get("node_id") or h.get("id"):
                top_item = h
                break
        if not top_item:
            return

        top_id = top_item.get("node_id") or top_item.get("id", "")
        if not top_id:
            return

        # Query graph edges from the top node with high confidence
        neighbour_ids: set = set()
        try:
            from pulse.core.brain_db import get_conn
            conn = get_conn()
            rows = conn.execute(
                "SELECT to_node FROM graph_edges "
                "WHERE from_node = ? AND confidence > ?",
                (str(top_id), _ASSOC_EDGE_THRESHOLD)
            ).fetchall()
            for row in rows:
                try:
                    neighbour_ids.add(str(row[0]))
                except Exception:
                    pass
            rows_rev = conn.execute(
                "SELECT from_node FROM graph_edges "
                "WHERE to_node = ? AND confidence > ?",
                (str(top_id), _ASSOC_EDGE_THRESHOLD)
            ).fetchall()
            for row in rows_rev:
                try:
                    neighbour_ids.add(str(row[0]))
                except Exception:
                    pass
        except Exception:
            return  # DB unavailable — fail open

        if not neighbour_ids:
            return

        # Apply bonus to items whose node_id matches a strong-edge neighbour
        for h in all_items:
            nid = str(h.get("node_id") or h.get("id", ""))
            if nid and nid in neighbour_ids:
                h["confidence"] = min(1.0, h.get("confidence", 0.5) + _ASSOC_BONUS)
                h["_assoc_strength_bonus"] = True
    except Exception as exc:
        log.debug("apply_associative_strength failed: %s", exc)


# ---------------------------------------------------------------------------
# Item 145: Knowledge freshness indicator (Search)
# ---------------------------------------------------------------------------
_FRESHNESS_BRACKETS = [
    (timedelta(days=1),   "fresh"),
    (timedelta(days=7),   "recent"),
    (timedelta(days=30),  "aging"),
]


def _compute_freshness(timestamp_str: Optional[str]) -> str:
    """Return freshness label: fresh / recent / aging / stale."""
    if not timestamp_str:
        return "stale"
    try:
        ts = timestamp_str.replace("Z", "+00:00")
        dt = datetime.fromisoformat(ts)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        age = datetime.now(timezone.utc) - dt
        for threshold, label in _FRESHNESS_BRACKETS:
            if age <= threshold:
                return label
        return "stale"
    except Exception:
        return "stale"


def annotate_freshness(results: Dict[str, List[Any]]) -> None:
    """Add _freshness metadata to every result item. Fails open."""
    try:
        for hits in results.values():
            if not isinstance(hits, list):
                continue
            for h in hits:
                ts = (h.get("timestamp") or h.get("updated_at") or h.get("created_at"))
                h["_freshness"] = _compute_freshness(ts)
    except Exception as exc:
        log.debug("annotate_freshness failed: %s", exc)


# ---------------------------------------------------------------------------
# Item 146: Selective attention filter — task-relevant only (Attention)
# ---------------------------------------------------------------------------
_TASK_RELEVANCE_PENALTY = 0.30    # fraction by which to reduce confidence
_TASK_RELEVANCE_THRESHOLD = 0.20  # below this similarity = penalty


def _simple_word_similarity(text_a: str, text_b: str) -> float:
    """Word-overlap Jaccard similarity — no external deps."""
    try:
        wa = set(re.findall(r"\b[a-z]{3,}\b", text_a.lower()))
        wb = set(re.findall(r"\b[a-z]{3,}\b", text_b.lower()))
        if not wa or not wb:
            return 0.0
        return len(wa & wb) / len(wa | wb)
    except Exception:
        return 0.5  # fail open


def apply_task_relevance_filter(results: Dict[str, List[Any]],
                                task_description: Optional[str]) -> None:
    """Penalise items with <0.2 similarity to the current task description.

    If task_description is None or empty, no penalty is applied.
    Fails open on any error.
    """
    try:
        if not task_description or not task_description.strip():
            return
        task_desc = task_description.strip()

        # Try semantic similarity first, fall back to word overlap
        try:
            from pulse.core.brain_analysis import text_similarity as _sim_fn
        except Exception:
            _sim_fn = _simple_word_similarity  # type: ignore[assignment]

        for hits in results.values():
            if not isinstance(hits, list):
                continue
            for h in hits:
                text = (h.get("text") or h.get("title") or h.get("content") or "")
                if not text:
                    continue
                try:
                    sim = _sim_fn(text[:200], task_desc[:200])
                    h["_task_relevance"] = round(float(sim), 3)
                    if sim < _TASK_RELEVANCE_THRESHOLD:
                        h["confidence"] = max(
                            0.05,
                            h.get("confidence", 0.5) * (1.0 - _TASK_RELEVANCE_PENALTY),
                        )
                        h["_task_relevance_penalty"] = True
                except Exception:
                    pass  # per-item fail open
    except Exception as exc:
        log.debug("apply_task_relevance_filter failed: %s", exc)
