#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""D11: Ghost Resonance — Agent Mortality Proceduralizer.

Pipeline: DETECT → EXTRACT → PROCEDURALIZE → STORE. 1h interval via orchestrator.
"""
from __future__ import annotations
import io, json, logging, os, re, sys, time
from datetime import datetime, timezone
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')

from pulse.core.paths import get_root_dir
from pulse.core.brain_utils import (
    HIPPOCAMPUS_DIR, LESSONS_JSONL, FAILS_JSONL,
    read_jsonl_entries, now_iso,
)
from pulse.core.brain_io import _acquire

ROOT = get_root_dir()
STATE_FILE = ROOT / "state" / "ghost_resonance_state.json"
ALERTS_FILE = ROOT / "state" / "supervisor_alerts.json"
SESSIONS_FILE = HIPPOCAMPUS_DIR / "Evidence" / "Sessions" / "agent_sessions.json"
PROCEDURES_FILE = HIPPOCAMPUS_DIR / "Evidence" / "Metrics" / "procedural_log.json"

MIN_SESSIONS, MIN_LEARNINGS, MIN_STRONG_DOMAINS = 3, 5, 1
MAX_GHOST_PROCEDURES, STALE_ALERT_DAYS = 8, 7
_DEFAULT_STATE = {"total_ghosts": 0, "total_procedures": 0, "processed_deaths": [],
                  "last_run": None, "runs": 0, "last_elapsed_s": 0}

GHOST_PROMPT = """Analyze a dead AI agent's expertise and create step-by-step procedures for successors.

DEAD AGENT: {worker_id} | Model: {model} | Sessions: {session_count} | Success: {success_rate}
Strong domains: {strong_domains}

LEARNINGS:
{learnings}

FAILURES:
{failures}

Create {max_procs} procedures capturing this agent's domain expertise. Successors following these steps should avoid the failures and replicate successes.

Output ONLY valid JSON:
{{"procedures": [{{"name": "Name", "description": "When to use", "steps": ["Step 1", "Step 2", "Step 3"], "domain": "category", "confidence": 0.7}}]}}

Rules: non-empty "steps" (2-6 items), domain in (architecture|testing|deployment|security|brain|agents|swarm|general), confidence 0.5-0.9, focus on STRONG domains, be specific not vague. JSON only."""


def _load_state() -> dict:
    if not STATE_FILE.exists():
        return dict(_DEFAULT_STATE)
    try:
        with _acquire(STATE_FILE):
            return json.loads(STATE_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return dict(_DEFAULT_STATE)


def _save_state(state: dict):
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    with _acquire(STATE_FILE):
        STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")


def _load_json_list(path: Path) -> list[dict]:
    """Load a JSON file expected to be a list at root."""
    if not path.exists():
        return []
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        return data if isinstance(data, list) else []
    except (json.JSONDecodeError, OSError):
        return []


def _grep_jsonl_for_agent(jsonl_path: Path, worker_id: str, max_items: int = 30) -> list[str]:
    """Extract titles attributed to a specific agent from a JSONL brain file."""
    rows = read_jsonl_entries(jsonl_path)
    if not rows:
        return []
    pat = re.compile(re.escape(worker_id), re.IGNORECASE)
    matches = []
    for r in rows:
        if pat.search(r.get("agent", "")) or pat.search(r.get("content", "")) or pat.search(r.get("title", "")):
            t = (r.get("title") or r.get("content", ""))[:200].strip()
            if t:
                matches.append(t)
    return matches[-max_items:]


def _load_procedures() -> dict:
    """Load procedural_log.json (dict with 'procedures' list)."""
    default = {"procedures": [], "last_updated": now_iso()}
    if not PROCEDURES_FILE.exists():
        return default
    try:
        data = json.loads(PROCEDURES_FILE.read_text(encoding="utf-8"))
        if isinstance(data, list):
            return {"procedures": data, "last_updated": now_iso()}
        return data if isinstance(data, dict) else default
    except (json.JSONDecodeError, OSError):
        return default


def _detect_deaths(state: dict) -> list[dict]:
    """Find unprocessed death alerts from supervisor_alerts.json."""
    processed, now, fresh = set(state.get("processed_deaths", [])), datetime.now(timezone.utc), []
    for alert in _load_json_list(ALERTS_FILE):
        wid = alert.get("worker_id", "")
        if not wid or wid in processed or alert.get("status") != "abandoned":
            continue
        ts = alert.get("timestamp", "")
        if ts:
            try:
                if (now - datetime.fromisoformat(ts.replace("Z", "+00:00"))).days > STALE_ALERT_DAYS:
                    continue
            except (ValueError, TypeError):
                pass
        fresh.append(alert)
    return fresh


def _extract_ghost_knowledge(worker_id: str) -> dict | None:
    """Gather the dead agent's accumulated knowledge. Returns None if insufficient."""
    from pulse.brain.metacognition.agent_procedures import get_agent_profile, get_strong_domains
    profile, strong = get_agent_profile(worker_id), get_strong_domains(worker_id)
    sessions = [s for s in _load_json_list(SESSIONS_FILE)
                if (s.get("agent") or "").lower() == worker_id.lower()]
    session_learnings, session_failures = [], []
    for sess in sessions:
        session_learnings.extend(sess.get("learnings", []))
        session_failures.extend(sess.get("failures", []))
    all_learnings = session_learnings + _grep_jsonl_for_agent(LESSONS_JSONL, worker_id, 30)
    all_failures = session_failures + _grep_jsonl_for_agent(FAILS_JSONL, worker_id, 15)
    session_count = max(len(sessions), profile.get("total_tasks", 0))
    if session_count < MIN_SESSIONS or len(all_learnings) < MIN_LEARNINGS or len(strong) < MIN_STRONG_DOMAINS:
        return None
    return {"profile": profile, "strong_domains": strong,
            "learnings": all_learnings[-30:], "failures": all_failures[-15:],
            "session_count": session_count}


