#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Hot Cache — Instant knowledge access for brain search (Phase 3 Reflex Cache).

High-salience knowledge items are written here by the bridge immediately on flush.
brain_search checks this FIRST (priority 0 source, <10ms read).

The hot cache is small (max 200 entries), append-only JSONL, and periodically
trimmed. It solves the latency problem: a mistake detected by the bridge is
available to the next brain_search call within milliseconds, not minutes.

Public API:
    write_hot(entry: dict) -> bool   — write if salience >= threshold
    search_hot(query, config) -> list — search for brain_search integration
    trim_hot() -> int                — keep only recent entries
"""

import json
import time
from datetime import datetime, timezone

from pulse.core.brain_utils import STATE_DIR, search_relevance
from pulse.core.brain_io import _acquire
from pulse.brain.index.search_utils import compute_salience, days_since, get_retrieval_count

HOT_CACHE_FILE = STATE_DIR / "hot_cache.jsonl"
HOT_SALIENCE_THRESHOLD = 4.0  # only cache actionable+ items
HOT_MAX_ENTRIES = 200
HOT_MAX_AGE_S = 3600  # 1 hour — entries older than this get trimmed
_STAGING_PERIOD = 30  # Item 30: CLS staging — seconds before item is LTP-eligible


def write_hot(entry: dict) -> bool:
    """Write a high-salience item to the hot cache. Returns True if written."""
    sal = entry.get("salience", 0)
    if sal < HOT_SALIENCE_THRESHOLD:
        return False

    content = entry.get("content", "") or entry.get("description", "")
    if not content or len(content) < 15:
        return False

    hot_entry = {
        "content": content[:300],
        "title": (entry.get("title", "") or content[:80]),
        "type": entry.get("type", "unknown"),
        "salience": sal,
        "confidence": entry.get("confidence", 0.5),
        "source": entry.get("agent", "") or entry.get("source", ""),
        "domain": entry.get("domain", "general"),
        "timestamp": entry.get("timestamp", datetime.now(timezone.utc).isoformat()),
        "id": entry.get("id", ""),
        "_hot_written": time.time(),
    }

    try:
        HOT_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
        line = json.dumps(hot_entry, ensure_ascii=False, separators=(",", ":"))
        with _acquire(HOT_CACHE_FILE):
            with open(HOT_CACHE_FILE, "a", encoding="utf-8") as f:
                f.write(line + "\n")
        return True
    except Exception:
        return False


def search_hot(query: str, config: dict) -> list:
    """Search the hot cache. Called by brain_search as priority 0 source.

    Auto-trims expired entries when file exceeds 2000 lines to prevent
    bloat (was 54K lines / 22MB with 99% expired, costing 500ms/search).
    """
    if not HOT_CACHE_FILE.exists():
        return []
    results = []
    now = time.time()

    # Auto-trim: if file has grown beyond 2000 lines, rewrite with only valid entries
    try:
        line_count = sum(1 for _ in open(HOT_CACHE_FILE, encoding="utf-8", errors="replace"))
        if line_count > 2000:
            valid = []
            with open(HOT_CACHE_FILE, "r", encoding="utf-8", errors="replace") as f:
                for line in f:
                    try:
                        e = json.loads(line.strip())
                        if now - e.get("_hot_written", 0) <= HOT_MAX_AGE_S:
                            valid.append(line.strip())
                    except Exception:
                        pass
            with open(HOT_CACHE_FILE, "w", encoding="utf-8") as f:
                f.write("\n".join(valid) + "\n" if valid else "")
    except Exception:
        pass

    try:
        with open(HOT_CACHE_FILE, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    entry = json.loads(line)
                except json.JSONDecodeError:
                    continue

                # Skip expired entries
                written_at = entry.get("_hot_written", 0)
                if now - written_at > HOT_MAX_AGE_S:
                    continue

                content = entry.get("content", "")
                title = entry.get("title", "")
                search_text = f"{title} {content}"
                sim = search_relevance(query, search_text)
                if sim < 0.2:
                    continue

                days = days_since(entry.get("timestamp", ""))
                item_sal = entry.get("salience", 1.0)
                hit = {
                    "source": "hot_cache",
                    "title": title[:80],
                    "text": content[:200],
                    "relevance": round(sim, 3),
                    "salience": compute_salience(sim, days, config, item_sal,
                                                 retrieval_count=get_retrieval_count(content[:200])),
                    "confidence": entry.get("confidence", 0.5),
                    "type": entry.get("type", "reflex"),
                    "domain": entry.get("domain", "general"),
                    "id": entry.get("id", ""),
                }
                # Item 30: CLS staging — mark items that have been in cache > 30s as LTP-eligible
                try:
                    hit["_staged"] = (now - entry.get("_hot_written", now)) >= _STAGING_PERIOD
                except Exception:
                    hit["_staged"] = False
                # Item 64: Hot cache = trusted; skip epistemic qualifiers
                hit["_skip_epistemic"] = True
                results.append(hit)
    except Exception:
        pass  # Never break search
    return sorted(results, key=lambda x: -x["salience"])


def trim_hot() -> int:
    """Remove expired entries. Returns count removed. Called by GC daemon."""
    if not HOT_CACHE_FILE.exists():
        return 0
    now = time.time()
    kept, removed = [], 0
    try:
        with _acquire(HOT_CACHE_FILE):
            with open(HOT_CACHE_FILE, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        entry = json.loads(line)
                        if now - entry.get("_hot_written", 0) <= HOT_MAX_AGE_S:
                            kept.append(line)
                        else:
                            removed += 1
                    except json.JSONDecodeError:
                        removed += 1

            # Keep only most recent HOT_MAX_ENTRIES
            if len(kept) > HOT_MAX_ENTRIES:
                removed += len(kept) - HOT_MAX_ENTRIES
                kept = kept[-HOT_MAX_ENTRIES:]

            HOT_CACHE_FILE.write_text(
                "\n".join(kept) + ("\n" if kept else ""), encoding="utf-8"
            )
    except Exception:
        pass
    return removed
