# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
D20: Swarm Immunity Daemon — Cross-Agent Fatal Trap Inoculation.

When workers crash, captures final log context, synthesizes "Fatal Trap"
lessons with high salience, and injects them into the brain so new agents
spawn pre-inoculated against the exact pitfalls that killed their peers.

Source: DMN session_20260227T033542 | Usage: pulse immunity
"""

import json
import logging
import time
from datetime import datetime, timezone
from pathlib import Path

_log = logging.getLogger("pulse.d20_immunity")

from pulse.core.brain_utils import STATE_DIR, LESSONS_JSONL, FAILS_JSONL

ALERTS_FILE = STATE_DIR / "supervisor_alerts.json"
WORKER_LOGS_DIR = STATE_DIR / "worker_logs"
IMMUNITY_FILE = STATE_DIR / "health" / "immunity_tracker.json"
_FATAL_TRAP_SALIENCE = 7.0    # high salience = surfaces in every brain search
_FATAL_TRAP_CONFIDENCE = 0.85  # high confidence = trusted immediately
_LOG_TAIL_LINES = 80          # capture last N lines from dead worker's log


def _load_alerts() -> list[dict]:
    """Load supervisor alerts (abandoned workers)."""
    if not ALERTS_FILE.exists():
        return []
    try:
        data = json.loads(ALERTS_FILE.read_text(encoding="utf-8"))
        return data if isinstance(data, list) else []
    except (json.JSONDecodeError, OSError):
        return []


def _load_immunity() -> dict:
    """Load immunity tracker state."""
    if not IMMUNITY_FILE.exists():
        return {"processed": [], "traps": 0, "runs": 0}
    try:
        return json.loads(IMMUNITY_FILE.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"processed": [], "traps": 0, "runs": 0}


def _save_immunity(state: dict):
    """Save immunity tracker state atomically."""
    import os
    IMMUNITY_FILE.parent.mkdir(parents=True, exist_ok=True)
    tmp = IMMUNITY_FILE.with_suffix(".tmp")
    try:
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(state, f, indent=2, ensure_ascii=False)
        os.replace(str(tmp), str(IMMUNITY_FILE))
    except OSError:
        if tmp.exists():
            tmp.unlink()


def _detect_new_deaths(immunity: dict) -> list[dict]:
    """Find supervisor alerts not yet processed by immunity daemon."""
    processed = set(immunity.get("processed", []))
    alerts = _load_alerts()
    new = []
    for a in alerts:
        wid = a.get("worker_id", "")
        if not wid or wid in processed:
            continue
        if a.get("status") != "abandoned":
            continue
        new.append(a)
    return new


def _capture_crash_context(worker_id: str) -> str:
    """Grab final lines from dead worker's log file."""
    log_file = WORKER_LOGS_DIR / f"{worker_id}.log"
    if not log_file.exists():
        return ""
    try:
        with open(log_file, "r", encoding="utf-8", errors="replace") as f:
            lines = f.readlines()
        tail = lines[-_LOG_TAIL_LINES:] if len(lines) > _LOG_TAIL_LINES else lines
        return "".join(tail).strip()
    except OSError:
        return ""


def _extract_task_context(worker_id: str) -> dict:
    """Extract the task the worker was working on when it died."""
    tb_path = STATE_DIR / "task_board.json"
    if not tb_path.exists():
        return {}
    try:
        data = json.loads(tb_path.read_text(encoding="utf-8"))
        for d in data.get("dispatches", []):
            for t in d.get("tasks", []):
                if t.get("claimed_by") == worker_id and t.get("status") == "active":
                    return {"task_id": t.get("task_id", ""),
                            "title": t.get("title", ""),
                            "domain": t.get("domain", "general")}
    except (json.JSONDecodeError, OSError):
        pass
    return {}