def _synthesize_procedures(worker_id: str, knowledge: dict) -> list[dict]:
    """LLM-synthesize ghost procedures from extracted knowledge."""
    from pulse.core.llm_router import dispatch, extract_json
    profile = knowledge["profile"]
    strong_names = [d["domain"] for d in knowledge["strong_domains"][:5]]
    prompt = GHOST_PROMPT.format(
        worker_id=worker_id, model=profile.get("model", "unknown"),
        session_count=knowledge["session_count"],
        strong_domains=", ".join(strong_names) or "general",
        success_rate=f"{profile.get('overall_success_rate', 0):.0%}",
        learnings="\n".join(f"- {l}" for l in knowledge["learnings"][-20:]) or "(none)",
        failures="\n".join(f"- {f}" for f in knowledge["failures"][-10:]) or "(none)",
        max_procs=min(MAX_GHOST_PROCEDURES, max(3, len(strong_names) * 2)))
    try:
        result = extract_json(dispatch(prompt, task_type="fast"))
    except Exception as e:
        logging.getLogger(__name__).warning("ghost synthesis failed: %s", e)
        return []
    valid = []
    for proc in result.get("procedures", []):
        name = (proc.get("name") or "").strip()
        steps = [s for s in proc.get("steps", []) if isinstance(s, str) and s.strip()]
        if not name or not steps:
            continue
        proc.update(steps=steps)
        proc.setdefault("confidence", 0.7); proc.setdefault("domain", "general"); proc.setdefault("description", "")
        valid.append(proc)
    return valid[:MAX_GHOST_PROCEDURES]


