#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Brain Search Utilities: Helper functions for search operations (V43.13)

Provides salience computation, retrieval tracking, and date utilities.

Dependencies: Python stdlib + brain_utils
"""

import hashlib
import logging
import math
import time
from datetime import datetime, timezone

from pulse.core.brain_utils import load_json_dict, RETRIEVAL_METRICS

log = logging.getLogger("pulse.brain.search_utils")

# === SOURCE GROUNDING WEIGHTS ===
SOURCE_WEIGHT = {
    'CONFIRMED': 1.0,
    'OBSERVED': 0.85,
    'INFERRED': 0.6,
    'CONVERSATIONAL': 0.85,
}

# === RETRIEVAL TRACKING (LTP) ===

_retrieval_cache = {}  # {hash: count}
_cache_loaded_at = 0.0
_CACHE_TTL = 60  # seconds


def _item_key(text: str) -> str:
    """Stable key from first 80 chars — MD5 hash. Aligned with brain_search + consolidation."""
    return hashlib.md5(text[:80].lower().strip().encode("utf-8")).hexdigest()[:12]


def _get_from_sqlite(key: str) -> int | None:
    """Query retrieval count from SQLite. Returns None if unavailable."""
    try:
        from pulse.core.brain_db import get_conn
        row = get_conn().execute(
            "SELECT retrieval_count FROM retrieval_metrics WHERE item_key = ?",
            (key,)
        ).fetchone()
        return row["retrieval_count"] if row else None
    except Exception:
        return None


def _load_retrieval_cache():
    """Load retrieval counts from JSON with 60s TTL cache (fallback only)."""
    global _retrieval_cache, _cache_loaded_at
    now = time.time()
    if _retrieval_cache and (now - _cache_loaded_at) < _CACHE_TTL:
        return _retrieval_cache
    data = load_json_dict(RETRIEVAL_METRICS)
    _retrieval_cache = {k: v.get("retrieval_count", 0)
                        for k, v in data.get("items", {}).items()}
    _cache_loaded_at = now
    return _retrieval_cache


def get_retrieval_count(text: str, item_id: str = "", table_name: str = "") -> int:
    """Get retrieval count. Checks source table access_count first (QR8 fix).

    The retrieval_metrics table uses MD5 hash keys that often mismatch.
    The source table's access_count (updated by FTS item_id resolution
    in retrieval.py) is the authoritative source.
    """
    # Primary: check access_count on source table (works via item_id)
    if item_id and table_name:
        try:
            from pulse.core.brain_db import get_conn
            conn = get_conn()
            row = conn.execute(
                f"SELECT access_count FROM {table_name} WHERE id = ?",
                (item_id,)
            ).fetchone()
            if row and row[0]:
                return int(row[0])
        except Exception:
            pass
    # Fallback: retrieval_metrics (MD5 hash key)
    key = _item_key(text)
    count = _get_from_sqlite(key)
    if count is not None:
        return count
    cache = _load_retrieval_cache()
    return cache.get(key, 0)


def compute_salience(relevance: float, days_old: int, config: dict,
                     item_salience: float = 1.0,
                     retrieval_count: int = 0,
                     ground_weight: float = 1.0) -> float:
    """Salience = weighted sum of 4 factors, scaled by epistemic ground weight.

    Four factors:
      - relevance (0.45 weight): how well does query match content
      - recency (0.15 weight): exponential decay over time
      - item_salience (0.25 weight): how important is the event itself
      - retrieval (0.15 weight): how often has this been retrieved (LTP)

    ground_weight: SOURCE_WEIGHT[source_type] — scales final score by epistemic
    confidence. Default 1.0 = zero regression for callers without source_type.
    """
    decay_const = config.get("decay", {}).get("benchmark_decay_constant", 100)
    recency = math.exp(-days_old / decay_const)
    # Normalize item_salience from 1.0-10.0 range to 0.0-1.0 (7.0 scale — critical tier tops out)
    norm_salience = min(1.0, item_salience / 7.0) if item_salience > 0 else 0.1
    # Retrieval factor: log scale, caps at 10+ retrievals
    retrieval_factor = min(1.0, math.log(1 + retrieval_count) / math.log(11)) if retrieval_count > 0 else 0.0
    r_weight = config.get("search", {}).get("relevance_weight", 0.45)
    t_weight = config.get("search", {}).get("recency_weight", 0.15)
    s_weight = config.get("search", {}).get("salience_weight", 0.25)
    ltp_weight = config.get("search", {}).get("retrieval_weight", 0.15)
    base = (relevance * r_weight + recency * t_weight
            + norm_salience * s_weight + retrieval_factor * ltp_weight)
    return round(base * ground_weight, 4)


def days_since(date_str: str) -> int:
    """Compute days since a date string."""
    if not date_str:
        return 365
    try:
        ts = str(date_str).replace("Z", "+00:00")
        if "T" in ts:
            d = datetime.fromisoformat(ts)
        else:
            d = datetime.fromisoformat(ts).replace(tzinfo=timezone.utc)
        if d.tzinfo is None:
            d = d.replace(tzinfo=timezone.utc)
        return max(0, (datetime.now(timezone.utc) - d).days)
    except (ValueError, TypeError):
        return 365