def _synthesize_fatal_trap(alert: dict, crash_log: str, task_ctx: dict) -> dict | None:
    """Synthesize a Fatal Trap lesson from crash context.

    Returns a structured lesson dict ready for brain injection, or None
    if insufficient context to learn from.
    """
    worker_id = alert.get("worker_id", "unknown")
    reason = alert.get("reason", "unknown")
    respawns = alert.get("respawn_count", 0)
    domain = task_ctx.get("domain", "general")
    task_title = task_ctx.get("title", "")

    # Extract meaningful failure signal from crash log
    error_lines = []
    for line in crash_log.split("\n"):
        low = line.lower()
        if any(k in low for k in ("error", "exception", "traceback", "failed",
                                    "timeout", "killed", "oom", "crash")):
            error_lines.append(line.strip()[:200])

    if not error_lines and not task_title:
        return None  # no useful context to learn from

    # Build the fatal trap lesson
    error_summary = "; ".join(error_lines[:5]) if error_lines else f"Worker died: {reason}"
    trap_content = (
        f"[FATAL TRAP] Worker {worker_id} crashed after {respawns} respawns. "
        f"Cause: {error_summary[:300]}."
    )
    if task_title:
        trap_content += f" Task: {task_title}."

    import hashlib
    trap_id = f"FT-{hashlib.md5(trap_content.encode()[:200]).hexdigest()[:8]}"
    now = datetime.now(timezone.utc).isoformat()

    return {
        "id": trap_id,
        "fingerprint": hashlib.md5(trap_content.encode()[:120]).hexdigest()[:16],
        "type": "failure",
        "timestamp": now,
        "agent": worker_id,
        "content": trap_content,
        "title": trap_content[:120],
        "domain": domain,
        "salience": _FATAL_TRAP_SALIENCE,
        "confidence": _FATAL_TRAP_CONFIDENCE,
        "consolidation_count": 1,
        "evidence_count": 1,
        "tags": ["fatal_trap", "immunity", "crash"],
        "_provenance": {
            "source_agent": worker_id,
            "source_daemon": "d20_swarm_immunity",
            "crash_reason": reason,
            "respawn_count": respawns,
            "domain": domain,
        },
    }


def _inject_to_brain(trap: dict) -> bool:
    """Write fatal trap lesson to brain DB."""
    try:
        from pulse.core.brain_db import insert_knowledge
        insert_knowledge("failures", trap)
        return True
    except Exception as e:
        _log.warning("Failed to inject fatal trap: %s", e)
        return False


def _inject_to_hot_cache(trap: dict):
    """Push fatal trap to hot cache for instant retrieval."""
    try:
        from pulse.brain.memory.hot_cache import write_hot
        write_hot({
            "content": trap["content"],
            "source": "fatal_trap",
            "score": trap["salience"],
            "timestamp": trap["timestamp"],
        })
    except Exception:
        pass  # hot_cache is optional


def run_once(verbose: bool = False) -> dict:
    """Run one immunity cycle: detect deaths → synthesize traps → inject."""
    immunity = _load_immunity()
    deaths = _detect_new_deaths(immunity)

    if not deaths:
        if verbose:
            _log.info("No new worker deaths to process")
        return {"deaths": 0, "traps": 0, "status": "no_deaths"}

    if verbose:
        _log.info("Processing %d new worker death(s)", len(deaths))

    traps_created = 0
    for alert in deaths:
        wid = alert.get("worker_id", "")

        # Capture crash context
        crash_log = _capture_crash_context(wid)
        task_ctx = _extract_task_context(wid)

        # Synthesize fatal trap
        trap = _synthesize_fatal_trap(alert, crash_log, task_ctx)
        if trap:
            written = _inject_to_brain(trap)
            if written:
                _inject_to_hot_cache(trap)
                traps_created += 1
                if verbose:
                    _log.info("  [TRAP] %s → %s", wid, trap["content"][:80])

        # Mark as processed regardless (avoid re-processing)
        immunity.setdefault("processed", []).append(wid)

    # Cap processed list to prevent unbounded growth
    immunity["processed"] = immunity["processed"][-200:]
    immunity["traps"] = immunity.get("traps", 0) + traps_created
    immunity["runs"] = immunity.get("runs", 0) + 1
    immunity["last_run"] = datetime.now(timezone.utc).isoformat()
    _save_immunity(immunity)

    if verbose:
        _log.info("Immunity cycle done: %d deaths, %d traps created", len(deaths), traps_created)

    return {"deaths": len(deaths), "traps": traps_created, "status": "ok"}


def cmd_immunity(args: list):
    """CLI entry point for `pulse immunity`."""
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    if args and args[0] == "status":
        s = _load_immunity()
        print(f"[D20] Swarm Immunity — Runs: {s.get('runs', 0)}, "
              f"Traps: {s.get('traps', 0)}, "
              f"Deaths processed: {len(s.get('processed', []))}")
        print(f"  Last run: {s.get('last_run', 'never')}")
        return
    result = run_once(verbose=True)
    print(json.dumps(result, indent=2))


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