def _store_ghost_procedures(worker_id: str, knowledge: dict, raw_procs: list[dict]) -> int:
    """Append ghost procedures to procedural_log.json. Returns count stored."""
    if not raw_procs:
        return 0
    profile, strong_names = knowledge["profile"], [d["domain"] for d in knowledge["strong_domains"][:5]]
    ghost_meta = {"ghost_agent": worker_id, "ghost_model": profile.get("model", "unknown"),
                  "ghost_strong_domains": strong_names, "ghost_sessions": knowledge["session_count"],
                  "ghost_success_rate": profile.get("overall_success_rate", 0)}
    with _acquire(PROCEDURES_FILE):
        current = _load_procedures()
        procs = current.get("procedures", [])
        existing_names = {(p.get("name") or "").lower() for p in procs}
        added = 0
        for proc in raw_procs:
            name = proc["name"]
            if name.lower() in existing_names:
                continue
            entry = {"name": name, "description": proc.get("description", ""),
                     "steps": proc["steps"], "domain": proc.get("domain", "general"),
                     "confidence": proc.get("confidence", 0.7), "created": now_iso(),
                     "source": "ghost_resonance", "executions": 0, "successes": 0, "failures": 0}
            entry.update(ghost_meta)
            procs.append(entry)
            existing_names.add(name.lower())
            added += 1
        current["procedures"], current["last_updated"] = procs, now_iso()
        PROCEDURES_FILE.parent.mkdir(parents=True, exist_ok=True)
        # P47: SQLite-first — upsert ONLY the entries added in this call
        new_entries = procs[-added:] if added > 0 else []
        try:
            from pulse.brain.sqlite_writers import upsert_procedure
            for entry in new_entries:
                upsert_procedure(
                    name=entry["name"],
                    description=entry.get("description", ""),
                    steps=entry.get("steps", []),
                    domain=entry.get("domain", "general"),
                    confidence=float(entry.get("confidence", 0.7)),
                    source="ghost_resonance",
                )
        except Exception as _e:
            logging.getLogger("pulse.ghost_resonance").warning("upsert_procedure failed: %s", _e)
        PROCEDURES_FILE.write_text(json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8")
    return added


def run_once(verbose: bool = False, dry_run: bool = False) -> dict:
    """Full D11 cycle: DETECT → EXTRACT → PROCEDURALIZE → STORE."""
    state, t0 = _load_state(), time.time()
    vp = print if verbose else lambda *a: None

    deaths = _detect_deaths(state)
    vp(f"[D11] {len(deaths)} unprocessed death(s)")

    if not deaths:
        state.update(last_run=datetime.now(timezone.utc).isoformat(), last_elapsed_s=round(time.time() - t0, 1))
        state["runs"] = state.get("runs", 0) + 1; _save_state(state)
        return {"deaths": 0, "ghosts": 0, "procedures": 0}
    ghosts_created = procedures_added = 0
    for alert in deaths:
        wid = alert.get("worker_id", "")
        vp(f"[D11] Processing: {wid} ({alert.get('reason', '?')})")
        knowledge = _extract_ghost_knowledge(wid)
        if not knowledge:
            vp(f"[D11]   Insufficient data — skipping")
            state.setdefault("processed_deaths", []).append(wid)
            continue

        vp(f"[D11]   {len(knowledge['learnings'])} learnings, "
           f"{len(knowledge['failures'])} failures, {len(knowledge['strong_domains'])} domains")

        if dry_run:
            vp(f"[D11]   [DRY RUN] Would synthesize for {wid}")
            state.setdefault("processed_deaths", []).append(wid)
            ghosts_created += 1
            continue

        procs = _synthesize_procedures(wid, knowledge)
        stored = _store_ghost_procedures(wid, knowledge, procs)
        vp(f"[D11]   {len(procs)} synthesized, {stored} stored")
        state.setdefault("processed_deaths", []).append(wid)
        ghosts_created += 1
        procedures_added += stored

    state.update(total_ghosts=state.get("total_ghosts", 0) + ghosts_created,
                 total_procedures=state.get("total_procedures", 0) + procedures_added,
                 last_run=datetime.now(timezone.utc).isoformat(),
                 last_elapsed_s=round(time.time() - t0, 1))
    state["runs"] = state.get("runs", 0) + 1
    _save_state(state)
    summary = {"deaths": len(deaths), "ghosts": ghosts_created, "procedures": procedures_added}
    vp(f"[D11] Done: {summary}")
    return summary


def show_status():
    """Print D11 ghost resonance stats."""
    s = _load_state()
    print(f"\n  D11 Ghost Resonance\n  {'=' * 40}")
    for k, v in [("Runs", s.get("runs", 0)),
                 ("Last run", (s.get("last_run") or "never")[:19]),
                 ("Ghosts", f"{s.get('total_ghosts', 0)} processed"),
                 ("Procedures", f"{s.get('total_procedures', 0)} generated"),
                 ("Deaths", f"{len(s.get('processed_deaths', []))} tracked"),
                 ("Elapsed", f"{s.get('last_elapsed_s', 0)}s")]:
        print(f"  {k + ':':<14} {v}")


def cmd_ghost(args: list):
    """CLI entry: pulse ghost [--dry-run] [-v] [status]."""
    if args and args[0] == "status":
        return show_status()
    run_once(verbose=("-v" in args or "--verbose" in args), dry_run="--dry-run" in args)


if __name__ == "__main__":
    cmd_ghost(sys.argv[1:])
