# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
D18: Cognitive HUD — Real-Time Subconscious Stream.

Watches capture logs, runs fast brain searches on new chat lines, and
streams proactive insights to an auto-refreshing terminal dashboard.

Source: DMN session_20260227T034238 | Usage: pulse hud
"""

import hashlib
import json
import re
import threading
import time
from collections import deque
from pathlib import Path

try:
    from pulse.core.brain_utils import STATE_DIR
except Exception:
    STATE_DIR = Path("state")

LOG_DIR = STATE_DIR
HUD_STATE_DIR = STATE_DIR / "hud"
POLL_INTERVAL = 2.0       # seconds between tail checks
DEEP_SEARCH_INTERVAL = 30  # full brain search every 30s
MAX_CHAT_LINES = 15        # visible chat lines in HUD
MAX_INSIGHTS = 12          # visible insight lines

# --- Log Tailing ---

_LINE_RE = re.compile(
    r"^\[([^\]]+)\]\s+(\w+):\s+(.+?)\s*\|\s*SIG:\s*\w+$"
)


def _tail_capture_logs(positions: dict) -> list[dict]:
    """Tail all capture logs, return new lines. Seek-based like bridge."""
    new_lines = []
    for logfile in sorted(LOG_DIR.glob("pulse_captured_*.log")):
        fname = logfile.name
        pos = positions.get(fname, 0)
        try:
            size = logfile.stat().st_size
            if size < pos:
                pos = 0  # log rotated
            if size <= pos:
                continue
            with open(logfile, "r", encoding="utf-8", errors="replace") as f:
                f.seek(pos)
                chunk = f.read(size - pos)
                positions[fname] = size
            for line in chunk.splitlines():
                m = _LINE_RE.match(line.strip())
                if m:
                    new_lines.append({
                        "ts": m.group(1),
                        "agent": m.group(2),
                        "text": m.group(3),
                    })
        except OSError:
            continue
    return new_lines


# --- Dedup Cache ---

class _DedupCache:
    """Fingerprint-based dedup with TTL to avoid repeating insights."""
    __slots__ = ("_seen", "_ttl")

    def __init__(self, ttl: float = 300.0):
        self._seen: dict[str, float] = {}
        self._ttl = ttl

    def is_new(self, text: str) -> bool:
        fp = hashlib.md5(text.encode()[:200]).hexdigest()[:12]
        now = time.time()
        # Prune expired
        if len(self._seen) > 200:
            self._seen = {k: v for k, v in self._seen.items() if now - v < self._ttl}
        if fp in self._seen and now - self._seen[fp] < self._ttl:
            return False
        self._seen[fp] = now
        return True


# --- Search Layer ---

def _fast_search(query: str) -> list[dict]:
    """Hot cache + inverted index search. Target: <50ms."""
    results = []
    try:
        from pulse.brain.memory.hot_cache import search_hot
        hits = search_hot(query, {})
        for h in hits[:3]:
            results.append({"text": h.get("content", h.get("text", ""))[:120],
                            "source": "hot", "score": h.get("score", 0)})
    except Exception:
        pass
    try:
        from pulse.brain.index.search_index import query_index
        hits = query_index(query, {}, max_results=5)
        for h in hits[:3]:
            results.append({"text": h.get("text", h.get("content", ""))[:120],
                            "source": h.get("source", "index"),
                            "score": h.get("score", 0)})
    except Exception:
        pass
    return results


def _deep_search(query: str) -> list[dict]:
    """Full brain search (background, may take seconds)."""
    results = []
    try:
        from pulse.brain.brain_search import brain_search
        data = brain_search(query, max_per_source=2,
                            exclude_sources={"private"})
        for source_name, hits in data.get("results", {}).items():
            for h in hits[:2]:
                results.append({
                    "text": h.get("text", h.get("content", ""))[:120],
                    "source": source_name,
                    "score": h.get("score", 0),
                })
    except Exception:
        pass
    return results


# --- Status Reader ---

def _read_vitals() -> dict:
    """Lightweight system vitals for HUD sidebar."""
    vitals = {}
    for name, path, parser in [
        ("tasks", STATE_DIR / "task_board.json", lambda d: {
            s: sum(1 for dd in d.get("dispatches", []) for t in dd.get("tasks", [])
                   if t.get("status", "open") == s) for s in ("open", "active", "done")}),
        ("daemons", STATE_DIR / "health" / "daemon_health.json", lambda d:
            f"{sum(1 for v in d.values() if isinstance(v, dict) and v.get('status') == 'alive')}/{len(d)}"),
        ("lock_waits", STATE_DIR / "health" / "contention_tracker.json", lambda d:
            sum(v.get("total_waits", 0) for v in d.values())),
    ]:
        if path.exists():
            try:
                vitals[name] = parser(json.loads(path.read_text(encoding="utf-8")))
            except Exception:
                pass
    return vitals


# --- Terminal Rendering (rich) ---

def _render_loop(chat_lines: deque, insights: deque, running: threading.Event):
    """Main render loop using rich.Live for auto-refreshing HUD."""
    try:
        from rich.live import Live
        from rich.layout import Layout
        from rich.panel import Panel
        from rich.text import Text
        from rich.table import Table
        from rich.console import Console
    except ImportError:
        print("[HUD] rich library not found. Install: pip install rich")
        return

    console = Console()

    def _build_layout():
        layout = Layout()
        layout.split_column(
            Layout(name="header", size=3),
            Layout(name="body"),
            Layout(name="footer", size=5),
        )
        layout["body"].split_row(
            Layout(name="chat", ratio=3),
            Layout(name="insights", ratio=2),
        )
        # Header
        layout["header"].update(
            Panel(Text("PULSE Cognitive HUD — Real-Time Subconscious Stream",
                        style="bold cyan", justify="center"), style="cyan")
        )
        # Chat panel
        chat_text = Text()
        for line in list(chat_lines)[-MAX_CHAT_LINES:]:
            agent = line.get("agent", "?")
            color = "green" if agent == "CLAUDE" else "yellow" if agent == "GEMINI" else "white"
            chat_text.append(f"[{line['ts'][-8:]}] ", style="dim")
            chat_text.append(f"{agent}: ", style=f"bold {color}")
            chat_text.append(line["text"][:100] + "\n")
        layout["chat"].update(Panel(chat_text, title="Live Chat", border_style="green"))

        # Insights panel
        insight_text = Text()
        for ins in list(insights)[-MAX_INSIGHTS:]:
            src = ins.get("source", "?")[:8]
            score = ins.get("score", 0)
            color = "red" if score > 5 else "yellow" if score > 2 else "dim"
            insight_text.append(f"[{src}] ", style="bold magenta")
            insight_text.append(ins["text"][:100] + "\n", style=color)
        if not insights:
            insight_text.append("Waiting for chat activity...", style="dim italic")
        layout["insights"].update(Panel(insight_text, title="Brain Insights", border_style="magenta"))

        # Footer — vitals
        vitals = _read_vitals()
        vt = Table.grid(padding=(0, 3))
        vt.add_row(
            f"Tasks: {vitals.get('tasks', {}).get('open', 0)} open / {vitals.get('tasks', {}).get('active', 0)} active",
            f"Daemons: {vitals.get('daemons', '?')}",
            f"Lock waits: {vitals.get('lock_waits', 0)}",
            f"Insights: {len(insights)}",
        )
        layout["footer"].update(Panel(vt, title="System Vitals", border_style="blue"))
        return layout

    with Live(_build_layout(), console=console, refresh_per_second=1,
              screen=True) as live:
        while running.is_set():
            time.sleep(1.0)
            live.update(_build_layout())


# --- Main Daemon ---

def run_hud():
    """Launch the Cognitive HUD. Blocks until Ctrl+C."""
    running = threading.Event()
    running.set()

    chat_lines: deque = deque(maxlen=50)
    insights: deque = deque(maxlen=50)
    dedup = _DedupCache(ttl=300)
    positions: dict = {}
    last_deep = 0.0
    last_query = ""

    def _search_thread():
        nonlocal last_deep, last_query
        while running.is_set():
            time.sleep(POLL_INTERVAL)
            # Tail new lines
            new = _tail_capture_logs(positions)
            # Dedup capture log duplicates
            seen_texts = set()
            for line in new:
                if line["text"] in seen_texts:
                    continue
                seen_texts.add(line["text"])
                chat_lines.append(line)

            if not new:
                continue

            # Build query from recent lines
            recent_text = " ".join(
                l["text"] for l in list(chat_lines)[-5:]
            )[:500]
            if not recent_text.strip():
                continue

            # Fast search (every cycle)
            hits = _fast_search(recent_text)
            for h in hits:
                if h["text"] and dedup.is_new(h["text"]):
                    insights.append(h)

            # Deep search (periodic)
            now = time.time()
            if now - last_deep > DEEP_SEARCH_INTERVAL:
                last_deep = now
                deep = _deep_search(recent_text)
                for h in deep:
                    if h["text"] and dedup.is_new(h["text"]):
                        insights.append(h)

    search_t = threading.Thread(target=_search_thread, daemon=True, name="hud_search")
    search_t.start()

    try:
        _render_loop(chat_lines, insights, running)
    except KeyboardInterrupt:
        pass
    finally:
        running.clear()


if __name__ == "__main__":
    run_hud()
