#!/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
import json
import os
import sys
import time
from pathlib import Path

if os.name == 'nt' and __name__ == '__main__':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

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,
    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"
POLL_INTERVAL = 30
DECAY_INTERVAL = 3600
MAX_COMPLETED_TASKS = 10

# ── LLM Prompt ────────────────────────────────────────────────────────────────

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}

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)
- 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": []}}"""


# ── Task Board Queries ────────────────────────────────────────────────────────

def _get_active_tasks() -> list:
    """Get all actively claimed tasks from the board."""
    board = load_json_dict(TASK_BOARD_FILE)
    tasks = []
    for d in board.get("dispatches", []):
        for t in d.get("tasks", []):
            if t.get("status") == "active":
                tasks.append(t)
    return tasks


def _get_completed_tasks(limit: int = MAX_COMPLETED_TASKS) -> list:
    """Get the most recently completed tasks from the board."""
    board = load_json_dict(TASK_BOARD_FILE)
    completed = []
    for d in board.get("dispatches", []):
        for t in d.get("tasks", []):
            if t.get("status") == "done":
                completed.append(t)

    # Sort by completion time (most recent first)
    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."""
    from pulse.core.brain_utils import HIPPOCAMPUS_DIR

    context_parts = []
    for name, rel_path in [
        ("Failures", "Failures/auto_fails.md"),
        ("Lessons", "Lessons/auto_lessons.md"),
    ]:
        fp = HIPPOCAMPUS_DIR / rel_path
        if fp.exists():
            content = fp.read_text(encoding="utf-8", errors="replace")
            # Take last 2000 chars (most recent entries)
            if len(content) > 2000:
                content = content[-2000:]
            context_parts.append(f"## Recent {name}:\n{content}")

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


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

def generate_predictions(verbose=False) -> list:
    """Generate prediction alerts by feeding completed tasks to the LLM Oracle."""
    completed = _get_completed_tasks()
    if not completed:
        if verbose:
            print("[ORACLE] No completed tasks to analyze")
        return []

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

    try:
        from pulse.core.llm_router import dispatch, extract_json

        raw = dispatch(prompt, task_type="fast")
        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:
        if verbose:
            print("[ORACLE] LLM predicts all clear — no risks detected")
        return []

    # Convert LLM predictions 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]],
            "timestamp": _now_iso(),
            "status": "active",
            "source": "oracle_v5",
        }
        new_alerts.append(alert)
        if verbose:
            print(f"  [!] PREDICTION (conf={confidence}): {pred_text[:100]}")

    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 run_daemon(verbose=False):
    """Run prediction daemon as a polling loop."""
    print("[ORACLE] Oracle Daemon V5 starting...")
    print(f"[ORACLE] Polling every {POLL_INTERVAL}s, decay every {DECAY_INTERVAL}s")
    last_decay = 0.0
    while True:
        try:
            new_alerts = generate_predictions(verbose=verbose)
            if new_alerts and verbose:
                print(f"[ORACLE] Generated {len(new_alerts)} new prediction(s)")
            now = time.time()
            if now - last_decay >= DECAY_INTERVAL:
                decayed, archived = decay_stale_predictions()
                if (decayed or archived) and verbose:
                    print(f"[ORACLE] Decay: {decayed} updated, {archived} expired")
                last_decay = now
        except Exception as e:
            if verbose:
                print(f"[ORACLE] Error: {e}")
        time.sleep(POLL_INTERVAL)


if __name__ == "__main__":
    verbose = "--verbose" in sys.argv or "-v" in sys.argv
    if "--once" in sys.argv:
        alerts = generate_predictions(verbose=True)
        print(f"\n[ORACLE] Generated {len(alerts)} prediction(s)")
        if alerts:
            print(json.dumps(alerts, indent=2))
    elif "--status" in sys.argv:
        active = get_active_predictions()
        print(f"Active predictions: {len(active)}")
        for a in active:
            print(f"  [{a.get('task_id', '?')}] (conf={a.get('confidence', 0)}) {a.get('prediction', '')[:80]}")
    else:
        run_daemon(verbose=verbose)
