#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Oracle Daemon — Brain V5 Phase 2

Replaces V1 heuristic concept-extraction + graph-query with LLM-based
predictive intelligence. Feeds the last N completed Swarm tasks into
llm_router.dispatch() so the LLM predicts architectural breakages and
writes insights to prediction_alerts.json.

The feedback loop (prediction_feedback.py) remains unchanged — it tracks
TP/FP/TN accuracy and decays stale predictions.

Storage: state/prediction_alerts.json
"""
import io, json, logging, os, sys, time

log = logging.getLogger("pulse.daemons.neural.prediction_daemon")
from pathlib import Path

if os.name == 'nt' and __name__ == '__main__':
    if hasattr(sys.stdout, 'reconfigure'):
        sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    if hasattr(sys.stderr, 'reconfigure'):
        sys.stderr.reconfigure(encoding='utf-8', errors='replace')

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent

if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

from pulse.core.brain_utils import (
    load_json_dict, atomic_update_json,
    TASK_BOARD_FILE, LESSONS_JSONL, FAILS_JSONL,
    read_jsonl_entries, now_iso as _now_iso,
)

# Re-export feedback functions for backward compat
from pulse.daemons.neural.prediction_feedback import (  # noqa: F401
    record_prediction_outcome, decay_stale_predictions,
    get_prediction_accuracy, get_active_predictions,
    _update_metrics,
)

STATE_DIR = ROOT_DIR / "state"
ALERTS_FILE = STATE_DIR / "prediction_alerts.json"
BRAIN_QUERY_LOG = STATE_DIR / "brain_query_log.json"
POLL_INTERVAL = 30
DECAY_INTERVAL = 3600
MAX_COMPLETED_TASKS = 10

ORACLE_PROMPT = """You are the PULSE Oracle — a predictive failure-prevention engine for a multi-agent AI system.
Analyze the recently completed Swarm tasks below. Based on the changes made, predict what architectural components, files, or subsystems are most likely to break or cause issues next.
RECENTLY COMPLETED TASKS:
{tasks}
RECENT FAILURES/LESSONS (from brain):
{brain_context}
YOUR PAST PREDICTION ACCURACY:
{accuracy_context}
Based on this activity, predict 1-5 specific risks. For each risk:
- What might break and why
- How confident you are (0.3-0.95) — calibrate using your accuracy stats above
- What file(s) or component(s) are at risk
OUTPUT strict JSON only (no markdown fences):
{{"predictions": [{{"prediction": "description of what might break and why", "confidence": 0.7, "risk_components": ["file_or_component"], "severity": "low|medium|high"}}]}}
If everything looks stable: {{"predictions": []}}"""
def _get_completed_tasks(limit: int = MAX_COMPLETED_TASKS) -> list:
    board = load_json_dict(TASK_BOARD_FILE)
    completed = [t for d in board.get("dispatches", []) for t in d.get("tasks", []) if t.get("status") == "done"]
    completed.sort(key=lambda t: t.get("completed_at", t.get("claimed_at", "")), reverse=True)
    return completed[:limit]

def _format_tasks_for_llm(tasks: list) -> str:
    """Format completed tasks as concise text for the LLM prompt."""
    lines = []
    for t in tasks:
        title = t.get("title", "unknown")[:100]
        outcome = t.get("outcome", "")[:200]
        files = ", ".join(t.get("files_touched", [])[:5])
        domain = t.get("domain", "general")
        lines.append(
            f"- [{domain}] {title}\n"
            f"  Outcome: {outcome or 'completed'}\n"
            f"  Files: {files or 'none recorded'}"
        )
    return "\n".join(lines) if lines else "No recently completed tasks."

def _load_brain_context() -> str:
    """Load recent failures and lessons for LLM context — JSONL first, MD fallback."""
    from pulse.core.brain_utils import HIPPOCAMPUS_DIR

    context_parts = []
    for name, jsonl_path in [
        ("Failures", FAILS_JSONL),
        ("Lessons", LESSONS_JSONL),
    ]:
        rows = read_jsonl_entries(jsonl_path)
        if rows:
            titles = [(r.get("title") or r.get("content", ""))[:120] for r in rows[-40:]]
            content = "\n".join(f"- {t}" for t in titles if t)
            if content:
                context_parts.append(f"## Recent {name}:\n{content}")

    return "\n\n".join(context_parts) if context_parts else "No recent brain data."

def _get_recent_query_log(limit: int = 10) -> list:
    """Read recent brain queries — breaks task board circular dependency."""
    if not BRAIN_QUERY_LOG.exists(): return []
    try:
        raw = json.loads(BRAIN_QUERY_LOG.read_text(encoding="utf-8"))
        return raw[-limit:][::-1] if isinstance(raw, list) else []
    except Exception: return []

def _format_query_log_for_llm(entries: list) -> str:
    """Format query log for LLM context."""
    return "\n".join(f"- [{e.get('timestamp','')[:19]}] '{e.get('query','')[:100]}' n={e.get('results',0)}" for e in entries) if entries else ""

def _get_recent_failures_from_brain(limit: int = 5) -> list:
    """SELECT recent failures from SQLite — bypasses empty task board."""
    try:
        from pulse.core.brain_db import get_conn
        rows = get_conn().execute("SELECT title, content, confidence, timestamp FROM failures ORDER BY timestamp DESC LIMIT ?", (limit,)).fetchall()
        return [{"title": r[0] or "", "content": r[1] or "", "confidence": r[2] or 0.0, "timestamp": r[3] or ""} if isinstance(r, (tuple, list))
                else {"title": r["title"] or "", "content": r["content"] or "", "confidence": r["confidence"] or 0.0, "timestamp": r["timestamp"] or ""}
                for r in rows]
    except Exception: return []

# ── Core Prediction Logic (V5: LLM-based) ─────────────────────────────────────

def _format_accuracy_context() -> str:
    """Format prediction accuracy stats for Oracle self-calibration."""
    try:
        overall = get_prediction_accuracy()
        from pulse.daemons.neural.prediction_feedback import get_domain_accuracy
        domain_data = get_domain_accuracy()
    except Exception:
        return "No prediction history yet — this is your first run."
    if overall["total"] == 0:
        return "No prediction history yet — this is your first run."
    lines = [f"Overall: {overall['accuracy']:.0%} accurate ({overall['correct']}/{overall['total']} predictions)"]
    by_conf = domain_data.get("by_confidence", {})
    for bucket in ("0.3-0.5", "0.5-0.7", "0.7-1.0"):
        bd = by_conf.get(bucket, {})
        if bd.get("total", 0) > 0:
            acc = bd["correct"] / bd["total"]
            lines.append(f"  Confidence {bucket}: {acc:.0%} accurate ({bd['correct']}/{bd['total']})")
    domains = domain_data.get("domains", {})
    if domains:
        for dom, stats in sorted(domains.items(), key=lambda x: -x[1].get("total", 0))[:5]:
            lines.append(f"  Domain '{dom}': {stats['accuracy']:.0%} ({stats['correct']}/{stats['total']})")
    if overall["accuracy"] < 0.4:
        lines.append("WARNING: Your accuracy is low. Be more conservative with confidence scores.")
    elif overall["accuracy"] > 0.8:
        lines.append("Your accuracy is strong. Maintain calibration.")
    return "\n".join(lines)

def generate_predictions(verbose=False) -> list:
    """Generate prediction alerts by feeding completed tasks to the LLM Oracle.

    P2-2: Falls back to brain activity (queries + failures) when task board is empty.
    """
    completed = _get_completed_tasks()
    if completed:
        tasks_text = _format_tasks_for_llm(completed)
    else:
        # P2-2: break circular dependency — use brain activity instead of task board
        qlog, fails = _get_recent_query_log(10), _get_recent_failures_from_brain(5)
        if not qlog and not fails:
            return []
        parts = []
        if qlog: parts.append("RECENT QUERIES:\n" + _format_query_log_for_llm(qlog))
        if fails:
            parts.append("RECENT FAILURES:\n" + "\n".join(
                f"- [{f.get('timestamp','')[:19]}] {(f.get('title') or f.get('content',''))[:100]}" for f in fails))
        tasks_text = "\n\n".join(parts)

    brain_context = _load_brain_context()
    accuracy_context = _format_accuracy_context()
    prompt = ORACLE_PROMPT.format(tasks=tasks_text, brain_context=brain_context[:4000],
                                  accuracy_context=accuracy_context)

    try:
        from pulse.core.llm_router import dispatch, extract_json
        raw = dispatch(prompt, task_type="standard")
        result = extract_json(raw)
    except Exception as e:
        if verbose:
            print(f"[ORACLE] LLM dispatch failed: {e}")
        return []

    predictions = result.get("predictions", [])
    if not predictions:
        return []
    # Convert to alert format
    alerts_data = load_json_dict(ALERTS_FILE)
    existing_texts = {
        a.get("prediction", "")[:60].lower()
        for a in alerts_data.get("alerts", [])
        if a.get("status") == "active"
    }

    new_alerts = []
    for p in predictions:
        if not isinstance(p, dict):
            continue
        pred_text = p.get("prediction", "")
        if not pred_text or pred_text[:60].lower() in existing_texts:
            continue

        confidence = p.get("confidence", 0.5)
        if not isinstance(confidence, (int, float)):
            confidence = 0.5
        confidence = max(0.3, min(0.95, confidence))

        alert = {
            "task_id": f"oracle_{int(time.time())}_{len(new_alerts)}",
            "task_title": f"[ORACLE] {pred_text[:80]}",
            "prediction": pred_text[:500],
            "confidence": round(confidence, 2),
            "risk_components": p.get("risk_components", [])[:5],
            "severity": p.get("severity", "medium"),
            "risk_count": 1,
            "evidence_chain": ([t.get("title", "")[:60] for t in completed[:3]] if completed else ([e.get("query", "")[:60] for e in qlog[:3]] if qlog else [f.get("title", f.get("content", ""))[:60] for f in fails[:3]])),
            "timestamp": _now_iso(),
            "status": "active",
            "source": "oracle_v5",
        }
        new_alerts.append(alert)
    if new_alerts:
        def _updater(data):
            if not isinstance(data, dict):
                data = {"alerts": [], "total_generated": 0}
            if "alerts" not in data:
                data["alerts"] = []
            data["alerts"].extend(new_alerts)
            data["alerts"] = data["alerts"][-50:]
            data["total_generated"] = data.get("total_generated", 0) + len(new_alerts)
            data["last_updated"] = _now_iso()
            return data
        ALERTS_FILE.parent.mkdir(parents=True, exist_ok=True)
        atomic_update_json(ALERTS_FILE, _updater, default={"alerts": [], "total_generated": 0})

    return new_alerts

# ── Daemon Loop ───────────────────────────────────────────────────────────────

def _decay_no_feedback_predictions():
    try:
        from pulse.core.brain_db import get_conn
        from datetime import datetime, timezone, timedelta
        cutoff = (datetime.now(timezone.utc) - timedelta(hours=48)).isoformat()
        conn = get_conn()
        tbl_check = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='predictions'").fetchone()
        if not tbl_check:
            return
        with conn:
            conn.execute("UPDATE predictions SET confidence=MAX(0.1, confidence-0.1) WHERE confidence>0.3 AND timestamp<? AND id NOT IN (SELECT DISTINCT item_key FROM retrieval_metrics WHERE last_accessed>?)", (cutoff, cutoff))
    except Exception as _e:
        log.debug("_decay_no_feedback_predictions failed: %s", _e)

def run_daemon(verbose=False):
    """Run prediction daemon as a polling loop."""
    last_decay = 0.0
    while True:
        try:
            generate_predictions(verbose=verbose)
            if time.time() - last_decay >= DECAY_INTERVAL:
                decay_stale_predictions()
                _decay_no_feedback_predictions()  # Item 79
                last_decay = time.time()
        except Exception:
            pass
        time.sleep(POLL_INTERVAL)

if __name__ == "__main__":
    _a = sys.argv
    if "--once" in _a: print(json.dumps(generate_predictions(verbose=True), indent=2))
    elif "--status" in _a:
        for a in get_active_predictions(): print(f"  [{a.get('task_id','?')}] {a.get('prediction','')[:80]}")
    else: run_daemon(verbose="--verbose" in _a or "-v" in _a)
